Skip to content

Comments

feat(gui): add Tauri v2 desktop GUI application#17

Merged
clroot merged 19 commits intomainfrom
feat/tauri-gui
Feb 20, 2026
Merged

feat(gui): add Tauri v2 desktop GUI application#17
clroot merged 19 commits intomainfrom
feat/tauri-gui

Conversation

@clroot
Copy link
Owner

@clroot clroot commented Feb 20, 2026

Summary

  • Tauri v2 + React + Tailwind CSS v4 + shadcn/ui 기반 데스크톱 GUI 앱
  • 이미지 변환(convert), 최적화(optimize), 리사이즈(resize), 크롭(crop), 확장(extend) 지원
  • 단일/배치 파일 처리, 폴더 드래그앤드롭, 실시간 미리보기
  • 크로스 플랫폼 릴리스 CI (macOS ARM/Intel, Linux, Windows)

Changes

GUI Application (gui/)

  • Tauri v2 + Vite + React 프로젝트 스캐폴딩
  • Rust IPC 명령어: load_image, process_image, preview_image, process_batch, scan_directory
  • 모든 Tauri 명령어를 spawn_blocking으로 비동기 처리 (UI 프리징 방지)
  • 컴포넌트: Sidebar, DropZone, OptionsPanel, ImagePreview, ProcessResultCard, BatchList, Settings
  • 폴더 단위 입력 지원 (드래그앤드롭 + 폴더 브라우저)
  • 처리 옵션 UI에 overwrite 체크박스 추가

CI (release-gui.yml)

  • tauri-apps/tauri-action으로 4개 플랫폼 빌드
  • 플랫폼별 네이티브 의존성 설정 (libjxl, dav1d, mozjpeg)
  • gui-v* 태그 푸시 시 Draft Release 자동 생성

Test plan

  • macOS에서 cd gui && bun run tauri dev로 앱 실행 확인
  • 이미지 드래그앤드롭 및 폴더 입력 테스트
  • convert/optimize/resize/crop/extend 각 기능 동작 확인
  • 배치 처리 (여러 파일 동시 처리) 테스트
  • overwrite 옵션 동작 확인
  • release-gui.yml 워크플로우 빌드 성공 확인

Tauri v2 + React + Tailwind + shadcn/ui 기반 데스크톱 GUI 설계.
사이드바 네비게이션 구조로 5개 기능 전체 지원, 단일/배치 처리,
전/후 비교 미리보기 포함.
10단계 구현 계획: 스캐폴딩 → Tailwind/shadcn → Rust IPC →
Sidebar → DropZone → Options → 처리 실행 → 미리보기 → 배치 → Settings
Add Tauri v2 with React + TypeScript frontend scaffolding:
- gui/src-tauri: Rust backend with slimg-core dependency
- gui/: React + Vite frontend with bun package manager
- Add gui/src-tauri to Cargo workspace members
- Configure product name as "slimg" with io.clroot.slimg identifier
- Install Tailwind CSS v4 with Vite plugin (no PostCSS config needed)
- Initialize shadcn/ui with New York style, Zinc base color, CSS variables
- Add shadcn components: button, select, slider, input, label,
  radio-group, checkbox, progress, separator, tooltip
- Configure path alias (@/) in tsconfig.json and vite.config.ts
- Remove scaffold boilerplate: greet command, demo SVGs, App.css
- Remove tauri-plugin-opener dependency (Rust + JS)
- Update index.html title to "slimg"
Add 4 Tauri commands bridging the frontend to slimg-core:
- load_image: decode file and return metadata with base64 thumbnail
- process_image: apply pipeline (convert/optimize/resize/crop/extend) and save
- preview_image: same as process_image but returns base64 without saving
- process_batch: process multiple files with batch-progress events
Add a sidebar with 5 feature navigation items (Convert, Optimize,
Resize, Crop, Extend) and a Settings button. Update App.tsx with
the sidebar + main content area layout using sidebar theme variables.
- Create typed API wrapper (lib/tauri.ts) matching Rust IPC commands
- Add DropZone component with native OS drag-and-drop via Tauri events
  and file picker dialog using @tauri-apps/plugin-dialog
- Integrate DropZone in App.tsx with image loading and thumbnail display
- Register tauri-plugin-dialog in Rust backend and capabilities
- Add useImageProcess hook for single-file processing state management
- Add Process button with loading state in App.tsx
- Add ProcessResultCard component with size comparison and compression %
- Add cross-platform basename/capitalize utilities
- Reset options and result when switching features
Add ImagePreview component with side-by-side before/after comparison
view showing original and result thumbnails, file metadata, and
compression ratio. Extend useImageProcess hook to automatically load
result file info for thumbnail display. Update App.tsx to show the
comparison view after processing completes with Process Again and
Clear action buttons.
- Extract formatBytes utility to shared lib/format.ts, removing duplicates
  from App.tsx, ImagePreview.tsx, and ProcessResultCard.tsx
- Add useBatchProcess hook that listens to Tauri "batch-progress" events
  and tracks per-file processing status (pending/processing/completed/error)
- Add BatchList component with progress bar, per-file status icons,
  size change display, and completion summary
- Update App.tsx to branch into batch mode when 2+ files are selected,
  showing "Process All" button and real-time BatchList during processing
- Completed batch items are clickable to show before/after comparison
Add Settings page with default output directory, quality, and overwrite
options. Settings are persisted to localStorage and applied when
processing images. Also set minimum window size to 800x600.
…reads

All Tauri commands (load_image, process_image, preview_image, process_batch)
now use tauri::async_runtime::spawn_blocking to avoid blocking the main
thread and causing the WebView to freeze during image processing.

Also adds scan_directory command for recursive image file discovery.
- DropZone: support drag-and-drop folders with recursive scanning
- DropZone: add "Browse Folder" button alongside "Browse Files"
- App: add overwrite checkbox to processing options
- App: fix options state merge to prevent mutual overwrites
Builds Tauri app for macOS (ARM + Intel), Linux, and Windows using
tauri-apps/tauri-action. Includes platform-specific native dependency
setup for libjxl, dav1d, and mozjpeg. Creates a draft GitHub release
on gui-v* tag push.
@gemini-code-assist
Copy link

Summary of Changes

Hello @clroot, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 slimg 프로젝트에 Tauri v2 기반의 데스크톱 GUI 애플리케이션을 도입하여 비개발자 및 디자이너 사용자들이 이미지 처리 기능을 더욱 쉽게 활용할 수 있도록 합니다. 기존 Rust 코어 로직을 재사용하면서 React 프론트엔드로 직관적인 인터페이스를 구축하고, 다양한 이미지 조작 기능을 제공합니다. 또한, 효율적인 이미지 처리를 위한 비동기 로직과 여러 플랫폼을 지원하는 빌드 시스템을 포함하여 애플리케이션의 접근성과 유용성을 크게 향상시킵니다.

Highlights

  • Tauri v2 데스크톱 GUI 애플리케이션 추가: slimg CLI의 기능을 활용하는 Tauri v2 기반의 데스크톱 GUI 애플리케이션이 추가되었습니다. React, Tailwind CSS v4, shadcn/ui를 사용하여 현대적인 사용자 인터페이스를 제공합니다.
  • 주요 이미지 처리 기능 지원: 이미지 변환(convert), 최적화(optimize), 리사이즈(resize), 크롭(crop), 확장(extend) 등 slimg-core의 모든 핵심 기능을 GUI에서 지원합니다.
  • 사용자 친화적인 기능 구현: 단일/배치 파일 처리, 폴더 드래그앤드롭, 실시간 미리보기, 그리고 처리 옵션 UI에 덮어쓰기(overwrite) 체크박스 등 사용자 편의성을 위한 기능들이 포함되었습니다.
  • 비동기 처리 및 크로스 플랫폼 CI: 모든 Tauri 명령어는 UI 프리징을 방지하기 위해 spawn_blocking으로 비동기 처리되며, macOS (ARM/Intel), Linux, Windows를 위한 크로스 플랫폼 릴리스 CI가 설정되었습니다.
Changelog
  • Cargo.toml
    • 워크스페이스 멤버에 'gui/src-tauri' 경로를 추가하여 GUI 애플리케이션의 Rust 백엔드를 포함시켰습니다.
  • docs/plans/2026-02-20-tauri-gui-design.md
    • slimg CLI의 전체 기능을 데스크톱 GUI 애플리케이션으로 제공하기 위한 Tauri GUI 디자인 문서를 추가했습니다.
    • 타겟 사용자, 핵심 기능, UI 레이아웃, 기능별 옵션 패널, 미리보기 및 비교, 파일 입출력, 기술 아키텍처 및 스택에 대한 상세 계획을 명시했습니다.
  • docs/plans/2026-02-20-tauri-gui-implementation.md
    • Tauri GUI 구현을 위한 상세한 단계별 계획 문서를 추가했습니다.
    • 프로젝트 스캐폴딩, Tailwind CSS 및 shadcn/ui 설정, Tauri Rust 백엔드 IPC 명령어 구현, 사이드바, 드롭존, 옵션 패널, 이미지 처리 및 미리보기, 배치 처리, 설정 패널 구현 등 10가지 주요 태스크를 정의했습니다.
  • gui/.gitignore
    • GUI 프로젝트에 대한 표준 .gitignore 파일을 추가하여 로그, node_modules, dist 폴더 및 에디터 관련 파일을 무시하도록 설정했습니다.
  • gui/.vscode/extensions.json
    • VS Code 사용자를 위한 권장 확장 프로그램(Tauri, Rust Analyzer) 목록을 추가했습니다.
  • gui/README.md
    • Tauri + React + Typescript 프로젝트의 기본 README 파일을 추가했습니다.
  • gui/components.json
    • shadcn/ui 컴포넌트 라이브러리 설정을 추가하여 스타일, 테마, 별칭 등을 정의했습니다.
  • gui/index.html
    • Tauri 애플리케이션의 메인 HTML 진입점 파일을 추가했습니다.
  • gui/package.json
    • GUI 프론트엔드에 필요한 bun 패키지 의존성 및 스크립트를 추가했습니다.
    • Tauri API, shadcn/ui, React, Tailwind CSS 관련 라이브러리들이 포함되었습니다.
  • gui/src-tauri/.gitignore
    • Tauri Rust 백엔드 프로젝트에 대한 .gitignore 파일을 추가하여 컴파일된 파일 및 스키마 파일을 무시하도록 설정했습니다.
  • gui/src-tauri/Cargo.toml
    • Tauri Rust 백엔드의 Cargo.toml 파일을 추가하여 slimg-coretauri-plugin-dialog와 같은 Rust 의존성을 정의했습니다.
  • gui/src-tauri/build.rs
    • Tauri 빌드 스크립트를 추가했습니다.
  • gui/src-tauri/capabilities/default.json
    • Tauri 애플리케이션의 기본 권한 및 윈도우 설정을 정의하는 기능을 추가했습니다.
  • gui/src-tauri/src/commands.rs
    • 이미지 처리 및 디렉토리 스캔을 위한 Rust IPC 명령어(load_image, process_image, preview_image, process_batch, scan_directory)를 구현했습니다.
    • 이미지 정보, 처리 옵션, 결과 및 배치 진행 상황을 위한 데이터 구조를 정의했습니다.
    • 파일 덮어쓰기 방지를 위한 고유 경로 생성 로직을 추가했습니다.
  • gui/src-tauri/src/lib.rs
    • Tauri 애플리케이션의 라이브러리 진입점을 설정하고, tauri-plugin-dialog를 초기화하며, 모든 IPC 명령어를 등록했습니다.
  • gui/src-tauri/src/main.rs
    • Tauri 애플리케이션의 메인 실행 파일을 추가했습니다.
  • gui/src-tauri/tauri.conf.json
    • Tauri 애플리케이션의 구성 파일을 추가하여 제품 이름, 버전, 빌드 설정, 윈도우 크기 및 보안 설정을 정의했습니다.
  • gui/src/App.tsx
    • 메인 React 애플리케이션 컴포넌트를 추가하여 사이드바, 드롭존, 옵션 패널, 이미지 미리보기, 배치 목록 및 설정 컴포넌트를 통합했습니다.
    • 단일/배치 이미지 처리 흐름, 파일 선택 및 로딩, 오류 처리, 그리고 결과 표시 로직을 구현했습니다.
  • gui/src/components/BatchList.tsx
    • 배치 처리 진행 상황과 개별 파일의 상태 및 결과를 표시하는 컴포넌트를 추가했습니다.
    • 파일 크기 변화 및 압축률을 시각적으로 보여주는 기능을 포함했습니다.
  • gui/src/components/DropZone.tsx
    • 파일 및 폴더 드래그앤드롭, 파일 선택 대화 상자를 통해 이미지를 로드하는 컴포넌트를 추가했습니다.
    • 지원되는 이미지 확장자를 정의하고, 드래그 상태에 따른 시각적 피드백을 제공합니다.
  • gui/src/components/ImagePreview.tsx
    • 원본 이미지와 처리된 이미지의 전/후 비교 뷰를 표시하는 컴포넌트를 추가했습니다.
    • 각 이미지의 파일명, 크기, 해상도 및 압축률을 함께 보여줍니다.
  • gui/src/components/OptionsPanel.tsx
    • 선택된 기능(변환, 최적화, 리사이즈, 크롭, 확장)에 따라 적절한 옵션 컴포넌트를 동적으로 렌더링하는 패널을 추가했습니다.
  • gui/src/components/ProcessResultCard.tsx
    • 이미지 처리 결과를 요약하여 출력 경로, 크기 변화, 해상도 및 포맷을 표시하는 컴포넌트를 추가했습니다.
  • gui/src/components/Settings.tsx
    • 기본 출력 디렉토리, 기본 품질, 원본 덮어쓰기 여부 등 애플리케이션 설정을 관리하고 변경할 수 있는 컴포넌트를 추가했습니다.
  • gui/src/components/Sidebar.tsx
    • 애플리케이션의 주요 기능(변환, 최적화, 리사이즈, 크롭, 확장) 및 설정으로 이동할 수 있는 사이드바 내비게이션 컴포넌트를 추가했습니다.
  • gui/src/components/options/ConvertOptions.tsx
    • 이미지 변환 기능을 위한 포맷 선택 및 품질 슬라이더 옵션 컴포넌트를 추가했습니다.
  • gui/src/components/options/CropOptions.tsx
    • 이미지 크롭 기능을 위한 영역 또는 비율 기반 크롭 모드, 좌표, 너비, 높이 입력 및 포맷/품질 옵션 컴포넌트를 추가했습니다.
  • gui/src/components/options/ExtendOptions.tsx
    • 이미지 확장 기능을 위한 목표 비율(너비, 높이), 채움 색상 선택 및 포맷/품질 옵션 컴포넌트를 추가했습니다.
  • gui/src/components/options/FormatSelect.tsx
    • 다양한 이미지 포맷을 선택할 수 있는 재사용 가능한 드롭다운 컴포넌트를 추가했습니다.
  • gui/src/components/options/OptimizeOptions.tsx
    • 이미지 최적화 기능을 위한 품질 슬라이더 옵션 컴포넌트를 추가했습니다.
  • gui/src/components/options/QualitySlider.tsx
    • 이미지 품질을 조절하는 재사용 가능한 슬라이더 컴포넌트를 추가했습니다.
  • gui/src/components/options/ResizeOptions.tsx
    • 이미지 리사이즈 기능을 위한 너비/높이 입력, 비율 유지 체크박스, 다양한 리사이즈 모드 및 포맷/품질 옵션 컴포넌트를 추가했습니다.
  • gui/src/components/ui/button.tsx
    • shadcn/ui의 Button 컴포넌트를 추가했습니다.
  • gui/src/components/ui/checkbox.tsx
    • shadcn/ui의 Checkbox 컴포넌트를 추가했습니다.
  • gui/src/components/ui/input.tsx
    • shadcn/ui의 Input 컴포넌트를 추가했습니다.
  • gui/src/components/ui/label.tsx
    • shadcn/ui의 Label 컴포넌트를 추가했습니다.
  • gui/src/components/ui/progress.tsx
    • shadcn/ui의 Progress 컴포넌트를 추가했습니다.
  • gui/src/components/ui/radio-group.tsx
    • shadcn/ui의 RadioGroup 및 RadioGroupItem 컴포넌트를 추가했습니다.
  • gui/src/components/ui/select.tsx
    • shadcn/ui의 Select 관련 컴포넌트(Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue)를 추가했습니다.
  • gui/src/components/ui/separator.tsx
    • shadcn/ui의 Separator 컴포넌트를 추가했습니다.
  • gui/src/components/ui/slider.tsx
    • shadcn/ui의 Slider 컴포넌트를 추가했습니다.
  • gui/src/components/ui/tooltip.tsx
    • shadcn/ui의 Tooltip 관련 컴포넌트(Tooltip, TooltipTrigger, TooltipContent, TooltipProvider)를 추가했습니다.
  • gui/src/hooks/useBatchProcess.ts
    • 배치 이미지 처리를 관리하고 진행 상황을 추적하는 커스텀 React 훅을 추가했습니다.
  • gui/src/hooks/useImageProcess.ts
    • 단일 이미지 처리 및 결과 로딩을 관리하는 커스텀 React 훅을 추가했습니다.
  • gui/src/hooks/useSettings.ts
    • 애플리케이션 설정을 로컬 스토리지에 저장하고 로드하며 업데이트하는 커스텀 React 훅을 추가했습니다.
  • gui/src/index.css
    • Tailwind CSS 및 shadcn/ui를 위한 전역 스타일과 커스텀 테마 변수를 정의했습니다.
  • gui/src/lib/format.ts
    • 바이트 단위를 사람이 읽기 쉬운 형식으로 변환하는 유틸리티 함수를 추가했습니다.
  • gui/src/lib/path.ts
    • 파일 경로에서 파일 이름만 추출하고 문자열의 첫 글자를 대문자로 만드는 유틸리티 함수를 추가했습니다.
  • gui/src/lib/tauri.ts
    • Tauri IPC 명령어 호출을 위한 타입 안전한 래퍼와 관련 데이터 인터페이스를 정의했습니다.
  • gui/src/lib/utils.ts
    • Tailwind CSS 클래스를 조건부로 병합하는 유틸리티 함수를 추가했습니다.
  • gui/src/main.tsx
    • React 애플리케이션의 메인 렌더링 진입점 파일을 추가했습니다.
  • gui/src/vite-env.d.ts
    • Vite 환경 변수 타입 정의 파일을 추가했습니다.
  • gui/tsconfig.json
    • GUI 프론트엔드 프로젝트를 위한 TypeScript 설정 파일을 추가했습니다.
  • gui/tsconfig.node.json
    • Vite 설정 파일과 같은 Node.js 환경에서 사용되는 TypeScript 설정 파일을 추가했습니다.
  • gui/vite.config.ts
    • Vite 빌드 구성을 추가하여 React 및 Tailwind CSS 플러그인을 설정하고, Tauri 개발 환경에 맞게 서버 및 HMR 설정을 조정했습니다.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/release-gui.yml
Activity
  • clroot님이 새로운 Tauri v2 데스크톱 GUI 애플리케이션을 추가하는 작업을 시작했습니다.
  • GUI 애플리케이션의 디자인 및 구현 계획 문서를 작성하여 프로젝트의 방향성을 명확히 했습니다.
  • Tauri Rust 백엔드와 React 프론트엔드 간의 IPC 통신을 위한 명령어를 구현했습니다.
  • 사용자 인터페이스를 구성하는 다양한 React 컴포넌트(사이드바, 드롭존, 옵션 패널, 미리보기, 배치 목록, 설정)를 개발했습니다.
  • 이미지 처리 로직을 위한 커스텀 React 훅을 구현하여 상태 관리 및 비동기 작업을 효율적으로 처리했습니다.
  • 크로스 플랫폼 릴리스를 위한 CI 워크플로우 설정을 포함하여 배포 준비를 진행했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

- Remove unused maintainAspect state from ResizeOptions
- Extract calcSavingsPercent util to eliminate duplication
- Add eslint-disable for intentional mount-only useEffect deps
- Wrap handleFilesSelected in useCallback to stabilize DropZone listener
- Add pull_request trigger to release-gui.yml for CI build testing
- Only create GitHub release on tag push, not on PR builds
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request successfully adds a GUI application using Tauri v2 and React, demonstrating good architecture and component separation. However, critical security vulnerabilities have been identified, including a disabled Content Security Policy (CSP) and unrestricted file system operations (std::fs) using user-supplied paths, which elevate the risk of XSS. The recursive directory scanning also presents a Denial of Service (DoS) risk due to symlink loops. Furthermore, the implementation could benefit from improvements in stability for large file processing (concurrency control), safety in directory traversal (recursive calls), and efficiency for large file loading and processing (memory and unnecessary re-decoding). A minor issue is that custom commands are missing from capabilities/default.json.

gui/src/App.tsx Outdated
Comment on lines 87 to 92
const loaded = await Promise.all(
paths.map(async (path) => ({
path,
info: await api.loadImage(path),
}))
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Promise.allmap을 조합하여 모든 이미지를 동시에 로드하고 있습니다. 파일 개수가 많을 경우 브라우저와 백엔드 리소스를 과도하게 점유하여 앱이 불안정해질 수 있습니다. 순차적으로 처리하거나 동시 실행 개수를 제한하는 방식을 권장합니다.

Suggested change
const loaded = await Promise.all(
paths.map(async (path) => ({
path,
info: await api.loadImage(path),
}))
);
const loaded = [];
for (const path of paths) {
loaded.push({
path,
info: await api.loadImage(path),
});
}

Comment on lines 87 to 211
pub fn scan_directory(path: String) -> Result<Vec<String>, String> {
let dir_path = Path::new(&path);
if !dir_path.is_dir() {
return Err(format!("Not a directory: {}", path));
}

let mut files = Vec::new();
collect_images(dir_path, &mut files)?;
files.sort();
Ok(files)
}

fn collect_images(dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
let entries = std::fs::read_dir(dir).map_err(|e| e.to_string())?;
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.is_dir() {
collect_images(&path, out)?;
} else if Format::from_extension(&path).is_some() {
out.push(path.to_string_lossy().to_string());
}
}
Ok(())
}

#[tauri::command]
pub async fn load_image(path: String) -> Result<ImageInfo, String> {
tauri::async_runtime::spawn_blocking(move || {
let file_path = Path::new(&path);

if Format::from_extension(file_path).is_none() {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
return Err(format!("Unsupported file type: {}", ext));
}

let raw_bytes = std::fs::read(file_path).map_err(|e| e.to_string())?;
let size_bytes = raw_bytes.len() as u64;

let (image, format) = slimg_core::decode(&raw_bytes).map_err(|e| e.to_string())?;

let thumbnail = slimg_core::resize::resize(
&image,
&ResizeMode::Fit(THUMBNAIL_MAX_DIMENSION, THUMBNAIL_MAX_DIMENSION),
)
.map_err(|e| e.to_string())?;

let png_bytes = encode_as_png(&thumbnail)?;
let thumbnail_base64 = BASE64.encode(&png_bytes);

Ok(ImageInfo {
width: image.width,
height: image.height,
format: format.extension().to_string(),
size_bytes,
thumbnail_base64,
})
})
.await
.map_err(|e| format!("Task failed: {}", e))?
}

#[tauri::command]
pub async fn process_image(input: String, options: ProcessOptions) -> Result<ProcessResult, String> {
tauri::async_runtime::spawn_blocking(move || process_single_file(&input, &options))
.await
.map_err(|e| format!("Task failed: {}", e))?
}

#[tauri::command]
pub async fn preview_image(input: String, options: ProcessOptions) -> Result<PreviewResult, String> {
tauri::async_runtime::spawn_blocking(move || {
let input_path = Path::new(&input);

let raw_bytes = std::fs::read(input_path).map_err(|e| e.to_string())?;
let (image, source_format) = slimg_core::decode(&raw_bytes).map_err(|e| e.to_string())?;

let pipeline_result = if matches!(options.operation, Operation::Optimize) {
slimg_core::optimize(&raw_bytes, options.quality).map_err(|e| e.to_string())?
} else {
let pipeline_options = build_pipeline_options(&options, source_format)?;
slimg_core::convert(&image, &pipeline_options).map_err(|e| e.to_string())?
};

let (decoded_result, _) =
slimg_core::decode(&pipeline_result.data).map_err(|e| e.to_string())?;

let data_base64 = BASE64.encode(&pipeline_result.data);

Ok(PreviewResult {
data_base64,
size_bytes: pipeline_result.data.len() as u64,
width: decoded_result.width,
height: decoded_result.height,
format: pipeline_result.format.extension().to_string(),
})
})
.await
.map_err(|e| format!("Task failed: {}", e))?
}

#[tauri::command]
pub async fn process_batch(
inputs: Vec<String>,
options: ProcessOptions,
window: tauri::Window,
) -> Result<(), String> {
let total = inputs.len();

for (index, file_path) in inputs.iter().enumerate() {
let progress_processing = BatchProgress {
index,
total,
file_path: file_path.clone(),
status: "processing".to_string(),
result: None,
error: None,
};
let _ = window.emit("batch-progress", &progress_processing);

let fp = file_path.clone();
let opts = options.clone();
let result = tauri::async_runtime::spawn_blocking(move || process_single_file(&fp, &opts))
.await
.map_err(|e| format!("Task failed: {}", e))?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The scan_directory command, along with others, accepts arbitrary file paths from the frontend and uses them directly with std::fs operations without validation or scoping. This poses a significant security risk, as a compromised frontend (e.g., via XSS) could read or overwrite arbitrary files. It is crucial to implement strict path validation or utilize Tauri's scoped file system permissions to restrict operations to intended directories. Furthermore, the current recursive directory traversal method is susceptible to stack overflow in very deep folder structures; an iterative approach using a Vec as a stack would be safer and more robust.

fn collect_images(dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
    let mut stack = vec![dir.to_path_buf()];
    while let Some(current_dir) = stack.pop() {
        let entries = std::fs::read_dir(current_dir).map_err(|e| e.to_string())?;
        for entry in entries {
            let entry = entry.map_err(|e| e.to_string())?;
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
            } else if Format::from_extension(&path).is_some() {
                out.push(path.to_string_lossy().to_string());
            }
        }
    }
    Ok(())
}

],
"security": {
"csp": null
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The Content Security Policy (CSP) is explicitly disabled (csp: null). This removes a critical layer of defense against Cross-Site Scripting (XSS) attacks. In a Tauri application, an XSS vulnerability can be escalated to execute sensitive Rust commands, potentially leading to full system compromise. It is highly recommended to implement a restrictive CSP that only allows trusted sources.

Comment on lines 99 to 111
fn collect_images(dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
let entries = std::fs::read_dir(dir).map_err(|e| e.to_string())?;
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.is_dir() {
collect_images(&path, out)?;
} else if Format::from_extension(&path).is_some() {
out.push(path.to_string_lossy().to_string());
}
}
Ok(())
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The collect_images function recursively scans directories using path.is_dir(), which follows symbolic links. A directory structure containing a symlink loop will cause infinite recursion, leading to a stack overflow and crashing the backend process.

Recommendation: Use path.symlink_metadata()?.is_dir() to avoid following symlinks during recursion, and consider implementing a maximum recursion depth.

return Err(format!("Unsupported file type: {}", ext));
}

let raw_bytes = std::fs::read(file_path).map_err(|e| e.to_string())?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

std::fs::read는 파일 전체를 메모리에 한 번에 로드합니다. 매우 큰 이미지 파일을 처리할 때 메모리 부족(OOM) 문제가 발생할 수 있으므로, 스트리밍 방식이나 메모리 맵핑을 고려해 보시기 바랍니다.

Comment on lines 270 to 271
let (decoded_result, _) =
slimg_core::decode(&pipeline_result.data).map_err(|e| e.to_string())?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이미지 처리 결과의 해상도를 얻기 위해 전체 데이터를 다시 디코딩하고 있습니다. slimg_core::convert가 이미 내부적으로 최종 해상도를 알고 있으므로, PipelineResult에 해상도 정보를 포함하도록 코어 라이브러리를 수정하거나, 리사이즈 옵션 등을 통해 계산된 값을 사용하는 것이 성능상 유리합니다.

- Add width/height to PipelineResult to avoid redundant re-decoding
- Limit concurrent image loading to 5 at a time
- Enable CSP for defense-in-depth security
- Use entry.file_type() to prevent symlink loop in directory scan
Option components were initializing quality to hardcoded 80, ignoring
the user's configured default quality in settings. Now defaultQuality
is passed as a prop through OptionsPanel to all option components.
PipelineResult now includes width/height fields, which is a public API
addition. Bump all dependent crates to reflect the updated dependency.
@clroot clroot merged commit fb55faf into main Feb 20, 2026
1 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant