이미지 편집기
storytold/photocraft · ⭐71,526 · 포크 12,630 · Rust · v0.14.0.
수치는 2026-10-23 GitHub 기준, 코드 설명은 커밋 ff47388 기준입니다.
PhotoCraft는 사진을 열어 레이어·선택 영역·마스크·브러시·필터로 편집하고, 결과를 저장하는 프로그램입니다.
Photoshop과 비슷한 작업 흐름을 목표로 하며, PSD/PSB와 자체 형식 .pcraft를 다룹니다.
중요한 학습 대상은 기능 목록보다 같은 편집을 여러 인터페이스에서 재사용하는 구조입니다.
PhotoCraft에서는 화면의 버튼도 터미널의 명령도 같은 문서 모델과 편집 엔진으로 들어갑니다.
데스크톱은 egui/eframe로 화면을 그리고, 브라우저판은 Rust를 WebAssembly로 컴파일합니다.
CLI는 창을 띄우지 않고 작업하며, MCP 서버는 외부 에이전트가 엔진을 호출할 수 있게 합니다.
crates/engine과 crates/ui-egui의 분리는 이 차이를 코드로 표현합니다.매장 계산대에서 주문하든 배달 주문을 받든, 조리법은 주방에 있어야 합니다. 화면 버튼 안에 조리법을 넣으면 배달 창구를 만들 때 같은 기능을 다시 작성하게 됩니다.
PhotoCraft는 filter.blur.gaussianBlur 같은 명령 ID로 주문을 받습니다.
각 창구는 입력을 전달하고, 엔진은 조건 검사·편집·기록을 처리합니다.
Document와 레이어로 바꿉니다.Session이 명령과 JSON 매개변수를 받아 문서 상태를 바꿉니다.History가 실행 취소를 담당합니다.| 파일 | 처음 찾을 식별자 | 읽으며 답할 질문 |
|---|---|---|
crates/doc/src/lib.rs | Document, LayerContent | 편집기가 무엇을 상태로 보관하는가? |
crates/engine/src/commands.rs | CommandSpec, find | 기능 이름과 실행 함수는 어떻게 연결되는가? |
crates/engine/src/lib.rs | Session::execute, Session::edit | 편집 한 번을 어떻게 일관된 상태 변경으로 묶는가? |
전체 기능을 외울 필요는 없습니다. 흐림 필터 하나가 입력·검증·알고리즘·이력·렌더링을 통과하는 길을 설명할 수 있다면, 다른 기능을 읽을 기준점이 생깁니다.
2026-10-23 기준 TrendShift Monthly #2이며, GitHub 스타는 71,526개입니다. 저장소 생성일은 2026-09-30으로 분석일까지 23일이 지났습니다. 이 짧은 공개 기간에 비해 편집기·파일 형식·자동화·배포를 함께 다룬다는 점이 눈길을 끕니다. 다만 저장소 생성일은 모든 코드의 개발 시작일을 증명하지 않습니다.
어떤 홍보나 커뮤니티가 유입을 만들었는지는 코드만으로 확정할 수 없습니다. 여기서 짚을 수 있는 기술적 매력은 Rust 중심의 제품 구조, 편집 엔진의 재사용, 파일 호환성 검증을 공개했다는 점입니다.
비교 기준은 학습 가능한 내부 구조와 편집 자동화입니다.
Photoshop은 저장소가 명시한 기능·동작의 비교 목표이고, image는 PhotoCraft도 사용하는 Rust 영상 입출력 라이브러리입니다.
세 도구는 범위가 다르므로, 코드 공개나 명령 인터페이스의 장점을 편집 품질의 우열로 확대하면 안 됩니다.
| 비교 대상 | PhotoCraft에서 얻는 장점 | 함께 따질 조건 |
|---|---|---|
| Photoshop | 레이어·PSD 작업의 내부 구현을 Rust 소스로 읽고, CLI·MCP와 엔진의 연결까지 추적할 수 있음 | 비슷한 메뉴가 있어도 동작·색상·PSD 교환이 같은 수준인지는 별도 검증 필요 |
Rust image | 코덱 활용을 넘어 레이어·실행 취소·문서 저장·UI까지 제품 전체를 학습할 수 있음 | 간단한 이미지 변환만 필요하면 작은 라이브러리를 직접 쓰는 편이 범위가 작음 |
| 화면 조작 중심 작업 | 동일 편집을 명령 ID와 JSON으로 표현해 반복 작업과 자동화에 재사용 | GUI 전용 기능과 파일 접근 권한은 셸·자동화 계층에서 추가 처리됨 |
crates/automation/src/server.rs의 Backend는
Headless와 Bridge를 구분합니다.
전자는 프로세스 안의 엔진을 사용하고, 후자는 실행 중인 데스크톱 앱과 통신합니다.
따라서 자동화의 두 경로를 같은 도구 인터페이스 아래에서 비교할 수 있습니다.
자동화하기 쉬운 제품은 핵심 동작을 화면 밖에서도 호출할 수 있도록 설계되어 있습니다.
명령 목록에는 enabled, disabledReason, needsDocument가 포함됩니다.
단순히 실행 버튼을 제공하는 것에서 한 걸음 더 나아가, 호출자가 다음 행동을 판단할 정보를 줍니다.
| 항목 | 문서의 표현 | 커밋 ff47388에서 확인되는 사실 |
|---|---|---|
| 크레이트 수 | README: 24개 | crates/*/Cargo.toml은 26개, apps/는 4개 |
| 테스트 | README: 1,700개 이상 | Rust 전체의 #[test] 표시는 10,656개. 실행·통과 개수와는 다름 |
| 코드 규모 | 아키텍처 문서: 약 53.9만 줄, 과거 집계 | Rust 물리 줄 수 661,699줄 / 1,502파일. 테스트·예제·생성 데이터 포함 |
| 라이선스 | GitHub 대표 표기: Apache-2.0 | Cargo.toml과 README: MIT OR Apache-2.0 |
문서의 숫자는 서로 다른 시점과 집계 범위를 반영합니다. 특히 테스트 표시 수는 비동기 테스트 속성이나 매크로 생성 사례를 모두 세는 방식이 아니며, 무시되거나 환경에 따라 건너뛰는 테스트도 구별하지 않습니다.
README와 ROADMAP.md는 초기 알파 단계라고 명시합니다.
docs/gaps.md에는 PSD 교환·전문 작업·생성형 기능 등의 빈틈이 남아 있습니다.
스타 수와 메뉴 연결률은 전문 편집기의 안정성이나 호환성을 증명하는 수치가 아닙니다.
이 프로젝트에서 “백엔드”는 주로 기기 안에서 실행되는 편집 코어를 뜻합니다. 별도 원격 API 서버나 데이터베이스가 기본 편집 경로의 중심에 있지 않습니다. 프론트엔드는 Rust 네이티브 UI이며, 웹판도 같은 UI를 WebAssembly로 실행합니다.
아래의 “선언”은 Cargo.toml의 버전 요구 범위이고,
“잠금”은 루트 Cargo.lock에 기록된 구체 버전입니다.
예를 들어 선언 "1"은 정확히 1.0.0만 쓰라는 뜻이 아닙니다.
| 역할 | 선언 → 잠금 버전 | 실제 사용 위치 |
|---|---|---|
| 문서·엔진 | 내부 패키지 0.14.0 | crates/doc, engine, ops |
| 직렬화 | serde "1" → 1.0.229serde_json "1" → 1.0.151 | 루트 선언; 명령 JSON·설정·문서 메타데이터 |
| CPU 병렬 처리 | rayon "1" → 1.12.0 | raster, compose, algo 등의 네이티브 경로 |
| 일반 이미지 코덱 | image "0.25" → 0.25.10 | crates/codecs/Cargo.toml; 필요한 형식을 기능 플래그로 선택 |
| GPU 합성 | wgpu "30" → 30.0.1 | crates/gpu/Cargo.toml; std, wgsl 활성화 |
| 문자 배치 | parley "0.11" → 0.11.1skrifa "0.44" → 0.44.0 | crates/text/Cargo.toml; 글꼴·글리프·줄 배치 |
| 자동화 | rmcp "3.5" → 3.5.0tokio "1" → 1.53.1 | crates/automation/Cargo.toml; MCP·비동기 통신 |
| 플러그인 실행 | wasmi "2.0.0" → 2.0.0 | crates/plugins/Cargo.toml; 제한을 둔 WebAssembly 해석기 |
| 자체 파일 형식 | blake3 "1.8" → 1.8.7ruzstd "0.9.1" → 0.9.1 | crates/format/Cargo.toml; 콘텐츠 해시와 zstd 처리 |
| 영역 | 실제 구성 | 읽을 지점 |
|---|---|---|
| 데스크톱 UI | egui/eframe "0.36" → 0.36.2 | apps/photocraft/Cargo.toml: wgpu, accesskit, wayland, x11, persistence |
| 공용 UI | craft-ui Git 리비전 5c874d5 고정 | 루트 의존성: storytold/craft-libs의 불변 커밋 참조 |
| 파일 선택창 | rfd "0.17" → 0.17.2 | 플랫폼 서비스와 UI 계층에서 OS 기능 연결 |
| 브라우저 연결 | wasm-bindgen "0.2" → 0.2.129 | apps/photocraft-web/Cargo.toml: web-sys, js-sys도 사용 |
| 웹 시작점 | eframe::WebRunner | apps/photocraft-web/src/web.rs::start에서 캔버스와 PhotocraftApp 연결 |
“Rust로 만든 웹 UI”는 브라우저 기능을 쓰지 않는다는 뜻이 아니라, UI와 엔진의 주 구현 언어가 Rust라는 뜻입니다. JavaScript·TypeScript 소스 파일은 이 리비전에서 발견되지 않지만, 웹 빌드에는 바인딩과 부트스트랩이 필요합니다. Tauri나 Electron을 쓰는 구조와도 다릅니다.
heif가 그 예입니다.| 설정 파일 | 확인되는 값과 역할 |
|---|---|
Cargo.toml | edition = "2024", rust-version = "1.95", resolver = "3". 릴리스는 lto = "thin", codegen-units = 1 |
.cargo/config.toml | xtask = "run -p xtask --". 레이어 검사·통계·코퍼스 등을 Cargo 하위 명령처럼 제공 |
.github/workflows/ci.yml | Linux·조건부 Windows 테스트, cargo fmt, 엄격한 Clippy, cargo xtask layers, 점수표 일치 검사. Rust는 stable 추적 |
.github/workflows/release.yml | release 브랜치에서 공용 워크플로 storytold/craft-common-actions의 @main 호출 |
Dockerfile | 웹판 자체 호스팅용 2단계 빌드. Rust 기본값 1.99, Trunk 0.21.14, nginx 1.28-alpine; 런타임은 nginx 사용자와 8080 포트 |
packaging/web/package.sh | 개별 파일 기본 상한 24 MiB, 압축 WebAssembly 분할 기본값 20 MiB. 호스트 파일 제한에 맞춰 배포 |
.github/workflows/docs.yml | mdbook 0.4.52로 book/ 문서 빌드 |
Rust 최소 요구 버전 1.95, Docker의 기본 빌더 1.99, CI의 stable은 서로 다른 설정입니다.
공용 릴리스 워크플로는 이 저장소 바깥의 @main을 따라가므로,
로컬 YAML만으로 서명·공증·모든 타깃의 전체 절차까지 확정할 수는 없습니다.
아래에서 위쪽은 사용자의 입구, 가운데는 편집 상태와 계산, 아래쪽은 파일 경계입니다.
브라우저판의 ui-egui와 엔진은 WebAssembly 안에서 실행됩니다.
MCP의 브리지 경로만 실행 중인 앱의 로컬 제어 채널을 거칩니다.
이 그림은 주요 호출과 데이터 흐름을 요약합니다. 상자 사이의 모든 선이 직접적인 Cargo 의존성을 뜻하지는 않습니다.
실제 계층 규칙은 xtask/src/layers.rs의 TABLE과 검사 코드에 있습니다.
CommandSpec은 기능 ID·메뉴 위치·단축키·매개변수 설명과 실행 함수를 함께 보관합니다.
crates/engine/src/commands.rs::registry는 OnceLock으로 레지스트리를 한 번 만들고,
HashMap으로 ID를 찾아 명령 배열의 항목에 연결합니다.
// crates/engine/src/commands.rs — 실행 계약과 주요 필드 발췌
type Run = fn(&mut Session, &Value) -> Result<Value>;
type Enabled = fn(&Session) -> std::result::Result<(), String>;
pub struct CommandSpec {
pub id: &'static str,
pub label: &'static str,
// menu, shortcut, params 필드는 생략
pub enabled: Enabled,
pub run: Run,
pub journal: bool,
}
명령 메타데이터를 읽으면 메뉴와 자동화 도구를 구성할 수 있고,
enabled로 문서·레이어·선택 영역 같은 실행 조건을 검사할 수 있습니다.
다만 params는 설명 문자열이므로, 이것만으로 JSON이 완전히 타입 안전해지는 것은 아닙니다.
Session::execute는 crates/engine/src/jobs.rs의 Session::dispatch를 호출합니다.
여기서 명령 존재 여부, 매개변수가 JSON 객체 또는 null인지, 실행 권한과 공통·필터 매개변수 규칙을 검사합니다.
Session::admit는 활성 조건과 작업 충돌 등도 판단합니다.
crates/engine/src/filters/validation.rs::fields에서 Gaussian Blur의 radius 범위는
0.1..=1000.0입니다. crates/engine/src/filters.rs::params_for에는 기본값 1.0과 범위 제한도 있습니다.
신규 호출 검증과 예전 스마트 필터 기록의 관대한 해석이 공존하므로, 변환 함수의 제한만 보고 잘못된 입력이 모두 허용된다고 해석하면 안 됩니다.
journal과 이전 문서를 보관하는 History는 목적이 다릅니다.
PhotoCraft의 실행 취소를 “역명령을 하나씩 실행하는 방식”으로 이해하면 실제 구현과 어긋납니다.crates/doc/src/lib.rs는 레이어 종류를 열거형으로 표현합니다.
레이어가 픽셀 배열이라는 가정으로 시작하지 않기 때문에, 문자·도형·조정·스마트 오브젝트도 같은 레이어 트리에 놓을 수 있습니다.
// crates/doc/src/lib.rs — LayerContent
pub enum LayerContent {
Raster(Surface),
Group(Group),
Adjustment(Adjustment),
Fill(Fill),
Text(TextLayer),
Shape(ShapeLayer),
Smart(SmartObject),
}
Layer는 content 외에도 opacity, fill_opacity,
mask, effects 등을 가집니다.
조정 레이어의 설정을 바꾸는 것과 픽셀을 직접 덮어쓰는 것을 데이터 차원에서 구분하므로,
비파괴 편집과 파일 저장이 같은 모델을 공유할 수 있습니다.
색상도 비슷합니다. crates/color/src/lib.rs의 PixelFormat은
mode, sample, alpha를 보관하고,
SampleType은 실제로 U8·U16·F32 세 가지입니다.
설계 문서의 초기 계획에 등장하는 F16을 현재 문서 저장 타입에 추가된 것으로 읽으면 안 됩니다.
crates/raster/src/lib.rs의 Surface는
BTreeMap<TileCoord, Arc<Tile>>에 존재하는 타일만 저장합니다.
타일 한 변은 256픽셀이며, 없는 타일은 표면의 기본 픽셀로 읽습니다.
기본 픽셀은 일반 레이어의 투명색일 수도, 마스크의 흰색일 수도 있습니다.
// crates/raster/src/lib.rs — tile_mut의 실제 구현
pub fn tile_mut(&mut self, c: TileCoord) -> &mut Tile {
let fmt = self.format;
let dp = &self.default_pixel;
let arc = self.tiles.entry(c)
.or_insert_with(|| Arc::new(Tile::filled(&fmt, dp)));
Arc::make_mut(arc)
}
Arc::make_mut은 타일을 혼자 소유하면 그대로 수정하고,
다른 문서 상태와 공유 중이면 타일을 복사한 뒤 수정합니다.
오류를 반환하는 별도 경로 try_tile_mut에는 메모리 예약 실패를 다루는 코드도 있습니다.
도화지를 작은 타일로 조립하고, 과거 버전에는 같은 타일을 가리키는 목록을 보관한다고 생각해 보세요. 한 구석을 칠할 때 전체 도화지 대신 바뀐 타일만 새로 만들면, 이전 그림은 그대로 남습니다.
Session::edit는 이전 Arc<Document>를 보관하고 문서를 복제한 다음,
편집 클로저가 성공하면 새 문서를 활성 문서에 반영합니다.
새 이력 단계가 필요하면 History::record가 이전 문서를 기록합니다.
문서 복제는 레이어·타일 참조를 복사하는 비용이 있으므로 상수 시간이라고 단정할 수 없습니다.
실행 취소를 싸게 만드는 핵심은 “이전 픽셀을 다시 계산하기”보다 “바뀌지 않은 픽셀을 함께 보관하기”에 있습니다.
같은 coalesce 키를 쓰는 연속 편집은 하나의 이력 단계로 묶을 수 있습니다.
슬라이더를 조금씩 움직일 때 수십 개의 실행 취소 단계가 생기는 문제를 줄이는 장치입니다.
crates/compose/src/lib.rs::render는 레이어 트리를 부동소수점 RGBA 버퍼로 합성합니다.
render_tiled는 네이티브에서 독립 타일을 병렬 계산하고, 웹 타깃에서는 직렬 계산합니다.
GPU 쪽은 crates/gpu/src/lib.rs::Compositor와 crates/gpu/src/compose.wgsl이 맡습니다.
crates/gpu/tests/it/parity.rs는 두 합성 결과의 RGB에 알파를 미리 곱한 뒤, RGB와 알파의 최대 차이를 비교합니다.
실제 허용 오차 상수는 1.0 / 255.0 + 4.0 * f32::EPSILON입니다.
파일 첫 주석의 “2/255”보다 실행되는 상수와 검사 코드를 우선해 읽어야 합니다.
// crates/gpu/tests/it/parity.rs — 실제 허용 오차
const TOL: f32 = 1.0 / 255.0 + 4.0 * f32::EPSILON;
기준 구현은 최적화 결과가 맞는지 판단하는 비교점이 됩니다.
GPU가 없으면 일부 테스트는 건너뛰며, WGPU_BACKEND를 명시한 경우에는 해당 백엔드를 못 쓰면 실패하도록 분기합니다.
CPU·GPU 일치는 이 둘의 일관성을 뜻하며, Photoshop과의 일치까지 증명하지는 않습니다.
| 계층 | 주요 크레이트 | 책임 |
|---|---|---|
| L0 | geom, cms, color, raster | 좌표·색상·타일 등 기초 표현. L0 내부에도 명시된 의존 순서가 있음 |
| 독립 L0 | psd, raw, affinity, heif, tablet, ipp, codecs | 독립 사용을 지향. codecs → heif는 선택 의존성 예외 |
| L1 | doc | 문서·레이어·효과 등 편집 상태 |
| L2 | ops, algo, paint, text, vector | 이력·필터·브러시·문자·벡터 계산 |
| L3 | compose, gpu, format | 합성·GPU·자체 형식. gpu는 compose를 활용 가능 |
| L4 → L5 | io, plugins → engine | 입출력·확장을 연결하고 세션·명령 관리 |
| L6 | ui-egui, automation, testkit | UI·자동화·테스트 도우미. testkit에는 별도 사용 규칙 적용 |
xtask/src/layers.rs의 UI_MIN_LAYER = 6은
하위 코어가 egui·eframe·winit 같은 UI 의존성을 끌어들이지 않게 합니다.
TABLE에는 미래·예약 항목도 있으므로, 표에 이름이 있다는 사실만으로 해당 폴더가 존재한다고 판단하지 않습니다.
| 범위 | Rust 물리 줄 수 | 처음 읽을 때의 전략 |
|---|---|---|
crates/ui-egui | 235,614줄 / 444파일 | 대상 도구·대화상자 하나만 고르기 |
crates/engine | 141,687줄 / 304파일 | 명령 ID로 등록·실행·테스트 찾기 |
crates/algo | 58,745줄 / 148파일 | 수학 전체보다 입력·출력·경계 조건 먼저 읽기 |
crates/io | 45,092줄 / 132파일 | 문서 모델과 외부 형식의 대응 살피기 |
crates/raster | 2,130줄 / 2파일 | 작은 편이므로 타일 공유의 출발점으로 적합 |
apps/photocraft-cli | 2,080줄 / 6파일 | 창 없이 엔진까지 이어지는 경로 읽기 |
이 집계에는 각 폴더의 테스트·예제·빌드 코드가 포함됩니다. UI 계층이 논리적으로 얇다는 말은 줄 수가 작다는 뜻이 아닙니다. 도구·패널·다국어·플랫폼 상태가 많아도 핵심 편집 규칙을 엔진에 모은다는 뜻으로 읽으면 됩니다.
Arc는 여러 상태가 같은 데이터를 소유하게 하고, &mut는 변경 경계를 드러냅니다.
Result와 ?는 실패를 호출자에게 전달합니다.
Session::edit에서 f(&mut doc, &mut active)?가 새 문서를 반영하기 전에 있다는 점을 찾아보세요.
실습 아이디어: 문서를 복제한 뒤 편집 함수가 오류를 반환하는 경우를 손으로 따라가 보세요.
활성 문서 교체와 History::record에 도달하는지 표시하면,
“계산 중간의 실패가 기존 작업에 무엇을 남기는가”라는 질문을 코드로 답할 수 있습니다.
unsafe_code = "forbid"에도 펜 입력용 crates/tablet는 OS 연동을 위한 격리된 예외를 둡니다.
“Rust 중심”이라는 설명을 “모든 의존성·OS 동작까지 검증됐다”로 확대하지 마세요.crates/raster/src/lib.rs::convert_tiles는 네이티브에서 par_iter_mut를 사용합니다.
각 작업이 자기 타일의 바이트만 바꾸므로 공유 쓰기를 줄일 수 있습니다.
같은 함수의 웹 분기는 for 반복을 사용합니다.
실습 아이디어: 병렬 루프 안에서 읽는 값과 쓰는 값을 두 열로 적어 보세요. 이웃 픽셀을 읽는 필터에는 주변 영역이 필요하므로, 타일 경계가 계산 결과를 바꾸지 않게 입력 범위를 설계해야 합니다. “타일마다 나눴다”만으로 모든 영상 알고리즘이 독립적인 것은 아닙니다.
crates/algo/src/blur.rs::gaussian은 작은 반경에서 가우시안 커널과
conv_sep를 사용합니다. 가로·세로로 나누는 합성곱은,
한 픽셀마다 2차원 이웃 전체를 훑는 방식보다 필요한 계산을 줄일 수 있습니다.
box_widths는 반경이 4.0보다 크면 세 번의 박스 필터 근사를 선택합니다.
러닝 합은 창이 한 칸 움직일 때 빠진 값을 빼고 새 값을 더합니다.
큰 반경에서 창 안의 모든 값을 매번 다시 더하지 않도록 하는 최적화입니다.
연속된 100일의 평균을 매일 구할 때, 전날 100개를 다시 더할 필요는 없습니다. 합계에서 가장 오래된 하루를 빼고 오늘 값을 더하면 됩니다. 박스 흐림의 움직이는 창도 같은 계산을 이용합니다.
실습 아이디어: 흑백 한 줄 [0, 0, 1, 0, 0]에 폭 3 평균을 직접 계산해 보세요.
경계 바깥을 0으로 볼 때와 가장자리 값으로 반복할 때를 비교한 다음,
crates/algo/src/blur.rs의 경계 처리와 알파 미리 곱하기 흐름을 읽어 보세요.
crates/cms/src/lib.rs의 Profile, Transform, Intent는
ICC 프로파일과 색상 변환을 담당합니다. 같은 RGB 숫자라도 sRGB인지 Display P3인지에 따라 뜻하는 색이 달라집니다.
따라서 채널 깊이만 높인다고 색상 관리까지 해결되지는 않습니다.
실습 아이디어: PixelFormat::channels와 bytes_per_pixel로
RGBA8·RGBA16·CMYKA8의 저장 바이트 수를 계산하세요.
이어서 문서의 색 공간에서 표시 장치로 가는 변환이 합성 처리와 어디에서 분리되는지 찾아보세요.
crates/psd는 형식 수준의 구조를 읽고 쓰며, crates/io는 이를
편집 가능한 문서 모델과 대응시킵니다. 알려지지 않은 블록을 보존하더라도,
편집기가 그 블록의 시각 효과를 계산할 수 있다는 뜻은 아닙니다.
crates/format/src/lib.rs의 자체 형식은 manifest.json과
콘텐츠 해시를 이름으로 쓰는 타일·바이너리 묶음으로 구성됩니다.
PcraftWriter는 디렉토리 저장 시 기존 타일·바이너리를 재사용하고, 누락되거나 손상된 항목은 다시 씁니다.
ZIP 저장에서는 압축 결과를 재사용하더라도 아카이브 전체를 다시 만듭니다.
crates/format/src/atomic.rs::atomic_write_with_progress는 대상 옆에 임시 파일을 만들고,
쓰기·동기화 이후 교체합니다. Windows에서는 기존 파일 교체 후 디렉토리 동기화 실패를 오류로 반환하고, Unix에서는 디렉토리 동기화 오류를 무시합니다.
“파일 저장”이라는 버튼 아래에 플랫폼별 실패 조건이 있다는 사실을 배우기 좋은 지점입니다.
실습 아이디어: 동일한 PSD의 바이트 일치, 재저장 전후 렌더 일치, Photoshop의 합성 이미지와 일치를 별개의 시험 항목으로 나눠 보세요. 각각의 성공이 나머지 성공을 보장하지 않는 이유를 한 문장씩 적으세요.
crates/automation/src/server.rs::command_list와 command_run을 함께 읽으면,
도구가 명령을 발견하고 실행하는 흐름이 보입니다.
로컬 제어 채널은 crates/automation/src/security.rs에서 256비트 토큰,
요청 한 줄 1 MiB, 동시 연결 16개, 배치 256단계 같은 한계를 정합니다.
플러그인은 별도 경계입니다. crates/plugins/src/runtime.rs::Plugin::load는
호스트 임포트를 가진 모듈을 거절하며, 기본 한계는 모듈 32 MiB·선형 메모리 512 MiB·재귀 깊이 1,024입니다.
WebAssembly 명령 실행량에 따라 연료를 소모하고, 네이티브에서는 기본 60초 시간 제한도 사용합니다.
실습 아이디어: MCP는 누가 어떤 파일을 만질 수 있는지, 플러그인 런타임은 어떤 계산 자원을 쓸 수 있는지 구분해 표로 그려 보세요. 웹판에서는 단조 시계 대신 연료 제한으로 실행을 묶는 차이도 찾아보세요.
이 저장소에서 가져갈 가장 큰 기술은 기능을 추가할 때 데이터·계산·화면·검증의 경계를 함께 생각하는 습관입니다.
| 항목 | 확인되는 조건 | 학습할 때의 해석 |
|---|---|---|
| Windows | 설치 문서: Windows 10 이상. 소스 빌드는 MSVC와 C++ 빌드 도구 요구 | 공식 설치 파일 사용과 개발 도구 준비는 서로 다른 요구사항 |
| macOS | packaging/macos/Info.plist.in의 최소 버전 11.0 | 실제 GPU·드라이버·글꼴 환경의 동작 보장 범위와는 별개 |
| Linux | 개발 문서: X11·Wayland·GL·GTK 개발 패키지 | 빌드용 헤더와 설치된 앱의 런타임 의존성을 구별 |
| Rust | rust-version = "1.95", edition 2024 | 도구 버전이 낮으면 문법·표준 라이브러리 단계에서 막힐 수 있음 |
| GPU | wgpu 기반, CPU 캔버스 경로와 --safe-gpu 제공 | CPU 캔버스를 써도 창 표시에는 동작 가능한 그래픽·소프트웨어 어댑터가 필요 |
| 브라우저 | WebAssembly와 WebGPU 또는 WebGL2 경로 | 네이티브의 Rayon 병렬 경로와 동일한 성능을 가정하지 않기 |
| RAM·저장 공간 | 설치·개발 문서는 고정된 최소 RAM·VRAM·여유 디스크 수치를 제시하지 않음 | 문서 크기·깊이·레이어·효과·빌드 산출물에 따라 달라짐 |
6000 × 4000은 2,400만 픽셀입니다. 픽셀 전체가 채워진 단일 레이어를 단순 배열로 계산하면 다음과 같습니다. 아래 값은 채널 데이터만의 이론값이며 타일 가장자리 여유, 이력, 메타데이터, 임시 버퍼는 제외합니다.
| 포맷 | 계산 | 순수 픽셀 데이터 |
|---|---|---|
| RGBA8 | 24,000,000 × 4채널 × 1바이트 | 약 91.6 MiB |
| RGBA16 | 24,000,000 × 4채널 × 2바이트 | 약 183.1 MiB |
| RGBA32F | 24,000,000 × 4채널 × 4바이트 | 약 366.2 MiB |
합성용 부동소수점 버퍼, 필터 작업 버퍼, GPU 복사본까지 더해지면 실제 메모리는 커집니다. 빈 레이어는 희소 타일 덕분에 가볍지만, 전체를 바꾸는 필터는 많은 타일을 새로 만들 수 있습니다. 메모리 요구량은 사진 파일의 압축된 용량보다 풀린 픽셀과 동시에 보관하는 상태 수에 더 크게 좌우됩니다.
crates/engine/src/prefs.rs::Performance::default에는 메모리 예산 8192,
이력 50단계, 효과 캐시 768, GPU 예산 0이 설정됩니다.
바이트 변환에 1 << 20을 쓰므로 메모리 예산 8192는 8 GiB에 해당하고,
GPU의 0은 자동 설정을 뜻합니다. 이 값은 설치에 필요한 최소 RAM을 선언한 수치가 아닙니다.
처음 실험할 때는 512 × 512 이미지와 적은 레이어로 결과의 정확성을 확인하세요. 이후 해상도·깊이·레이어 수 중 하나만 늘리면 어느 자원이 비용을 만드는지 관찰하기 쉽습니다. CPU CLI 실습과 GPU 화면 실습을 나누면 환경 문제를 좁히는 데도 도움이 됩니다.
docs/scorecard.md의 성능 기준선은 2026-10-05, 커밋 f5d2bcb54a,
Apple M4 Pro·48 GB 환경의 기록입니다. 높은 시스템 부하도 함께 기록되어 있습니다.
이를 v0.14.0의 모든 장치 성능으로 일반화할 수 없습니다.
시간은 학습 계획용 대략값입니다. 먼저 소스 읽기로 흐름을 설명하고, 실행 실습은 개발 환경을 준비한 뒤 작은 합성 이미지로 진행하면 됩니다. 코드를 추가하는 과제는 별도의 학습용 사본에서 진행하세요.
목표: 명령 ID에서 알고리즘까지 파일을 찾아가는 능력을 익힙니다.
순서: apps/photocraft-cli/src/lib.rs::run_cmds에서 출발해
Headless::command_run, Session::execute, dispatch,
filters::params_for, crates/algo/src/blur.rs::gaussian 순으로 위치를 찾습니다.
완료 조건: 여섯 칸에 각각 입력·출력·실패 조건을 적고, “문서가 없는 경우”와 “반경이 잘못된 경우”가 어느 단계에서 거절되는지 설명합니다.
# 저장소 루트에서 명령 등록 지점과 알고리즘 찾기
rg -n 'gaussianBlur|params_for' crates/engine
rg -n 'fn gaussian|fn box_widths' crates/algo/src/blur.rs
# CLI 실행 파일이 준비되어 있다면 명령 메타데이터 확인
photocraft-cli commands --json --filter gaussian
목표: 복제한 표면이 픽셀을 바로 복사하지 않는다는 것을 확인합니다.
순서: 한 픽셀을 칠하고 표면을 복제한 뒤 Arc::ptr_eq로 타일 공유를 확인합니다.
복제본을 수정한 다음 두 타일이 서로 다른지 확인합니다.
완료 조건: 수정 전·후의 참조 관계를 그림으로 제출합니다. 좌표 255와 256도 시험해, 타일 경계를 넘을 때 타일 수가 어떻게 달라지는지 설명합니다.
다음은 photocraft-raster, photocraft-color, photocraft-geom을
사용할 수 있는 Rust 학습 코드에 넣는 예시입니다.
use std::sync::Arc;
use photocraft_color::PixelFormat;
use photocraft_geom::{Rect, TileCoord};
use photocraft_raster::Surface;
fn observe_cow() -> Result<(), &'static str> {
let c = TileCoord::new(0, 0);
let mut a = Surface::new(PixelFormat::RGBA8);
a.fill_rect(Rect::new(1, 1, 2, 2), &[1.0, 0.0, 0.0, 1.0]);
let mut b = a.clone();
assert!(Arc::ptr_eq(
a.tile(c).ok_or("원본 타일 없음")?,
b.tile(c).ok_or("복제본 타일 없음")?,
));
b.fill_rect(Rect::new(1, 1, 2, 2), &[0.0, 1.0, 0.0, 1.0]);
assert!(!Arc::ptr_eq(
a.tile(c).ok_or("원본 타일 없음")?,
b.tile(c).ok_or("복제본 타일 없음")?,
));
Ok(())
}
목표: JSON 매개변수와 검증 규칙의 관계를 이해합니다.
순서: 작은 문서에 픽셀 레이어와 붓 자국을 만들고 흐림을 적용합니다.
다음 예시의 반경을 -1, "4", 1001로 바꿔 오류를 관찰합니다.
완료 조건: 입력별 허용 여부를 표로 정리하고,
crates/engine/src/filters/validation.rs의 실제 규칙과 대조합니다.
잘못된 호출 뒤 파일이 만들어졌는지, 정상 호출과 오류 출력이 어떻게 다른지도 기록합니다.
photocraft-cli run \
--new '{"width":128,"height":128,"background":"transparent"}' \
--cmd layer.new.layer --params '{"name":"Ink"}' \
--cmd paint.stroke \
--params '{"points":[[32,64,1],[96,64,1]],"size":16,"color":"#ff0000"}' \
--cmd filter.blur.gaussianBlur --params '{"radius":4}' \
--out exercise.png
정상 호출에서는 붓 자국의 가장자리가 부드러워집니다. 실패 사례마다 새 출력 이름을 사용하면 이전 결과와 혼동하지 않고 확인할 수 있습니다.
목표: 빠른 구현을 검증하는 기준과 입력을 직접 설계합니다.
순서: crates/gpu/tests/it/parity.rs의 check와
hdr_adjustments_preserve_range_and_cpu_parity를 읽습니다.
반투명 레이어 둘과 마스크 하나를 가진 작은 문서 사례를 추가해 두 합성 결과를 비교합니다.
완료 조건: 최악의 픽셀 차이와 좌표를 기록하고, 허용 오차를 넘는 경우 원본 픽셀·알파·색 공간을 조사합니다. GPU 미탑재로 건너뛴 경우와 실제 비교를 통과한 경우를 구분합니다.
실습의 완료 기준은 “실행됐다”에서 끝내지 말고, 어떤 입력에서 어떤 상태가 보존되는지 설명하는 데 두세요. 고급 과제에서는 허용 오차를 키워 통과시키기 전에 기준 구현·출력 표현·비교 방식을 먼저 확인해야 합니다.
주당 4~6시간을 가정한 계획입니다. Rust가 처음이라면 1주차를 늘리고, 그래픽스 경험이 있다면 3~4주차에서 직접 손으로 계산하는 시간을 늘리세요. 각 주는 읽을 개념, 소스 근거, 설명 가능한 산출물로 묶었습니다.
| 주차 | 공부할 개념 | 소스와 결과물 |
|---|---|---|
| 1주차 | Rust 소유권·참조·열거형·Result | crates/color/src/lib.rs, crates/doc/src/lib.rs. 픽셀 형식과 레이어 종류를 쉬운 말로 설명하는 한 장 |
| 2주차 | Cargo 워크스페이스·계층·명령 패턴 | xtask/src/layers.rs, crates/engine/src/commands.rs. 과제 1의 명령 이동 경로와 의존성 그림 |
| 3주차 | Arc·복사 시 쓰기·트랜잭션·실행 취소 | crates/raster/src/lib.rs, crates/ops/src/lib.rs. 과제 2의 공유 실험과 실패 시 상태 분석 |
| 4주차 | 합성곱·알파·색 공간·병렬 처리 | crates/algo/src/blur.rs, crates/cms/src/lib.rs. 과제 3의 입력 표와 작은 필터 계산 |
| 5주차 | wgpu·WGSL·차등 테스트·파일 왕복 | crates/gpu/tests/it/parity.rs, crates/io/tests/it/corpus.rs. 과제 4의 비교 기록과 호환성 시험 구분 |
| 6주차 | 플랫폼 추상화·MCP·WASM·배포 | crates/automation/src/server.rs, apps/photocraft-web, Dockerfile. 데스크톱·CLI·웹에서 공통인 기능과 다른 서비스 표 |
Document 안에 있는가, UI 상태에 있는가?새로운 라이브러리를 많이 외우는 것보다, 기능 하나의 데이터 흐름과 검증 기준을 끝까지 설명하는 편이 오래 남습니다.
노트 앱이라면 Document를 노트 상태로, 필터를 문서 변환 명령으로 바꿔 생각해 보세요.
명령 레지스트리·실행 취소·직렬화·플랫폼 셸의 분리는 사진 편집 밖에서도 활용할 수 있습니다.
| 용어 | 쉬운 설명 |
|---|---|
| 크레이트 | Rust 코드의 컴파일 단위. 이 저장소는 문서·필터·UI 등의 책임을 여러 Cargo 패키지로 나눕니다. |
| 워크스페이스 | 여러 패키지의 버전·의존성·빌드 설정을 함께 관리하는 묶음입니다. |
| 잠금 파일 | 허용 범위에서 실제 선택된 의존성 버전을 기록한 Cargo.lock입니다. |
| 명령 레지스트리 | 명령 ID와 설명·실행 함수를 모은 목록. 메뉴와 자동화가 같은 기능을 찾는 기준점입니다. |
| 세션 | 열린 문서와 활성 문서, 명령 실행 등 편집 활동의 현재 상태를 관리하는 객체입니다. |
| 레이어 | 픽셀·문자·도형·조정 등을 개별적으로 다루는 문서 구성 요소입니다. |
| 마스크 | 어디에 얼마나 효과를 적용하거나 보이게 할지 정하는 값입니다. |
| 비파괴 편집 | 원본 픽셀을 바로 덮어쓰는 대신 조정·필터 설정을 남겨 나중에 다시 바꾸는 편집입니다. |
| 타일·희소 저장 | 작은 픽셀 블록으로 나누고 실제 필요한 블록만 저장하는 방식입니다. |
Arc | 참조 개수를 관리해 여러 소유자가 같은 데이터를 함께 보관하게 하는 Rust 타입입니다. |
| 복사 시 쓰기 | 읽을 때는 공유하고, 공유 데이터를 바꿀 때 필요한 부분을 복사하는 방식입니다. |
| 스냅샷 | 특정 시점의 상태. 실행 취소나 배경 계산이 안정적인 문서를 참조할 때 쓰입니다. |
| 합성기 | 레이어·마스크·불투명도·효과를 계산해 최종 픽셀을 만드는 부품입니다. |
| 알파·미리 곱하기 | 알파는 불투명도입니다. RGB에 알파를 미리 곱한 표현은 투명 경계의 계산에 쓰입니다. |
| ICC 프로파일 | 픽셀 숫자가 어떤 색을 뜻하는지 설명해, 장치·색 공간 사이 변환에 쓰는 데이터입니다. |
| 기준 구현·오라클 | 다른 구현이나 출력이 맞는지 판단할 비교점입니다. CPU 합성 결과나 외부 기준 이미지가 해당합니다. |
| 차등 테스트 | 같은 입력을 둘 이상의 구현에 넣고 결과 차이를 검사하는 테스트입니다. |
| 왕복 검사 | 읽고 저장한 뒤 다시 읽어, 정한 기준에 따라 정보나 렌더 결과가 유지되는지 보는 검사입니다. |
| 콘텐츠 주소 | 데이터 내용의 해시를 식별자로 사용하는 방식. 같은 타일의 중복 저장을 줄이는 데 쓰입니다. |
| WebAssembly | 브라우저나 별도 런타임에서 실행할 수 있는 바이너리 코드 형식입니다. |
| WGSL·wgpu | WGSL은 GPU 셰이더 언어이고, wgpu는 GPU 장치·버퍼·파이프라인을 다루는 Rust API입니다. |
| MCP | 외부 에이전트가 앱의 도구 목록을 발견하고 호출할 수 있게 하는 공통 프로토콜입니다. |
| 샌드박스·연료 | 실행 권한·메모리·계산량을 제한하는 경계와, 계산량을 소모성 예산으로 세는 장치입니다. |
| 항목 | 값과 해석 |
|---|---|
| 저장소 | storytold/photocraft · TrendShift Monthly #2 |
| 스타 / 포크 | 71,526 / 12,630 · 2026-10-23 GitHub 기준 |
| 생성일 | 2026-09-30 |
| 주 언어 | Rust |
| 최신 릴리스 | v0.14.0 · 2026-10-23 |
| 최근 push | 2026-10-23 · GitHub 기준 |
| 분석 기준 커밋 | ff47388 · Release 0.14.0 · 2026-10-22 20:06:08 −04:00, 한국 시간 2026-10-23 09:06:08 |
| 구성 | 라이브러리 패키지 26개 + 앱 4개 + xtask. 예제 플러그인·퍼징 패키지는 별도 |
| Rust 규모 | 1,502파일 / 661,699줄. 주석·빈 줄·테스트·예제·생성 데이터 포함 |
| 기타 코드 규모 | WGSL 1파일 / 1,520줄, Shell 15파일 / 1,271줄, PowerShell 3파일 / 322줄, Python 2파일 / 275줄 |
| 설정·웹 진입 파일 | TOML 55파일 / 3,574줄, YAML 15파일 / 998줄, HTML 2파일 / 60줄. JavaScript·TypeScript 파일 0개 |
| 테스트 표시 | #[test] 10,656곳. 정적 문자열 집계이며 실제 실행·통과 개수와 다름 |
| 라이선스 | GitHub 대표 표기 Apache-2.0. 소스 패키지는 MIT OR Apache-2.0; 자산·브랜드 규정은 별도 |
| 성숙도 | README·로드맵 기준 초기 알파. PSD·전문 작업·생성형 기능 등의 격차 공개 |
코드 규모는 커밋 ff47388의 저장소 전체에서 .git을 제외하고 확장자별로 센 물리 줄 수입니다.
비어 있지 않은 마지막 줄에 줄바꿈이 없어도 한 줄로 셉니다.
파일 수나 줄 수는 기능 수·품질·개발 기간을 직접 나타내지 않습니다.
버전과 실행 환경: 코드 경로·상수·의존성은 커밋 ff47388 기준입니다.
빌드 성공, OS별 동작, 모든 실습 코드의 실행 결과와 현재 성능은 환경별 확인이 필요합니다.
날짜가 다른 README·설계 문서·점수표의 숫자를 한 시점의 실측치로 합치지 마세요.
PSD의 세 기준: 형식 파서의 바이트 보존, 문서 재저장 전후 렌더 일치,
Photoshop 기준 이미지와 일치는 다른 시험입니다.
crates/io/tests/it/corpus.rs의 Photoshop 코퍼스 시각 일치 하한은 190,
왕복 일치 하한은 258입니다. 하한은 해당 시험의 회귀 방지 기준이며 모든 PSD의 성공률이 아닙니다.
점수표에는 분모 256과 왕복 하한 258이 함께 나오는 불일치도 있어, 이를 백분율로 환산하지 않습니다.
지능형 선택: crates/algo/src/segment/subject.rs::select_subject_with는
SLIC·현저성·GrabCut을 결합한 전통 영상처리 경로입니다.
소스도 의미론적 분할기가 아니라고 밝힙니다. “Select Subject”라는 이름으로 학습 모델이나 생성형 AI가 구현됐다고 추정하면 안 됩니다.
재사용 범위: LICENSE-MIT, LICENSE-APACHE, NOTICE,
ATTRIBUTION.md를 함께 확인하세요.
글꼴·아이콘에는 각자의 라이선스가 있고, docs/brand/LICENSE-brand.txt의 브랜드 자산은 코드 라이선스 적용 대상과 구분됩니다.