사진 편집기
23e1812 기준입니다.
LightCraft는 사진 라이브러리 관리와 RAW 현상을 한 엔진에 묶은 Rust 사진 편집기입니다. 별점·앨범으로 사진을 정리하고, 노출·화이트밸런스·마스크·자르기를 조정한 뒤 새 이미지로 내보냅니다. macOS·Windows·Linux용 앱과 WebAssembly 웹 앱, CLI, MCP 진입점이 있습니다.
편집의 중심은 바뀐 픽셀 파일이 아니라, 원본을 다시 그리는 데 필요한 설정 데이터입니다.
crates/develop/src/settings.rs의 DevelopSettings는 직렬화 가능한 값입니다.
카탈로그가 이 값을 보관하고, crates/pipeline이 원본과 설정을 받아 결과를 계산합니다.
노출을 바꾸는 일은 식재료를 매번 갈아버리는 작업과 다릅니다. “조명을 한 단계 밝게”라는 레시피를 바꾸고, 원본에서 다시 요리합니다. 같은 재료로 작은 시식 접시인 미리보기와 큰 접시인 최종 출력을 만들 수 있습니다.
레시피를 저장하면 실행 취소, 프리셋, 여러 사진에 같은 조정 적용, 이전 버전 재현이 자연스럽게 연결됩니다. 그래서 이 저장소를 읽는 출발점은 필터 수학보다 설정을 어떻게 표현하는가입니다.
| 경계 | 실제 부품 | 초보자가 볼 질문 |
|---|---|---|
| 입력과 설정 | crates/raw·crates/codecs / crates/develop | 파일을 읽는 일과 편집 의도를 표현하는 일이 분리되는가? |
| 설정과 계산 | pipeline::render | 화면 없이도 같은 사진을 만들 수 있는가? |
| 계산과 화면 | crates/engine / crates/ui-egui | GUI를 바꿔도 편집 규칙이 남는가? |
여기서 “백엔드”는 웹 서버를 뜻하지 않습니다. 사용자 컴퓨터 안의 처리 엔진입니다. 기본 사진 편집에 계정이나 별도 데이터베이스 서버를 두는 구조가 아니며, 카탈로그는 자체 파일 저널을 사용합니다.
2026-09-30 생성된 저장소가 2026-10-23 GitHub 기준 스타 14,376개와 포크 5,526개를 기록했고, TrendShift Monthly #21에 선정되었습니다. 카메라 파일 해독부터 사진 관리, GPU 계산, 웹 빌드까지 한 저장소에 들어 있습니다. 다만 이 숫자만으로 성장 원인이나 실제 사용자 수를 알아낼 수는 없습니다.
관심을 끌 만한 지점은 코드로 설명할 수 있습니다. 한 언어로 사진 처리의 바닥부터 화면과 자동화 인터페이스까지 연결한 구조가 이 저장소의 뚜렷한 학습 가치입니다. Rust 파일은 테스트·예제 등을 포함해 719개, 물리적 줄 수로 약 32만 줄입니다. “작은 필터 데모”보다 여러 실행 환경을 가진 제품의 설계를 공부하기에 가깝습니다.
비파괴 편집 자체는 새로운 발명이 아닙니다. darktable 공식 소개도 비파괴 처리와 부동소수점 파이프라인을 설명합니다. Lightroom Classic 공식 문서에는 노출·색·마스크 등 현상 도구가 정리되어 있습니다. 비교의 초점은 화질 순위보다 개발자가 구조를 이해하고 확장할 수 있는 경로에 둡니다.
| 비교 대상 | 겹치는 목적 | LightCraft를 공부할 이유 | 판단의 한계 |
|---|---|---|---|
| Lightroom Classic | 사진 관리와 현상 도구 | 관련 규칙을 Rust 소스에서 추적하고 CLI·MCP로 연결하는 과정을 볼 수 있음 | 소스 공개가 색 재현·카메라 호환성의 동등함을 뜻하지 않음 |
| darktable | 오픈소스 비파괴 RAW 처리 | 자체 RAW 해독기, Cargo 계층, WASM 호스트를 함께 읽을 수 있음 | 비파괴·부동소수점 처리는 LightCraft만의 장점이 아님 |
| 직접 만든 필터 CLI | 이미지를 읽어 변환 후 저장 | 변환 함수를 라이브러리·실행 취소·캐시·저장 복구까지 확장하는 사례 | 코드량이 커서 처음부터 전부 읽는 방식은 비효율적 |
crates/mcp/src/lib.rs에는 실행 중인 앱에 연결하는 Remote와
화면 없이 엔진 세션을 여는 Headless가 함께 있습니다.
MCP를 붙이기 위해 편집 기능을 다시 만드는 대신, 이미 존재하는 명령을 다른 통로로 노출합니다.
이것은 사진 앱뿐 아니라 문서 편집기·CAD·데이터 도구에도 적용할 수 있는 설계입니다.
README와 ROADMAP.md는 프로젝트를 알파로 분류합니다.
README의 약 82% 기능 범위와 약 61% 전체 실무 준비도는 프로젝트 자체 평가이며, 독립적인 품질 점수가 아닙니다.
RAW가 열려도 내장 JPEG 미리보기인지 센서 데이터 현상인지에 따라 편집 가능성이 달라집니다.
루트 Cargo.toml은 members = ["crates/*", "apps/*", "xtask"],
resolver = "3", edition = "2024", 공통 버전 0.13.0을 선언합니다.
라이브러리 크레이트 29개, 앱 3개, 빌드 도구 1개로 총 33개 멤버이며, 루트까지 매니페스트는 34개입니다.
Cargo.toml의 "0.36"은 허용 버전 범위이고,
Cargo.lock의 0.36.2는 이 소스에서 선택된 정확한 버전입니다.| 영역 | 선언 → 잠금 버전 | 구체적인 역할 |
|---|---|---|
| 직렬화 | serde "1" → 1.0.229serde_json "1" → 1.0.151 | 설정·명령 인자·카탈로그 기록을 데이터로 변환. JSON의 float_roundtrip 기능 활성화 |
| 병렬 계산 | rayon "1" → 1.12.0 | CPU에서 픽셀 행과 독립 작업을 나누어 처리 |
| 일반 이미지 | image "0.25" → 0.25.10 | 기본 기능을 끄고 PNG·JPEG·TIFF·WebP를 선택. 자체 코덱 계층과 함께 사용 |
| JPEG·색 관리 | zune-jpeg "0.5" → 0.5.15moxcms "0.9" → 0.9.1 | JPEG 해독과 ICC 색 변환. 잠금 파일에는 전이 의존성의 다른 moxcms 버전도 공존 |
| 메타데이터 | quick-xml "0.38" → 0.38.4 | XMP 등 XML 구조를 읽고 쓰는 기반. 다른 경로의 0.41.0도 잠금 파일에 존재 |
| 날짜·오류 | chrono "0.4.45" → 0.4.45thiserror "2" → 2.0.21 | 촬영 시각 처리와 오류 타입 정의. 수동 달력 계산과 무분별한 패닉을 줄이는 기반 |
| 선택적 모델 추론 | candle-core =0.9.2faer-core =0.17.1 | SAM 3 분할과 노이즈 제거 계산. 가중치 파일은 별도이며, 런타임·플랫폼 기능에 따라 포함 |
| 테스트 도구 | proptest "1" → 1.11.0 | 여러 입력을 자동 생성해 파서·자료구조의 성질 검증 |
표의 공통 선언은 루트 Cargo.toml, 행렬 연산 의존성은 crates/denoise/Cargo.toml 기준입니다.
RAW 처리는 C 라이브러리를 감싼 단일 호출로 끝나지 않습니다.
crates/raw 안에서 파일별 해독, 디모자이킹, 색 변환을 직접 나눕니다.
| 부품 | 선언 → 잠금 버전 | 읽을 위치와 역할 |
|---|---|---|
| 공통 UI | egui "0.36" → 0.36.2 | crates/ui-egui: 패널, 그리드, 슬라이더, 메뉴와 상태 표시 |
| 앱 호스트 | eframe "0.36" → 0.36.2 | 데스크탑 창과 웹 캔버스를 연결. 네이티브는 wgpu·glow, 웹은 glow 기능 사용 |
| GPU 현상 | wgpu "30" → 30.0.1 | crates/gpu: Metal·Vulkan·DX12 연산, WGSL 셰이더 |
| 브라우저 경계 | wasm-bindgen "0.2.129" → 0.2.129 | Rust와 브라우저 API 연결. web-sys·js-sys도 함께 사용 |
| 파일 선택 | rfd "0.17" → 0.17.2 | 네이티브 파일 대화상자. 하위 처리 계층에 섞지 않는 UI 의존성 |
웹에서도 Rust 엔진과 egui 화면을 재사용하고, 브라우저 API를 호스트에서 연결합니다.
apps/lightcraft-web/Cargo.toml은 공통 crates/ui-egui와 engine을 참조합니다.
웹 화면은 WebGL2로 그리지만, 사진 현상 연산에는 아직 WebGPU 경로가 없습니다.
화면을 GPU로 표시하는 일과 사진 필터를 GPU로 계산하는 일을 구분해야 합니다.
apps/lightcraft-web/src/workers.rs의 worker_main은 별도 Web Worker에서 WASM을 실행합니다.
OPFS 또는 IndexedDB에 원본·라이브러리를 저장하고, Worker를 만들 수 없으면 메인 스레드 처리로 돌아갑니다.
HTML과 JavaScript는 얇은 시작 코드이며, package.json·pyproject.toml·go.mod·Dockerfile은 없습니다.
| 파일 | 실제 설정 | 배울 점 |
|---|---|---|
.cargo/config.toml | xtask = "run --package xtask --" | 빌드·검증 작업도 Rust 명령으로 묶기 |
| 루트 프로필 | dist: thin LTO, 코드 생성 단위 1web: fat LTO, opt-level = "s" | 배포 크기와 계산 속도의 서로 다른 목표 |
| 웹의 예외 설정 | pipeline·raster·crates/raw 등은 opt-level = 3 | 전체 크기 최적화 속에서도 픽셀 연산 성능 보존 |
.github/workflows/release.yml | release 브랜치와 수동 실행, 공용 재사용 워크플로 호출 | 저장소의 YAML만으로 외부 워크플로 내부 동작까지 확정할 수는 없음 |
.github/workflows/freebsd.yml | 공용 FreeBSD 워크플로, craft-fonts 커밋 고정 | 플랫폼별 검증과 폰트 입력을 명시적으로 관리 |
flake.nix·nix/package.nix | 네이티브 바이너리와 패키징 환경 구성 | 빌드 환경도 코드로 설명. Nix CI 설정은 전체 빌드 대신 flake 평가를 명시 |
루트 공통 선언, 각 크레이트의 기능 선택, Cargo.lock의 해석 결과를 연결하세요.
예를 들어 루트의 Rust 최소 버전은 1.90이지만 crates/ui-egui/Cargo.toml은 1.95를 요구합니다.
README가 전체 앱 빌드에 Rust 1.95 이상을 안내하는 이유입니다.
전체 지도를 볼 때는 의존성과 실행 시 데이터 흐름을 나누세요.
UI가 엔진을 호출하지만, 엔진은 UI 타입을 몰라야 합니다.
xtask/src/layers.rs의 TABLE과 UI_MIN_LAYER = 5가 이 규칙을 코드로 표현합니다.
도식은 핵심 경로를 요약한 것입니다. 브라우저의 저장은 호스트가 메모리 상태와 비동기 저장소를 연결하며,
네이티브 FsStore의 파일 동기화와 동일한 방식으로 처리하지는 않습니다.
앱과 xtask는 계층 검사에서 예외로 분류됩니다.
또한 창 조작 명령과 파일 대화상자 같은 UI 전용 동작은 호스트에 남습니다.
crates/engine/src/cmd/mod.rs의 CommandSpec에는
id·label·menu·shortcut·params·enabled·run 등이 들어갑니다.
버튼의 제목과 실행 가능 여부, 실제 처리 함수를 한 명세에 모은 구조입니다.
// crates/engine/src/cmd/mod.rs의 함수 타입
pub type Run = fn(&mut Session, &Value) -> Result<Value>;
pub type Enabled = fn(&Session) -> std::result::Result<(), String>;
crates/engine/src/lib.rs의 Session::execute는
find_command(id)로 명세를 찾고, 실행 조건을 검사한 뒤 run_command로 보냅니다.
사진 ID를 명시하는 일부 명령은 선택 상태 검사를 건너뛰지만, 해당 명령이 대상 사진을 직접 검증합니다.
노출 조정의 실제 명령은 crates/engine/src/cmd/develop.rs의 develop.set입니다.
제어 ID와 숫자를 받아 controls::set을 호출하고, Session::set_develop으로 변경을 전달합니다.
UI와 CLI가 “노출을 바꾸는 서로 다른 규칙”을 가질 필요가 줄어듭니다.
# CLI 사용 예시: 예제 사진을 불러와 같은 엔진 명령 호출
lightcraft-cli run --demo develop.set control=light.exposure value=0.7
# 같은 호출을 표현한 엔진 명령 데이터
{"command":"develop.set","params":{"control":"light.exposure","value":0.7}}
run_command는 명령 주위의 패닉 방어, 자동 버전, XMP 자동 쓰기, 영속 저장을 묶습니다.
저장 실패 시 NotSaved를 돌려주는 경로도 있습니다.
명령 실행 성공과 디스크 저장 성공을 구분하는 책임까지 공통 진입점이 다룹니다.
DevelopSettings는 화면 위젯이 아닌 데이터입니다.
to_json·from_json·merged를 제공하고,
숫자 제어는 crates/develop/src/controls.rs의 CONTROLS에서 ID·범위·기본값을 공유합니다.
// ControlSpec::clamp의 실제 구현
pub fn clamp(&self, v: f64) -> f64 {
if v.is_finite() { v.clamp(self.min, self.max) } else { self.default }
}
// 학습용 사용 예시: light.exposure의 범위는 -5..=5
let mut settings = DevelopSettings::default();
let known = controls::set(&mut settings, "light.exposure", 99.0);
assert!(known);
assert_eq!(settings.light.exposure, 5.0);
crates/catalog/src/lib.rs의 Catalog::apply는 연산을 적용하고 역연산을 반환합니다.
Op::SetDevelop의 경우 새 설정을 넣으면서 이전 설정을 담은 반대 연산을 만듭니다.
Session::commit은 정방향 연산을 저장 대기열에 넣고, 역연산을 실행 취소 스택에 보관합니다.
“노출을 0.0에서 0.7로 바꿨다”는 메모와 함께 “돌아갈 때는 0.0으로”라는 귀환 표를 받는 방식입니다. 화면 전체를 복사해 보관하는 것보다 변경의 의미가 분명합니다. 파일 이동처럼 외부 상태가 바뀌는 연산은 별도 복구 정보도 필요하므로 모든 동작이 숫자 교체만큼 단순하지는 않습니다.
슬라이더를 드래그할 때는 begin_interaction으로 시작 상태를 잡고,
set_develop으로 미리보기를 갱신합니다.
end_interaction은 마지막 값을 한 번의 실행 취소 단계로 확정합니다.
중간값이 100번 바뀌어도 사용자의 한 번의 드래그라는 행동 단위를 살리는 설계입니다.
설정의 의미도 버전으로 관리합니다. crates/develop/src/settings.rs의
ProcessVersion과 Process::V1·V2는 계산법이 바뀌어도 오래된 편집을 해석할 기준을 보존합니다.
파일 형식의 버전과 렌더링 알고리즘의 버전은 서로 다른 문제입니다.
crates/pipeline/src/lib.rs의 plan은 설정·원본·출력 크기를 결합해
공통 Plan을 만듭니다. 여기에는 기하 변환, 출력 크기, 세부 효과의 배율,
geo·lin_key 같은 단계별 캐시 키가 들어갑니다.
render_cached는 render_impl을 통해 중간 결과를 재사용합니다.
StageCache의 기본 capacity는 2입니다.
노출만 바뀌었다면 원본 재샘플링과 노이즈 제거를 반복할 필요가 없고, 관련된 뒷단 계산을 다시 수행합니다.
빠른 편집의 핵심은 모든 계산을 더 빨리 하는 것뿐 아니라, 바뀌지 않은 계산을 다시 하지 않는 데 있습니다. 이것은 이미지 처리 밖에서도 빌드 시스템, 스프레드시트, 파생 상태 계산에 연결되는 생각입니다.
GPU는 CPU 구현과 Plan을 공유합니다.
crates/engine/src/media.rs의 develop_image는 조건이 맞으면
lightcraft_gpu::try_render를 시도하고, 사용할 수 없거나 실패하면
pipeline::render_cached 또는 render로 이어집니다.
일부 효과는 GPU 처리 안에서도 CPU 단계를 섞으므로 완전한 단일 GPU 경로라고 이해하면 안 됩니다.
README는 GPU와 CPU의 차이를 1/255 이내라고 소개합니다.
실제 crates/gpu/tests/equivalence.rs의 check는
8비트 sRGB 채널 차이에 대해 MEAN_LSB = 0.5 미만,
MAX_LSB = 3 이하를 검사합니다.
이는 테스트 입력에서 사용하는 허용 기준이며 모든 사진·GPU의 동등성을 증명하지 않습니다.
crates/catalog/src/journal.rs의 Journal은 두 파일을 관리합니다.
catalog.snap은 특정 순번까지의 전체 상태,
catalog.log는 그 뒤에 추가한 연산입니다.
각 로그는 seq·crc·op를 가진 JSON 한 줄입니다.
encode_record는 연산 JSON의 CRC-32를 만들고,
Journal::open은 스냅샷보다 뒤의 기록만 재생합니다.
손상된 마지막 줄과 중간에 생긴 손상을 구분하며, 더 새로운 형식은 파일을 손대지 않고 거부합니다.
CRC는 우발적인 손상 검출용이지 위조 방지용 암호가 아닙니다.
Store 트레이트는 read·append·write_atomic 등을 정의합니다.
FsStore::write_atomic은 임시 파일 쓰기, 동기화, 이름 교체를 수행합니다.
메모리용 MemStore도 같은 인터페이스를 구현하여 파일 손상을 가정한 테스트를 쉽게 만듭니다.
기본 SnapshotPolicy는 로그 2,000개 또는 16 MiB를 스냅샷 전환 판단에 사용합니다.
저장소가 백그라운드 쓰기를 지원하면 snapshot_in_background는 복사한 상태를 저장하는 동안 새 연산을 계속 받고,
poll은 스냅샷 이후 기록만 로그에 남깁니다. 지원하지 않는 저장소에서는 동기식 스냅샷으로 처리합니다.
저장 도중 어느 시점에 멈춰도 무엇을 재생할지 정하는 것이 핵심입니다.
명령 인자 → 제어 명세와 범위 검사 → 새 DevelopSettings → 카탈로그 연산과 역연산 →
저널 저장 → 렌더 계획과 캐시 → CPU/GPU 계산 → 화면 또는 내보내기.
이 흐름을 따라갈 수 있으면 처음 보는 대형 앱에서도 책임의 경계를 찾을 수 있습니다.
| 하고 싶은 질문 | 먼저 읽을 파일 | 이어 읽을 곳 |
|---|---|---|
| 슬라이더 범위는? | crates/develop/src/controls.rs | crates/engine/src/cmd/develop.rs |
| 편집은 어디에 저장? | crates/catalog/src/model.rs | crates/catalog/src/journal.rs |
| CPU는 무엇을 계산? | crates/pipeline/src/lib.rs | crates/pipeline/src/local.rs·crates/pipeline/src/finish.rs와 해당 테스트 |
| GPU 실패 후에는? | crates/engine/src/media.rs | crates/gpu/src/lib.rs·crates/gpu/tests/fallback.rs |
| RAW를 어떻게 읽나? | crates/raw/src/lib.rs | crates/raw/src/vendor/·crates/raw/src/demosaic/·crates/raw/src/color.rs |
| MCP 도구는 어디서? | crates/mcp/src/tools.rs | crates/mcp/src/server.rs·crates/mcp/src/backend.rs·crates/mcp/src/headless.rs |
| 웹에 사진이 남는가? | apps/lightcraft-web/src/store.rs | apps/lightcraft-web/src/backend.rs·apps/lightcraft-web/src/files.rs·apps/lightcraft-web/src/backup.rs |
crates/engine 약 92,000줄과 crates/ui-egui 약 82,800줄을 처음부터 모두 읽기보다,
약 2,800줄의 crates/develop에서 설정과 제어 ID를 파악하는 편이 좋습니다.
다음으로 명령 한 개와 관련 테스트를 이어 읽으면 읽을 범위를 좁힐 수 있습니다.
저장소 문서의 숫자는 작성 시점에 따라 다릅니다.
docs/architecture.md의 약 202,000줄·505파일·라이브러리 24개는 2026-10-10의 집계입니다.
커밋 23e1812의 319,925줄·719개 Rust 파일·라이브러리 29개와 구분해야 합니다.
대형 병원 지도를 외우기보다 “접수는 어디, 검사는 어디, 기록은 어디”를 아는 것과 같습니다. 파일 이름은 위치이고, 타입과 함수 사이의 호출은 실제 동선입니다. 디렉토리 트리 다음에는 반드시 명령 하나의 동선을 따라가 보세요.
crates/raster/src/lib.rs는 Rgb32f = Image<[f32; 3]>와
Rgba8 = Image<[u8; 4]>를 구분합니다.
전자는 계산용 선형 RGB, 후자는 주로 표시용 결과입니다.
타입 이름만으로 어느 단계의 버퍼인지 드러내는 장점을 먼저 보세요.
엔진과 캐시는 큰 이미지에 Arc<Rgb32f>를 사용합니다.
Arc::clone은 픽셀 전체 복사와 다르지만, 내부 이미지를 clone하면 실제 데이터가 복사될 수 있습니다.
pipeline::render_impl의 Arc::unwrap_or_clone과 캐시 유무 분기를 나란히 읽어보세요.
Arc는 여러 소유자가 같은 값을 공유하도록 참조 수를 관리합니다.
Mutex는 공유 상태에 한 번에 하나의 실행 흐름만 들어가도록 잠급니다.
“공유할 수 있다”와 “동시에 수정해도 된다”는 다른 조건입니다.작은 실습: 픽셀 버퍼를 가리키는 Arc 두 개를 그림으로 표시한 뒤,
“참조만 복제한 위치”와 “픽셀을 복제한 위치”를 구분하세요.
디스크 캐시, 중간 단계 캐시, 원본 캐시가 각각 어떤 값을 소유하는지도 함께 적어봅니다.
crates/color/src/transfer.rs의 srgb_to_linear는
화면에 저장된 sRGB 값을 빛의 계산에 맞는 선형 값으로 바꿉니다.
sRGB의 0.5는 선형 밝기 약 0.214에 해당합니다.
검정과 흰색을 선형 공간에서 반씩 섞은 값 0.5를 다시 sRGB로 바꾸면 약 0.735가 됩니다.
// transfer.rs의 실제 변환 함수
pub fn srgb_to_linear(v: f32) -> f32 {
if v <= 0.04045 { v / 12.92 }
else { ((v + 0.055) / 1.055).powf(2.4) }
}
작은 실습: 두 숫자를 그대로 평균하는 경우와 선형 변환 후 평균하는 경우를 종이에 비교하세요.
이후 crates/raw/src/color.rs의 카메라 색 → 선형 Rec.2020 변환,
crates/pipeline/src/output.rs의 출력 색 공간으로 학습 범위를 넓힙니다.
DevelopSettings::merged는 부분 JSON을 병합한 뒤 제어 범위를 다시 맞춥니다.
누락 필드의 기본값과 ProcessVersion의 과거 해석 규칙은 저장 데이터가 오래 살아남게 하는 장치입니다.
반대로 카탈로그는 지원하지 않는 새 형식을 거부합니다. 설정 필드를 유연하게 읽는 방식과 카탈로그 형식을 엄격히 보호하는 방식을 구분하세요.
작은 실습: 노출만 있는 설정, 빈 설정, 미래 처리 버전이 있는 설정을 나란히 놓고 어떤 값이 기본값을 받으며 어떤 정보가 보존되는지 표로 정리합니다. “기본값을 추가했더니 예전 사진의 모습이 바뀐다”는 문제를 버전 모델이 어떻게 막는지도 설명해 봅니다.
crates/raster/src/lib.rs의 par_rows는 기능 설정에 따라
Rayon의 par_chunks_mut 또는 순차 chunks_mut를 사용합니다.
서로 겹치지 않는 행 조각을 나눠 작업한다는 점이 안전성과 병렬성의 연결점입니다.
한편 crates/preview/src/pool.rs의 JobPool은 작업 우선순위와 중복을 관리합니다.
네이티브의 PreviewCache::put_deferred_at은 새 이미지를 포함한 디스크 쓰기 대기량이 128 MiB를 넘으면 그 이미지의 디스크 쓰기를 생략합니다.
결과를 메모리 캐시에 넣되, 느린 저장 장치 때문에 대기열이 끝없이 늘어나지 않게 합니다.
작은 실습: 현재 보이는 썸네일 10개와 화면 밖 썸네일 1,000개의 처리 순서를 설계해 보세요. 사용자가 빠르게 다음 사진으로 이동할 때, 오래된 결과가 현재 화면을 덮지 않도록 필요한 ID도 적습니다.
apps/lightcraft-web/src/workers.rs는 컴파일된 WASM 모듈을 Worker에 전달하고,
작업을 JSON으로 보내며 결과 RGBA 버퍼를 전송합니다. 공유 메모리 기반 WASM 스레드를 요구하는 구조와 다릅니다.
Worker마다 모듈 인스턴스와 캐시가 있으므로 메시지·메모리 비용까지 생각해야 합니다.
MCP 쪽 Server::handle_line은 JSON-RPC 메시지를 받아 Backend로 연결합니다.
crates/mcp/src/server.rs의 기본 프로토콜 버전은 2025-06-18입니다.
tools.rs의 command_tool_name은 엔진 명령 ID를 도구 이름으로 바꿉니다.
작은 실습: develop.set을 GUI 버튼, CLI 인자, MCP 인자로 각각 표현하고,
끝에서 같은 엔진 명령에 도착하는 지점을 그려보세요.
새 인터페이스를 만드는 실력은 핵심 기능을 복제하는 대신 기존 규칙에 연결하는 능력에서 드러납니다.
| 항목 | 소스·문서에서 확인되는 조건 | 학습 시 판단 |
|---|---|---|
| Rust | 공통 선언 1.90, UI 크레이트 1.95. README는 전체 빌드 1.95 이상 안내 | 앱 빌드는 1.95 이상을 기준으로 준비 |
| 운영체제 | macOS·Windows·Linux 네이티브. macOS 패키징 최소 버전은 11.0. FreeBSD 구성도 존재 | 각 운영체제의 모든 버전·기기가 검증되었다는 의미는 아님 |
| 사진 계산 GPU | macOS Metal, Windows 기본 DX12, Linux 계열 Vulkan. 실패 시 CPU 경로 | 독립 GPU를 학습의 필수 조건으로 볼 필요는 없음 |
| macOS 배포 설정 | packaging/macos/Info.plist.in의 LSMinimumSystemVersion = 11.0 | packaging/macos/package.sh의 배포 타깃도 11.0. 개별 기기 검증과는 구분 |
| 화면 표시 GPU | 네이티브 eframe 렌더러, 웹 WebGL2 | 사진 계산의 CPU 대체 경로가 창 표시까지 무조건 보장하지는 않음 |
| 웹 빌드 | wasm32-unknown-unknown, 잠금 버전과 맞는 wasm-bindgen CLI | 버전 불일치는 xtask web의 점검 대상 |
| 브라우저 저장 | OPFS 또는 IndexedDB, 비동기 저장과 백업 기능 | 저장 공간 제한·삭제 가능성을 고려해 원본과 백업 보관 |
| 모델 기능 | SAM 3 가중치 약 3.4 GB 별도 설치, 사용 시 메모리 수 GB 필요 | 첫 실습은 JPEG와 일반 조정으로 시작. 모델 기능은 선택 |
| 언어·폰트 | 추가 CJK 글꼴은 craft-fonts 빌드 입력. README의 UI 언어 목록에 한국어 없음 | 한국어 해설과 앱의 한국어 UI 지원은 별개 |
Rgb32f의 픽셀은 RGB 세 채널에 각각 4바이트를 씁니다.
6,000 × 4,000, 즉 24 MP 이미지를 한 벌만 펼쳐도 다음 크기입니다.
6,000 × 4,000 × 3 × 4바이트
= 288,000,000바이트
≈ 274.7 MiB
# 원본 파일 크기나 앱 전체 메모리 사용량과는 다른 값
# 중간 결과·마스크·GPU 버퍼·출력 버퍼가 추가될 수 있음
crates/engine/src/memory.rs의 default_budget은
RAM을 읽을 수 있으면 그 1/4을 256 MiB~1.5 GiB 범위로 제한해 기본 예산으로 사용합니다.
RAM을 읽지 못하면 1.5 GiB를 쓰며, LIGHTCRAFT_MEMORY_MB로 바꿀 수 있습니다.
캐시 예산은 프로세스 전체 메모리 사용량의 상한이 아닙니다. 소스 설명에서도 작업 중 이미지, UI 텍스처, 할당자, GPU 드라이버의 메모리를 구분합니다. 배치 내보내기는 추가 작업 메모리를 사용할 수 있으므로 예산 숫자만 보고 대용량 병렬 처리를 결정하면 안 됩니다.
공통 CPU 모델·RAM·VRAM 최소값 대신, 학습용 제안으로 RAM 8 GB급에서는 작은 JPEG 한 장부터, 16 GB 이상에서는 RAW 한 장씩 처리하는 순서를 권합니다. 이는 공식 최소 사양이나 동작 보장이 아닙니다. 모델 추론·고해상도 배치에는 별도 여유가 필요합니다.
빌드도 메모리를 사용합니다. README에는 16 GB 환경에서 동시 릴리스 링크 중 메모리 부족 사례가 기록되어 있습니다.
xtask/src/main.rs의 ci_jobs는 사용 가능 메모리를 1,536 MiB로 나눠 빌드 병렬 수를 잡고,
테스트 스레드는 최대 4개로 제한합니다. 일반 cargo build에 이 정책이 자동 적용되는 것은 아닙니다.
# 독자가 소스 빌드를 선택할 때 참고할 명령
CARGO_BUILD_JOBS=1 cargo build --release -p lightcraft -p lightcraft-cli
README의 24 MP RAW·Apple M4 Pro 사례는 슬라이더 약 4 ms, 2.5 MP 보기 약 30 ms, 전체 크기 내보내기 약 0.3초입니다. 프로젝트 문서의 측정 사례이며, 보편적인 보장 수치가 아닙니다. 입력 형식, 캐시 상태, 효과, 출력 크기, GPU에 따라 시간이 달라집니다.
브라우저의 사진 연산은 CPU 기반이고, Worker가 실패하면 메인 스레드로 돌아갑니다. 모델 기반 새 마스크 생성과 네이티브의 GPU 경로를 웹에서 그대로 기대하면 안 됩니다. 저장된 모델 마스크를 렌더링하는 기능과 모델을 실행해 새로 만드는 기능도 다릅니다.
docs/web.md는 명령 이후 브라우저 저장이 뒤따르는 지연 구간을 명시합니다.
이 구간에서 탭이 종료되면 마지막 명령을 잃을 수 있습니다. 네이티브가 저장을 완료한 뒤 성공을 응답하는 정책과 구분하세요.
읽기만으로 풀 수 있는 부분부터 시작하고, 실행·수정 실습은 독립된 연습용 사본과 복사한 사진으로 진행하세요. 아래 명령은 독자가 사용할 예시입니다. 원본 촬영본 대신 재생성 가능한 입력을 사용하면 결과를 비교하기 쉽습니다.
목표: 명령 ID, 제어 ID, 설정 필드를 구분합니다.
develop.set은 행동 이름이고, light.exposure는 바꿀 숫자의 이름입니다.
crates/develop/src/controls.rs에서 노출의 최소 -5, 최대 5, 기본 0, 간격 0.01을 찾습니다.crates/engine/src/cmd/develop.rs에서 제어 ID를 검사하고 값을 적용하는 분기를 찾습니다.Session::set_develop에서 설정이 연산으로 바뀌는 지점까지 화살표를 그립니다.산출물: 파일 경로 세 개와 함수 이름, 각 단계의 입력·출력을 담은 한 장짜리 표. 완료 기준: “슬라이더를 없애도 명령이 동작하는 이유”를 코드 위치와 함께 설명할 수 있습니다.
# 설치된 CLI가 있을 때 제어 명세를 살펴보는 예시
lightcraft-cli controls --json
lightcraft-cli commands --json
# 사진 사본을 작은 결과로 내보내는 예시
lightcraft-cli render sample.jpg -o practice.jpg --size 1600 \
--set light.exposure=0.7
목표: controls::set과 ControlSpec::clamp의 책임을 검증합니다.
UI에서 막는 것과 엔진 데이터 단계에서 막는 것의 차이를 배웁니다.
f64::NAN을 차례로 전달합니다.light.noSuchControl도 전달하고 반환값을 비교합니다.DevelopSettings::merged로 부분 JSON을 병합했을 때 범위 보정이 이어지는지 읽습니다.산출물: 입력, 성공 여부, 저장된 노출 값을 담은 검증 표와 작은 테스트.
완료 기준: 알려진 제어의 큰 값은 5.0, NaN은 기본값 0.0,
알 수 없는 제어는 false가 되는 이유를 구분합니다.
// 연습용 테스트의 핵심. 필요한 타입·모듈은 별도 import
let mut s = DevelopSettings::default();
assert!(controls::set(&mut s, "light.exposure", 99.0));
assert_eq!(s.light.exposure, 5.0);
assert!(controls::set(&mut s, "light.exposure", f64::NAN));
assert_eq!(s.light.exposure, 0.0);
assert!(!controls::set(&mut s, "light.noSuchControl", 1.0));
목표: 값이 바뀐 횟수와 사용자가 느끼는 행동 횟수를 구분합니다.
begin_interaction·set_develop·end_interaction을 읽고 작은 세션 실험을 구성하세요.
undo_step 한 번으로 처음 설정, redo_step 한 번으로 마지막 설정이 되는지 확인합니다.cancel_interaction을 호출했을 때 원래 값으로 돌아가는 경로를 비교합니다.산출물: “시작·중간·종료·취소” 상태표와 재현 가능한 테스트. 완료 기준: 미리보기 변경과 영구 기록의 시점이 다르다는 것을 설명합니다.
목표: 정상 종료만 통과하는 저장 코드에서 한 단계 나아갑니다.
crates/catalog/src/tests_torn_append.rs와 crates/catalog/src/tests_journal.rs를 참고하고,
실제 사진 라이브러리 대신 MemStore를 사용하세요.
Journal::open의 재생 결과와 LoadReport를 비교합니다.산출물: 정상 로그·잘린 로그·복구 후 상태의 비교표. 완료 기준: 마지막 기록 손상과 중간 기록 손상의 처리 차이, 성공을 반환하기 전에 저장을 완료해야 하는 이유를 설명합니다.
과제마다 결과 사진만 남기지 말고 입력 조건과 기대 결과를 적으세요. 특히 캐시·영속성·병렬 처리는 겉으로 같은 화면이 보여도 내부 상태가 틀릴 수 있어, 관찰 가능한 기준을 먼저 세우는 연습이 중요합니다.
Rust가 처음이라면 공식 입문서의 소유권·구조체·열거형·오류 처리를 먼저 익히세요. 아래 일정은 제안이며, 이미지 수학을 모두 배운 뒤 코드를 읽을 필요는 없습니다. 한 주에 새로운 개념 하나와 검증 가능한 결과물 하나를 연결하는 편이 지속하기 쉽습니다.
| 주차 | 학습 주제 | LightCraft에서 읽을 곳 | 남길 결과물 |
|---|---|---|---|
| 1주 | 소유권, 참조, Result, Cargo | crates/raster/src/lib.rs, 루트 매니페스트 | 픽셀 버퍼·소유권 그림, 크레이트 의존성 지도 |
| 2주 | Serde, 스키마, 범위 검사 | crates/develop/src/settings.rs·crates/develop/src/controls.rs | 과제 1·2의 입력 검증 표와 테스트 |
| 3주 | 명령 패턴, 역연산, 상호작용 | crates/engine/src/cmd/mod.rs·crates/engine/src/lib.rs | 과제 3의 상태 전이와 실행 취소 실험 |
| 4주 | 선형 RGB, 재샘플링, 캐시 의존성 | crates/color/src/transfer.rs·crates/pipeline/src/lib.rs | sRGB 혼합 비교, 슬라이더별 무효화 범위 표 |
| 5주 | 로그 재생, 원자적 쓰기, 복구 | crates/catalog/src/journal.rs·crates/catalog/src/store.rs | 과제 4와 장애 발생 시점별 복구 표 |
| 6주 | 실행 환경 경계와 공통 규칙 | crates/mcp/src/server.rs·apps/lightcraft-web/src/workers.rs·GPU 테스트 | 관심 분야 하나의 호출 흐름과 검증 계획 |
checked_mul과 잘못된 입력 테스트를 공부합니다.crates/gpu/src/wgsl/의 한 커널을 CPU 함수와 대응시키고, 오차 기준의 단위를 설명합니다.입력 검증은 어디서 끝나는가, 상태 변경은 어디서 확정되는가, 실패 시 누가 책임지는가, 어떤 입력이 캐시 키를 바꾸는가를 답할 수 있다면 학습이 진행된 것입니다. 함수 이름을 외우는 것보다 이 질문을 다른 앱에도 적용해 보세요.
| 용어 | 쉬운 설명 | 이 저장소의 연결점 |
|---|---|---|
| RAW | 카메라 센서 데이터와 해석 정보를 담는 파일 계열. 제조사·압축 방식마다 해독이 다름 | raw::decode·RawImage |
| 비파괴 편집 | 원본을 덮어쓰는 대신 조정값을 따로 저장하고 필요할 때 다시 계산 | DevelopSettings |
| CFA·디모자이킹 | 센서의 색 필터 배열에서 주변 표본을 이용해 RGB 색을 복원하는 과정 | crates/raw/src/demosaic/ |
| 선형 RGB | 숫자 변화가 빛의 양 변화에 대응하도록 표현한 RGB. 물리적인 혼합·노출 계산의 기반 | srgb_to_linear·Rgb32f |
| 색 영역 | 어떤 색 좌표계로 얼마나 넓은 색 범위를 표현하는지 나타내는 개념 | 선형 Rec.2020 내부 처리와 출력 색 공간 |
| ICC 프로필 | 화면·이미지·프린터의 색 특성을 설명해 색 변환에 사용하는 데이터 | moxcms·crates/engine/src/display.rs |
| 크레이트 | Rust에서 분리해 컴파일하고 의존성을 관리하는 라이브러리·프로그램 단위 | crates/의 각 Cargo.toml |
| 특성 선택 | Cargo 기능 플래그로 선택적 코드·의존성을 포함하는 것 | heif·denoise·sam |
| 직렬화 | 메모리의 구조체를 JSON 같은 저장·전달 형식으로 바꾸는 것 | Serde와 DevelopSettings::to_json |
| 명령 패턴 | 사용자의 행동을 ID·인자·실행 함수로 표현하는 설계 | CommandSpec·Session::execute |
| 역연산 | 변경 이전 상태로 돌아가게 하는 반대 방향의 연산 | Catalog::apply 반환값 |
| 저널 | 변경을 순서대로 덧붙여 두는 기록. 다시 읽어 상태를 재구성할 수 있음 | catalog.log |
| 스냅샷 | 특정 시점의 전체 상태. 오래된 변경을 처음부터 재생하는 비용을 줄임 | catalog.snap |
| 원자적 교체 | 중간에 섞인 파일 대신 이전 내용이나 새 내용 중 하나가 보이게 교체 | Store::write_atomic |
| CRC-32 | 기록이 우발적으로 손상됐는지 검사하는 짧은 값. 암호 인증과는 다름 | encode_record |
| LRU 캐시 | 오래 사용하지 않은 항목부터 비워 제한된 공간을 관리하는 캐시 | crates/preview/src/lru.rs |
| 캐시 무효화 | 입력이 달라졌을 때 이전 계산 결과를 재사용하지 않도록 판단하는 일 | Plan::geo·lin_key |
| 대체 경로 | 빠른 처리 방식이 불가능하거나 실패했을 때 다른 구현으로 완료하는 경로 | GPU 실패 뒤 CPU 렌더 |
| LSB | 표현 가능한 가장 작은 한 단계. 8비트 채널에서는 정수 값 1의 차이 | CPU/GPU 동등성 테스트의 오차 단위 |
| WASM·Web Worker | 브라우저용 실행 코드 형식과, 화면의 주 실행 흐름 밖에서 일을 처리하는 수단 | 웹 앱과 worker_main |
| OPFS·IndexedDB | 사이트 단위로 파일이나 구조화 데이터를 보관하는 브라우저 저장 기능 | 웹 라이브러리의 지속 저장 |
| MCP·JSON-RPC | 도구를 연결하는 프로토콜과, 이름·인자·응답으로 원격 호출을 표현하는 메시지 형식 | mcp::Server와 표준 입출력 |
| XMP 사이드카 | 사진 옆에 메타데이터·편집 정보를 따로 보관하는 XML 기반 파일 | crates/engine/src/sidecar.rs·meta |
| 처리 버전 | 같은 설정값을 어떤 계산법으로 해석할지 고정하는 버전 | ProcessVersion·Process |
처음부터 모든 약어를 외울 필요는 없습니다. “사진 원본은 무엇인가 → 설정은 무엇인가 → 계산 결과는 무엇인가 → 저장 기록은 무엇인가”로 묶으면 비슷해 보이는 타입과 파일도 역할을 나눠 이해할 수 있습니다.
| 항목 | 상태 | 기준 |
|---|---|---|
| 관심 지표 | 스타 14,376 · 포크 5,526 | 2026-10-23 GitHub 기준 |
| 생성일 | 2026-09-30 | GitHub 저장소 메타데이터 |
| 최근 푸시 | 2026-10-23 | GitHub 저장소 메타데이터 |
| 최신 릴리스 | v0.13.0 · 2026-10-23 | GitHub 릴리스 기준 |
| 분석 커밋 | 23e1812 · 2026-10-22 · Release 0.13.0 | 커밋 기록의 날짜. 릴리스 게시일과 구분 |
| 주 언어 | Rust | GitHub 기준, 코드 실측과도 일치 |
| Rust 규모 | 719파일 · 319,925줄 | 커밋 기준 물리적 줄 수, 테스트·예제·빌드 코드·주석·빈 줄 포함 |
| GPU 셰이더 | WGSL 11파일 · 1,920줄 | 언어별 확장자 실측 |
| 보조 언어 | Shell 12파일·1,027줄 Python 5파일·577줄 Nix 2파일·337줄 | 언어별 확장자 실측 |
| 웹 시작 코드 | JavaScript 1파일·15줄 HTML 1파일·49줄 | WASM 빌드가 생성하는 접착 코드는 집계에 포함하지 않음 |
| 패키지 구성 | 라이브러리 29개 · 앱 3개 · xtask 1개 | 루트 Cargo 워크스페이스 구성 |
| 라이선스 | GitHub 표시 Apache-2.0 공통 소스 선언 MIT OR Apache-2.0 | Cargo.toml·LICENSE-MIT·LICENSE-APACHE·NOTICE 확인 |
| 개발 단계 | 알파 | README·ROADMAP의 자체 분류 |
줄 수는 파일 확장자별 물리적 행의 합입니다. 실행되는 제품 코드만의 크기, 구현 품질, 개발 시간과 같지 않습니다.
crates/engine 92,033줄, crates/ui-egui 82,846줄,
crates/raw 38,311줄에는 해당 디렉토리의 테스트·예제도 포함됩니다.
호환성과 성숙도: 카메라·압축 변형에 따라 센서 데이터 대신 내장 JPEG를 표시할 수 있습니다. 색 보정, 카메라별 검증, HDR 화면 표시 등 미완성 영역이 남아 있습니다. 기능 체크 표시와 실제 촬영본의 품질을 구분하고, 원본·카탈로그 백업을 보관하세요.
수치의 범위: 기능 완성도·속도는 프로젝트 문서의 평가와 측정 사례입니다. 정적 소스와 테스트의 존재가 모든 플랫폼에서의 통과나 실측 성능을 뜻하지 않습니다. GPU·모델·사진 표본이 없으면 관련 테스트가 건너뛰어질 수 있으며, xtask는 그 사유를 보고하도록 설계되어 있습니다.
로컬 처리의 선택적 예외: 기본 편집은 로컬 중심이지만,
crates/denoise/src/remote.rs에는 선택적 원격 노이즈 제거가 있습니다.
이를 설정하면 이미지 타일이 다른 컴퓨터로 전송되며 연결 자체는 암호화되지 않습니다.
SAM 3 원격 실행도 별도 선택 기능입니다. “어떤 설정에서도 네트워크 전송이 없다”는 설명은 정확하지 않습니다.
라이선스 범위: 공통 워크스페이스의 이중 라이선스와 달리
crates/segment/Cargo.toml은 Apache-2.0을 명시합니다.
모델 가중치와 브랜드 자산에는 별도 조건이 있으므로, 소스 라이선스를 모든 배포물에 그대로 적용하면 안 됩니다.
구현의 경계: “순수 Rust”는 시스템 의존성까지 없다는 뜻이 아닙니다.
운영체제 API·그래픽 드라이버를 사용하고, crates/sysmem에는 macOS 메모리 반환용 FFI가 격리되어 있습니다.
외부 공용 CI 워크플로의 내부 구현이나 전체 변경 이력은 이 커밋의 소스 범위를 넘어섭니다.