diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 0b528ae1..c63fdeef 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -26,15 +26,15 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 300 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Optional font build input, so the Japanese font tests run (they skip without it). - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 path: craft-fonts - name: Build and test in a FreeBSD VM - uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9 with: release: "14.3" usesh: true diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index 0e2388a6..46ce3c90 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -23,7 +23,7 @@ jobs: env: ACTIONLINT_VERSION: 1.7.12 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install tools run: | sudo apt-get update @@ -51,6 +51,11 @@ jobs: if grep -oE '' 'Value="InstallDirDlg"' '' \ + 'Key="Software\LightCraft" Name="InstallFolder"' 'Value="LaunchLightcraft"'; do + grep -qF "$want" packaging/windows/lightcraft.wxs || { echo "::error::lightcraft.wxs lost: $want"; exit 1; } + done desktop-file-validate packaging/linux/ai.storyteller.lightcraft.desktop sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.lightcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.lightcraft.metainfo.xml" appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.lightcraft.metainfo.xml" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9c3a1ea..c81bf050 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: date: ${{ steps.v.outputs.date }} prerelease: ${{ steps.v.outputs.prerelease }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - id: v name: Resolve version run: | @@ -72,17 +72,17 @@ jobs: LIGHTCRAFT_VERSION: ${{ needs.version.outputs.version }} LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately path: craft-fonts - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable with: targets: aarch64-apple-darwin, x86_64-apple-darwin - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-macos-universal - name: Apply version override @@ -102,7 +102,7 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: packaging/macos/package.sh --arch universal - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-macos path: dist/release/* @@ -130,23 +130,24 @@ jobs: LIGHTCRAFT_VERSION: ${{ needs.version.outputs.version }} LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately path: craft-fonts - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-windows-${{ matrix.arch }} - name: Apply version override if: inputs.version != '' shell: bash run: cargo xtask version set "$LIGHTCRAFT_VERSION" + # package.ps1 adds the matching WixToolset.UI/Util.wixext (5.0.2) to the global extension cache. - name: Install WiX shell: pwsh run: dotnet tool install --global wix --version 5.0.2 @@ -164,7 +165,7 @@ jobs: AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-windows-${{ matrix.arch }} path: dist/release/* @@ -187,9 +188,9 @@ jobs: LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} NFPM_VERSION: 2.47.0 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately @@ -203,8 +204,8 @@ jobs: curl -fsSL -o "$RUNNER_TEMP/nfpm.deb" \ "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${{ matrix.deb }}.deb" sudo dpkg -i "$RUNNER_TEMP/nfpm.deb" - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-linux-${{ matrix.arch }} - name: Apply version override @@ -228,7 +229,7 @@ jobs: "$appimage" --appimage-updateinformation | grep -F 'gh-releases-zsync|' test -s "$appimage.zsync" done - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-linux-${{ matrix.arch }} path: dist/release/* @@ -250,8 +251,8 @@ jobs: LIGHTCRAFT_VERSION: ${{ needs.version.outputs.version }} LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-linux-${{ matrix.arch }} path: dist/linux @@ -265,7 +266,7 @@ jobs: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Build the Flatpak bundle run: packaging/linux/flatpak-bundle.sh "dist/linux/lightcraft-$LIGHTCRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-flatpak-${{ matrix.arch }} path: dist/release/*.flatpak @@ -282,16 +283,16 @@ jobs: LIGHTCRAFT_VERSION: ${{ needs.version.outputs.version }} LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately path: craft-fonts - name: Build and package in a FreeBSD VM (tar.gz) # vmactions/freebsd-vm v1.5.9, pinned by commit (it runs our build). - uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9 with: release: "14.3" usesh: true @@ -316,7 +317,7 @@ jobs: bash packaging/freebsd/package.sh tar -tzf dist/release/lightcraft-*-freebsd-x86_64.tar.gz \ | grep -E '/bin/lightcraft$|/bin/lightcraft-cli$|/share/applications/ai\.storyteller\.lightcraft\.desktop$|/share/doc/lightcraft/LICENSE-MIT$' - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-freebsd-x86_64 path: dist/release/* @@ -331,17 +332,17 @@ jobs: LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} WASM_BINDGEN_VERSION: 0.2.129 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: storytold/craft-fonts ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately path: craft-fonts - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable with: targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: release-web - name: Install wasm-bindgen CLI @@ -359,7 +360,7 @@ jobs: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts CRAFT_FONTS_REQUIRED: "1" run: packaging/web/package.sh - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-web path: dist/release/* @@ -378,7 +379,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: release-* path: artifacts diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml index d6067b3f..1cccd858 100644 --- a/.github/workflows/windows-arm64.yml +++ b/.github/workflows/windows-arm64.yml @@ -36,11 +36,11 @@ jobs: runs-on: windows-latest timeout-minutes: 90 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@89b12181fb390509a0842a86cc55eeb8eb928c1d # stable + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable with: targets: aarch64-pc-windows-msvc - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: key: windows-arm64-package - name: Install WiX @@ -51,7 +51,7 @@ jobs: env: DIST: ${{ github.workspace }}\dist\arm64 run: ./packaging/windows/package.ps1 -Arch arm64 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: lightcraft-windows-arm64 path: dist/arm64/* @@ -64,7 +64,7 @@ jobs: runs-on: windows-11-arm timeout-minutes: 30 steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: lightcraft-windows-arm64 path: dist diff --git a/AGENTS.md b/AGENTS.md index 3ab653c7..8cd199ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,12 +5,18 @@ LightCraft is a clean-room, open-source, pure-Rust photo library + non-destructi ## Start every session here 1. Read `plan/STATUS.md` (current milestone, next unchecked task, blockers). 2. Read the task in `plan/execution-plan.md` §3, the relevant section of `plan/architecture.md`, and the README/docs of the crate you touch. Behaviour/visual reference: `plan/lightroom/` (incl. `10-observed-ui.md` + screenshots). -3. **Know where we stand:** read [`ROADMAP.md`](ROADMAP.md) → *Where we stand* (honest assessment by dimension and - by kind of user) and *Where we're going* (ordered priorities). The checklist counts features that *exist*; the real - gaps are quality and coverage: camera colour calibration, CR3 / compressed raws, per-model verification, render - fidelity against Lightroom, AI models, HDR / video / Classic modules. A ✅ row is not proof of parity — if you find a - ✅ feature that is wrong or incomplete, downgrade it to 🟡 with a note. -4. **Pick work** from [`docs/parity.md`](docs/parity.md) → *Top gaps* (ordered by user impact; the tracker has one row per feature, menu item and shortcut). When you land a feature, update its row(s) and the gap list in the same commit, and the *Where we stand* / *Where we're going* sections of `ROADMAP.md` when a listed gap closes; `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and path the tracker cites still exists, and `cargo xtask parity --write` refreshes its summary. +3. **Know where we stand:** read [`ROADMAP.md`](ROADMAP.md) (stage, headline numbers, every dimension) and, for + method and evidence, [`docs/target-app-parity.md`](docs/target-app-parity.md). The checklist counts features that + *exist*; the real gaps are quality and coverage: camera colour calibration, CR3 / compressed raws, per-model + verification, render fidelity against Lightroom, AI models, HDR / video / Classic modules (raw detail: + [`docs/raw-parity.md`](docs/raw-parity.md)). A ✅ row is not proof of parity — if you find a ✅ feature that is + wrong or incomplete, downgrade it to 🟡 with a note. +4. **Pick work** from [`docs/gaps.md`](docs/gaps.md) (ranked by user impact; `B` marks beta blockers). The row-by-row + tracker is [`docs/parity-checklist.md`](docs/parity-checklist.md) (one row per feature, menu item and shortcut). + When you land a feature, update its row(s) and the gap entry in the same commit, and the numbers in `ROADMAP.md` / + `docs/target-app-parity.md` when a listed gap closes (bump each doc's *Last updated* line and revision history, per + craftrules `standards/progress-docs.md`); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and + path the tracker cites still exists, and `cargo xtask parity --write` refreshes its summary. 5. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's. `plan/` is gitignored (local-only). @@ -74,9 +80,10 @@ of it. Full standard: `../craftrules/standards/never-crash.md` font files here (Inter, already in `assets/fonts/`, is the one exception); add new fonts to craft-fonts. LightCraft uses it as the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts` then `CRAFT_FONTS_DIR=../craft-fonts cargo run -p lightcraft` (or any cargo/xtask command). `crates/engine/build.rs` - embeds the manifest's fonts as `lightcraft_engine::CRAFT_FONTS` (wasm32: BIZ UDPGothic Regular only); the UI - (`theme::font_definitions`) and the export watermark renderer use its CJK faces (picked by script) as fallbacks after Inter. Unset, - `CRAFT_FONTS` is empty: everything builds, tests and runs, but Japanese text has no glyphs. Releases always build + embeds the manifest's fonts as `lightcraft_engine::CRAFT_FONTS` (wasm32: BIZ UDPGothic Regular only); `cargo xtask web` + ships Noto Sans CJK SC Regular as a separate asset loaded by the UI before startup. The UI + (`theme::font_definitions`) and the export watermark renderer use CJK faces (picked by script) as fallbacks after Inter. Unset, + `CRAFT_FONTS` is empty: everything builds, tests and runs, but Chinese and Japanese text lack glyphs. Releases always build with it (`release.yml`, `CRAFT_FONTS_REQUIRED=1`) and ship the fonts' OFL licences. Tests that need these fonts skip without it; the FreeBSD CI job runs them with it. Rules: craftrules [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). diff --git a/CLAUDE.md b/CLAUDE.md index bdf7eceb..38f5bde4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,12 +5,18 @@ LightCraft is a clean-room, open-source, pure-Rust photo library + non-destructi ## Start every session here 1. Read `plan/STATUS.md` (current milestone, next unchecked task, blockers). 2. Read the task in `plan/execution-plan.md` §3, the relevant section of `plan/architecture.md`, and the README/docs of the crate you touch. Behaviour/visual reference: `plan/lightroom/` (incl. `10-observed-ui.md` + screenshots). -3. **Know where we stand:** read [`ROADMAP.md`](ROADMAP.md) → *Where we stand* (honest assessment by dimension and - by kind of user) and *Where we're going* (ordered priorities). The checklist counts features that *exist*; the real - gaps are quality and coverage: camera colour calibration, CR3 / compressed raws, per-model verification, render - fidelity against Lightroom, AI models, HDR / video / Classic modules. A ✅ row is not proof of parity — if you find a - ✅ feature that is wrong or incomplete, downgrade it to 🟡 with a note. -4. **Pick work** from [`docs/parity.md`](docs/parity.md) → *Top gaps* (ordered by user impact; the tracker has one row per feature, menu item and shortcut). When you land a feature, update its row(s) and the gap list in the same commit, and the *Where we stand* / *Where we're going* sections of `ROADMAP.md` when a listed gap closes; `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and path the tracker cites still exists, and `cargo xtask parity --write` refreshes its summary. +3. **Know where we stand:** read [`ROADMAP.md`](ROADMAP.md) (stage, headline numbers, every dimension) and, for + method and evidence, [`docs/target-app-parity.md`](docs/target-app-parity.md). The checklist counts features that + *exist*; the real gaps are quality and coverage: camera colour calibration, CR3 / compressed raws, per-model + verification, render fidelity against Lightroom, AI models, HDR / video / Classic modules (raw detail: + [`docs/raw-parity.md`](docs/raw-parity.md)). A ✅ row is not proof of parity — if you find a ✅ feature that is + wrong or incomplete, downgrade it to 🟡 with a note. +4. **Pick work** from [`docs/gaps.md`](docs/gaps.md) (ranked by user impact; `B` marks beta blockers). The row-by-row + tracker is [`docs/parity-checklist.md`](docs/parity-checklist.md) (one row per feature, menu item and shortcut). + When you land a feature, update its row(s) and the gap entry in the same commit, and the numbers in `ROADMAP.md` / + `docs/target-app-parity.md` when a listed gap closes (bump each doc's *Last updated* line and revision history, per + craftrules `standards/progress-docs.md`); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and + path the tracker cites still exists, and `cargo xtask parity --write` refreshes its summary. 5. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's. `plan/` is gitignored (local-only). @@ -55,6 +61,10 @@ of it. Full standard: `../craftrules/standards/never-crash.md` - **Pure Rust** in the product. No C/C++ dependencies. - **Layering** (`plan/architecture.md` §3, enforced by `cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd. - **Everything is a command** (`crates/engine`): id, label, menu path, shortcut, params, enabled(), run(). UI, CLI, control channel and MCP all dispatch by id. Every slider is a `develop` control spec. +- **Long-running work shows in the activity stack** ([`docs/background-tasks.md`](docs/background-tasks.md)): anything + that runs off the UI thread and can take more than a moment holds a `TaskGuard` from `session.activity.start(kind, + label, cancel)` for its whole run. It adopts the cancel flag the job already checks (`Cancel::Flag`), or uses + `Cancel::No` when the job can't stop. No progress window, panel or progress toast of its own; its tests assert the row. - **Resolution independence:** settings use normalized image coordinates and relative radii; previews and exports must match. - **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, assets, wasm). - **Commits:** one task id per commit (`M2.3: local Laplacian highlights/shadows`). Only green states. End messages with the attribution line required by the environment. @@ -74,9 +84,10 @@ of it. Full standard: `../craftrules/standards/never-crash.md` font files here (Inter, already in `assets/fonts/`, is the one exception); add new fonts to craft-fonts. LightCraft uses it as the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts` then `CRAFT_FONTS_DIR=../craft-fonts cargo run -p lightcraft` (or any cargo/xtask command). `crates/engine/build.rs` - embeds the manifest's fonts as `lightcraft_engine::CRAFT_FONTS` (wasm32: BIZ UDPGothic Regular only); the UI - (`theme::font_definitions`) and the export watermark renderer use its CJK faces (picked by script) as fallbacks after Inter. Unset, - `CRAFT_FONTS` is empty: everything builds, tests and runs, but Japanese text has no glyphs. Releases always build + embeds the manifest's fonts as `lightcraft_engine::CRAFT_FONTS` (wasm32: BIZ UDPGothic Regular only); `cargo xtask web` + ships Noto Sans CJK SC Regular as a separate asset loaded by the UI before startup. The UI + (`theme::font_definitions`) and the export watermark renderer use CJK faces (picked by script) as fallbacks after Inter. Unset, + `CRAFT_FONTS` is empty: everything builds, tests and runs, but Chinese and Japanese text lack glyphs. Releases always build with it (`release.yml`, `CRAFT_FONTS_REQUIRED=1`) and ship the fonts' OFL licences. Tests that need these fonts skip without it; the FreeBSD CI job runs them with it. Rules: craftrules [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). @@ -102,6 +113,9 @@ of it. Full standard: `../craftrules/standards/never-crash.md` the running app). See `docs/mcp.md`. Quick non-UI checks: `lightcraft-cli render in.jpg -o out.jpg --set light.exposure=1`. - Export goes through `lightcraft_engine::export` (one encoder for app, CLI, MCP and web); UI-only commands live in `crates/ui-egui/src/menus.rs`. +- Text boxes use `crates/ui-egui/src/text_field.rs` (`TextField`), not `egui::TextEdit`: it gives every field the + Cut / Copy / Paste / Select All menu, Esc restoring the text, and reports how an edit ended. Give each a widget id + that is unique on screen. Older fields still on `TextEdit` move over as they are touched. - Shell gotcha: `mv`/`cp` are aliased interactive here — use `/bin/mv -f` / `/bin/cp -f`. - Parallel agents: separate git worktrees and `CARGO_TARGET_DIR=target/agent-`; each agent uses its **own control port** (pick one in 18000–19999, never the default 7980) and its own scratch subfolder (`//`) — never `rm -rf` shared paths; delete your target dir when done (disk is shared); keep every `Cargo.toml` valid at all times (the `crates/*` glob means one broken manifest breaks everyone). - Test corpora: `cargo xtask corpus --download` into `corpus/` (gitignored, CC0 only). Never commit media. diff --git a/Cargo.lock b/Cargo.lock index 86d4bca6..0ad4ab84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,7 +209,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" dependencies = [ - "equator", + "equator 0.4.2", ] [[package]] @@ -967,6 +967,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "coe-rs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8f1e641542c07631228b1e0dc04b69ae3c1d58ef65d5691a439711d805c698" + [[package]] name = "color" version = "0.3.3" @@ -1256,6 +1262,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dyn-stack" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b" +dependencies = [ + "bytemuck", + "reborrow", +] + [[package]] name = "dyn-stack" version = "0.13.2" @@ -1537,13 +1553,33 @@ version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13" +[[package]] +name = "equator" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3b0a88aa91d0ad2b9684e4479aed31a17d3f9051bdbbc634bd2c01bc5a5eee8" +dependencies = [ + "equator-macro 0.1.9", +] + [[package]] name = "equator" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" dependencies = [ - "equator-macro", + "equator-macro 0.4.2", +] + +[[package]] +name = "equator-macro" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d08acb9849f7fb4401564f251be5a526829183a3645a90197dea8e786cf3ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1608,6 +1644,42 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "faer-core" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da0f6262445261106407bdfb548df4f8f1c89bcc3cff1bb6d08e7f525b575a" +dependencies = [ + "bytemuck", + "coe-rs", + "dyn-stack 0.10.0", + "equator 0.1.10", + "faer-entity", + "gemm 0.17.1", + "matrixcompare-core", + "num-complex", + "num-traits", + "paste", + "pulp 0.18.22", + "reborrow", + "seq-macro", +] + +[[package]] +name = "faer-entity" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fb12f36da3162b3fefc0c7343d905d5bd2c2bf444a7f63ceeaa2bc003311d3" +dependencies = [ + "bytemuck", + "coe-rs", + "libm", + "num-complex", + "num-traits", + "pulp 0.18.22", + "reborrow", +] + [[package]] name = "fastrand" version = "2.5.0" @@ -1783,13 +1855,33 @@ dependencies = [ "slab", ] +[[package]] +name = "gemm" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-c32 0.17.1", + "gemm-c64 0.17.1", + "gemm-common 0.17.1", + "gemm-f16 0.17.1", + "gemm-f32 0.17.1", + "gemm-f64 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + [[package]] name = "gemm" version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-c32 0.18.2", "gemm-c64 0.18.2", "gemm-common 0.18.2", @@ -1799,7 +1891,7 @@ dependencies = [ "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] @@ -1809,7 +1901,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-c32 0.19.0", "gemm-c64 0.19.0", "gemm-common 0.19.0", @@ -1819,7 +1911,22 @@ dependencies = [ "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", "seq-macro", ] @@ -1829,12 +1936,12 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] @@ -1844,12 +1951,27 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.19.0", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", "seq-macro", ] @@ -1859,12 +1981,12 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] @@ -1874,15 +1996,34 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.19.0", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] +[[package]] +name = "gemm-common" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" +dependencies = [ + "bytemuck", + "dyn-stack 0.10.0", + "half", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.18.22", + "raw-cpuid 10.7.0", + "seq-macro", + "sysctl 0.5.5", +] + [[package]] name = "gemm-common" version = "0.18.2" @@ -1890,7 +2031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" dependencies = [ "bytemuck", - "dyn-stack", + "dyn-stack 0.13.2", "half", "libm", "num-complex", @@ -1898,10 +2039,10 @@ dependencies = [ "once_cell", "paste", "pulp 0.21.5", - "raw-cpuid", + "raw-cpuid 11.6.0", "rayon", "seq-macro", - "sysctl", + "sysctl 0.6.0", ] [[package]] @@ -1911,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" dependencies = [ "bytemuck", - "dyn-stack", + "dyn-stack 0.13.2", "half", "libm", "num-complex", @@ -1919,10 +2060,27 @@ dependencies = [ "once_cell", "paste", "pulp 0.22.3", - "raw-cpuid", + "raw-cpuid 11.6.0", "rayon", "seq-macro", - "sysctl", + "sysctl 0.6.0", +] + +[[package]] +name = "gemm-f16" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "gemm-f32 0.17.1", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", ] [[package]] @@ -1931,14 +2089,14 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "gemm-f32 0.18.2", "half", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "rayon", "seq-macro", ] @@ -1949,30 +2107,45 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.19.0", "gemm-f32 0.19.0", "half", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "rayon", "seq-macro", ] +[[package]] +name = "gemm-f32" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + [[package]] name = "gemm-f32" version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] @@ -1982,12 +2155,27 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.19.0", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", "seq-macro", ] @@ -1997,12 +2185,12 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] @@ -2012,12 +2200,12 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.2", "gemm-common 0.19.0", "num-complex", "num-traits", "paste", - "raw-cpuid", + "raw-cpuid 11.6.0", "seq-macro", ] @@ -2296,6 +2484,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "heic-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf9dde634bb6d5b718bacd01bef80192f2334306b2f785519a0136cca7b9618" +dependencies = [ + "rayon", +] + [[package]] name = "hermit-abi" version = "0.5.3" @@ -2779,7 +2976,7 @@ dependencies = [ [[package]] name = "lightcraft" -version = "0.4.0" +version = "0.5.0" dependencies = [ "eframe", "egui", @@ -2797,10 +2994,11 @@ dependencies = [ [[package]] name = "lightcraft-catalog" -version = "0.4.0" +version = "0.5.0" dependencies = [ "crc32fast", "lightcraft-develop", + "lightcraft-geom", "lightcraft-meta", "log", "proptest", @@ -2812,7 +3010,7 @@ dependencies = [ [[package]] name = "lightcraft-cli" -version = "0.4.0" +version = "0.5.0" dependencies = [ "dhat", "lightcraft-codecs", @@ -2827,8 +3025,9 @@ dependencies = [ [[package]] name = "lightcraft-codecs" -version = "0.4.0" +version = "0.5.0" dependencies = [ + "avif-serialize", "bytemuck", "image", "image-webp", @@ -2837,10 +3036,12 @@ dependencies = [ "jxl-oxide", "lightcraft-color", "lightcraft-geom", + "lightcraft-heif", "lightcraft-raster", "moxcms 0.9.1", "png", "proptest", + "rav1e", "ravif", "rayon", "serde", @@ -2852,16 +3053,48 @@ dependencies = [ [[package]] name = "lightcraft-color" -version = "0.4.0" +version = "0.5.0" dependencies = [ "lightcraft-geom", "proptest", "serde", ] +[[package]] +name = "lightcraft-denoise" +version = "0.5.0" +dependencies = [ + "faer-core", + "lightcraft-denoise-core", + "lightcraft-raster", + "miniz_oxide 0.8.9", + "proptest", + "rayon", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.21", + "web-time", +] + +[[package]] +name = "lightcraft-denoise-core" +version = "0.5.0" +dependencies = [ + "lightcraft-raster", + "miniz_oxide 0.8.9", + "proptest", + "rayon", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.21", + "web-time", +] + [[package]] name = "lightcraft-develop" -version = "0.4.0" +version = "0.5.0" dependencies = [ "lightcraft-color", "lightcraft-geom", @@ -2873,7 +3106,7 @@ dependencies = [ [[package]] name = "lightcraft-engine" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ab_glyph", "harfrust", @@ -2881,9 +3114,14 @@ dependencies = [ "lightcraft-catalog", "lightcraft-codecs", "lightcraft-color", + "lightcraft-denoise", + "lightcraft-denoise-core", "lightcraft-develop", + "lightcraft-faces", + "lightcraft-fetch", "lightcraft-geom", "lightcraft-gpu", + "lightcraft-heif", "lightcraft-merge", "lightcraft-meta", "lightcraft-pipeline", @@ -2903,9 +3141,23 @@ dependencies = [ "web-time", ] +[[package]] +name = "lightcraft-faces" +version = "0.5.0" +dependencies = [ + "matrixmultiply", + "ndarray", + "proptest", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.21", + "web-time", +] + [[package]] name = "lightcraft-fetch" -version = "0.4.0" +version = "0.5.0" dependencies = [ "log", "rustls", @@ -2916,7 +3168,7 @@ dependencies = [ [[package]] name = "lightcraft-geom" -version = "0.4.0" +version = "0.5.0" dependencies = [ "proptest", "serde", @@ -2924,23 +3176,33 @@ dependencies = [ [[package]] name = "lightcraft-gpu" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytemuck", "lightcraft-color", + "lightcraft-denoise", "lightcraft-develop", "lightcraft-geom", "lightcraft-pipeline", "lightcraft-raster", + "lightcraft-raw", "lightcraft-scenes", "log", "pollster 1.0.1", "wgpu", ] +[[package]] +name = "lightcraft-heif" +version = "0.5.0" +dependencies = [ + "heic-rs", + "rayon", +] + [[package]] name = "lightcraft-mcp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "lightcraft-codecs", "lightcraft-engine", @@ -2951,7 +3213,7 @@ dependencies = [ [[package]] name = "lightcraft-merge" -version = "0.4.0" +version = "0.5.0" dependencies = [ "lightcraft-codecs", "lightcraft-color", @@ -2967,7 +3229,7 @@ dependencies = [ [[package]] name = "lightcraft-meta" -version = "0.4.0" +version = "0.5.0" dependencies = [ "lightcraft-geom", "lightcraft-tiff", @@ -2979,13 +3241,14 @@ dependencies = [ [[package]] name = "lightcraft-pipeline" -version = "0.4.0" +version = "0.5.0" dependencies = [ "image", "lightcraft-color", "lightcraft-develop", "lightcraft-geom", "lightcraft-raster", + "lightcraft-raw", "lightcraft-scenes", "proptest", "rayon", @@ -2995,7 +3258,7 @@ dependencies = [ [[package]] name = "lightcraft-preview" -version = "0.4.0" +version = "0.5.0" dependencies = [ "jpeg-encoder", "lightcraft-raster", @@ -3007,7 +3270,7 @@ dependencies = [ [[package]] name = "lightcraft-raster" -version = "0.4.0" +version = "0.5.0" dependencies = [ "libc", "lightcraft-color", @@ -3019,9 +3282,10 @@ dependencies = [ [[package]] name = "lightcraft-raw" -version = "0.4.0" +version = "0.5.0" dependencies = [ "jpeg-encoder", + "jxl-oxide", "lightcraft-color", "lightcraft-geom", "lightcraft-meta", @@ -3035,11 +3299,12 @@ dependencies = [ "thiserror 2.0.21", "zune-core", "zune-jpeg", + "zune-jpegxl", ] [[package]] name = "lightcraft-scenes" -version = "0.4.0" +version = "0.5.0" dependencies = [ "image", "lightcraft-color", @@ -3050,7 +3315,7 @@ dependencies = [ [[package]] name = "lightcraft-segment" -version = "0.4.0" +version = "0.5.0" dependencies = [ "candle-core", "candle-nn", @@ -3063,11 +3328,11 @@ dependencies = [ [[package]] name = "lightcraft-sysmem" -version = "0.4.0" +version = "0.5.0" [[package]] name = "lightcraft-tiff" -version = "0.4.0" +version = "0.5.0" dependencies = [ "proptest", "serde", @@ -3076,15 +3341,18 @@ dependencies = [ [[package]] name = "lightcraft-ui-egui" -version = "0.4.0" +version = "0.5.0" dependencies = [ "egui", "lightcraft-catalog", "lightcraft-codecs", "lightcraft-color", + "lightcraft-denoise", "lightcraft-develop", "lightcraft-engine", + "lightcraft-faces", "lightcraft-geom", + "lightcraft-merge", "lightcraft-meta", "lightcraft-pipeline", "lightcraft-preview", @@ -3098,7 +3366,7 @@ dependencies = [ [[package]] name = "lightcraft-web" -version = "0.4.0" +version = "0.5.0" dependencies = [ "crc32fast", "eframe", @@ -3174,6 +3442,22 @@ dependencies = [ "libc", ] +[[package]] +name = "matrixcompare-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0bdabb30db18805d5290b3da7ceaccbddba795620b86c02145d688e04900a73" + +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -3327,6 +3611,21 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "ndk" version = "0.9.0" @@ -4291,6 +4590,18 @@ dependencies = [ "unarray", ] +[[package]] +name = "pulp" +version = "0.18.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6" +dependencies = [ + "bytemuck", + "libm", + "num-complex", + "reborrow", +] + [[package]] name = "pulp" version = "0.21.5" @@ -4317,7 +4628,7 @@ dependencies = [ "num-complex", "paste", "pulp-wasm-simd-flag", - "raw-cpuid", + "raw-cpuid 11.6.0", "reborrow", "version_check", ] @@ -4518,6 +4829,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -4545,6 +4865,12 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.12.0" @@ -5245,6 +5571,20 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "sysctl" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "sysctl" version = "0.6.0" @@ -6486,11 +6826,12 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xtask" -version = "0.4.0" +version = "0.5.0" dependencies = [ "brotli", "flate2", "serde_json", + "sha2", ] [[package]] @@ -6752,6 +7093,15 @@ dependencies = [ "zune-core", ] +[[package]] +name = "zune-jpegxl" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba11ecd07cc23351500e840ae03841b7e4997923ec8e4832ee3ae199ea0b6b4" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.15.0" diff --git a/Cargo.toml b/Cargo.toml index 77b20666..58e5383b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/*", "apps/*", "xtask"] [workspace.package] -version = "0.4.0" +version = "0.5.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.90" @@ -11,7 +11,10 @@ repository = "https://github.com/storytold/lightcraft" [workspace.dependencies] # internal +lightcraft-denoise-core = { path = "crates/denoise-core" } +lightcraft-denoise = { path = "crates/denoise", default-features = false } lightcraft-geom = { path = "crates/geom" } +lightcraft-faces = { path = "crates/faces" } lightcraft-sysmem = { path = "crates/sysmem" } lightcraft-color = { path = "crates/color" } lightcraft-raster = { path = "crates/raster" } @@ -30,10 +33,12 @@ lightcraft-engine = { path = "crates/engine" } lightcraft-ui-egui = { path = "crates/ui-egui" } lightcraft-mcp = { path = "crates/mcp" } lightcraft-fetch = { path = "crates/fetch" } +lightcraft-heif = { path = "crates/heif" } lightcraft-segment = { path = "crates/segment" } # foundation thiserror = "2" +sha2 = "0.10" serde = { version = "1", features = ["derive", "rc"] } serde_json = { version = "1", features = ["float_roundtrip"] } log = "0.4" @@ -61,7 +66,11 @@ tiff = "0.11" image-webp = "0.2" moxcms = { version = "0.9", default-features = false, features = ["lut"] } jxl-oxide = { version = "0.12", default-features = false } +zune-jpegxl = { version = "0.5", default-features = false, features = ["std"] } # MIT/Apache-2.0/Zlib lossless JXL encoder (test fixtures only) ravif = { version = "0.13", default-features = false, features = ["threading"] } # no "asm": it needs nasm (non-Rust) +# HDR AVIF (PQ): ravif fixes the sRGB transfer curve, so HDR files drive its rav1e + avif-serialize directly +rav1e = { version = "0.8.1", default-features = false, features = ["threading"] } +avif-serialize = "0.8.6" # gpu (crates/gpu; native backends only, no GL: everything stays pure Rust) # SAM 3 inference (lightcraft-segment). 0.9.2: later releases pull `tokenizers` with the diff --git a/NOTICE b/NOTICE index f3f6499f..5fd60227 100644 --- a/NOTICE +++ b/NOTICE @@ -15,6 +15,13 @@ list (author, source, license) is in assets/ATTRIBUTION.md. Inter (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. Showcase photographs by Ansel Adams, Dorothea Lange and NASA (docs/images/) — public domain. See assets/ATTRIBUTION.md. +crates/raw/src/spectral_table.rs (lightcraft-raw) holds camera colour matrices derived from the +camera spectral sensitivities, training reflectances and model alias list of rawtoaces-data +(https://github.com/AcademySoftwareFoundation/rawtoaces-data, v1.1.0), an Academy Software +Foundation project licensed under the Apache License 2.0, and from the CIE 1931 colour-matching +functions it ships (CIE, CC-BY-4.0). The matrices were fitted by the LightCraft contributors +(2026) with tools/spectral_camera_table.py. See assets/ATTRIBUTION.md. + crates/segment (lightcraft-segment) ports the SAM 3 model code of Hugging Face Transformers (https://github.com/huggingface/transformers, src/transformers/models/sam3 and sam3_tracker), Copyright The HuggingFace Team and Meta Platforms, Inc., licensed under the Apache License 2.0. @@ -27,6 +34,15 @@ the facebook/sam3 checkpoint only when they choose to (in the app, or by hand), License (https://github.com/facebookresearch/sam3/blob/main/LICENSE), a separate, non-OSI licence. See docs/ai-masks.md. +AI denoise weights are not part of LightCraft and are never distributed with it. +The default build offers user-supplied models only. The optional rawnind-model +feature offers a separately downloaded RawNIND UtNet2 Bayer checkpoint under +GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html), after the user accepts its terms. +Commercial use is subject to those terms, including applicable source, licence and +notice obligations on redistribution. Enabling that offer in releases remains a +maintainer decision. The ONNX reader and CPU/GPU executor are our Rust implementation; +no darktable, RawSpeed, LibRaw or other GPL decoder/runtime code is included. + Builds made with craft-fonts (the optional CRAFT_FONTS_DIR build input; all official releases) also embed the fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (the Japanese fonts BIZ UDPGothic, BIZ UDMincho and Shippori Mincho; the web build embeds only BIZ diff --git a/README.md b/README.md index 5aacbee0..d395a104 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,20 @@ lightcraft --control 7980 ~/Pictures/trip path=out.jpg longEdge=2048` runs any chain of commands (headless, on a saved library, or against the running app) and prints one JSON result per command; `lightcraft-cli render in.dng -o out.jpg --set light.exposure=0.7 --preset …`. - **Undo for everything**, including agent actions: a slider drag (or a scripted burst of updates) is one undo step. +- **Contact sheets (native app / CLI):** select photos, then **File → Contact Sheet PDF…**. Choose A4 or Letter, landscape, + rows/columns and filename captions; save a paginated PDF and print it from your PDF viewer. Exports use the + current edits, fit each photo without further cropping, and render at 150 dpi in sRGB. The background export + supports cancellation and protects originals and XMP sidecars. From the CLI: + + ```sh + lightcraft-cli run --import ~/Pictures/shoot library.selectAll export.contactSheet path=Contact.pdf paper=a4 columns=3 rows=4 captions=true + ``` + + `export.contactSheet` also accepts explicit `ids`, `landscape=true`, and `paper=letter`. It replaces an existing + output only after the entire PDF succeeds. Limits: 1,000 photos, 100 pages, 256 MiB; captions are rasterized + with the same font coverage as watermarks (CJK requires craft-fonts), with long names shortened in the middle. + This is a contact-sheet workflow, + not a printer-driver or custom picture-package layout editor. - **Every widget is addressable** (`ui.widgets`) and clickable by name, so agents operate the real UI, not a side door. - The screenshots in this README were produced end to end by the [`docs/showcase/`](docs/showcase/) scripts. @@ -260,7 +274,8 @@ lightcraft --control 7980 ~/Pictures/trip - **Pure Rust, no C.** Our own RAW decoders (DNG, Canon CR2/CR3, Sony ARW, Nikon NEF, Fujifilm RAF incl. X-Trans, Panasonic RW2 / Leica RWL, Pentax PEF, Olympus ORF), our own colour science, our own pipeline. JPEG, PNG, TIFF, WebP, - PSD composites and JPEG XL open today. + PSD composites and JPEG XL open today; HEIC/HEIF photos too with `--features heif` (HEVC is a build-time choice, + as in PhotoCraft). - **Scene-referred & wide-gamut.** Linear Rec.2020 float internally, Bradford-adapted white balance, gamut mapping instead of clipping, a filmic shoulder for raw and pixel-exact pass-through for JPEGs you haven't touched. - **Resolution-independent edits.** Radii and brush sizes are relative to the image, so a 400 px preview, your @@ -268,7 +283,8 @@ lightcraft --control 7980 ~/Pictures/trip - **GPU-accelerated, CPU-exact.** The whole develop pipeline runs as wgpu compute kernels (Metal / Vulkan / DX12), checked against the CPU pipeline to within 1/255. On a 24 MP raw (Apple M4 Pro): a slider update re-renders in ~4 ms, a cold 2.5 MP loupe in ~30 ms, and a full-size export in ~0.3 s including a parallel JPEG encode. - Without a GPU the same pipeline runs on all CPU cores, redoing only the stages a slider affects. + Without a GPU the same pipeline runs on all CPU cores, redoing only the stages a slider affects. Batch exports + render several photos side by side, so one's decode and encode overlap another's render. - **Instant culling.** Opening a raw shows its embedded camera preview or cached render within ~0.1 s while the full render follows (~0.2–0.5 s for 24 MP). The next and previous photos are prepared in the background, so stepping through a shoot takes ~50 ms per photo. @@ -281,18 +297,20 @@ lightcraft --control 7980 ~/Pictures/trip ## Feature status -LightCraft is young and moving fast. **Where we honestly stand** (details in the [roadmap](ROADMAP.md#where-we-stand)): +LightCraft is young and moving fast: **alpha**. **Where we honestly stand** (details in the [roadmap](ROADMAP.md) and +[docs/target-app-parity.md](docs/target-app-parity.md)): -- **By feature count we're at ~79%** of Lightroom (core features 98%), tracked row by row in - [docs/parity.md](docs/parity.md). -- **As a day-to-day Lightroom replacement we're nearer 60–70%.** It's great for JPEG/DNG and most Nikon / Sony / - older-Canon raws on one machine. +- **By feature count we're at ~81%** of Lightroom (core features 98%), tracked row by row in + [docs/parity-checklist.md](docs/parity-checklist.md). +- **As a day-to-day Lightroom replacement we're nearer 61% for the whole product, ~49% for a typical working photographer and ~64% for + casual use.** It's great for JPEG/DNG and most Sony / Panasonic / + Fujifilm / Nikon / older-Canon raws on one machine; see [docs/raw-parity.md](docs/raw-parity.md) for your camera. - **The biggest gaps:** - - **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 raws have guarded estimates from their camera JPEGs, with built-in ILCE-7M4, X-H2S and X-T4 profiles; measured calibration is missing, and other raws or rejected fits retain a neutral matrix; + - **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 raws have guarded estimates from their camera JPEGs, with built-in ILCE-7CR, ILCE-7M4, X-H2S and X-T4 profiles; measured calibration is missing, and other raws or rejected fits retain a neutral matrix; - **compressed Olympus raws and unsupported CR3 variants:** these use embedded JPEG previews when present. Fujifilm lossless/lossy compressed RAF now decodes sensor data; [verification and existing-library reload instructions](docs/raf-compression.md); - **AI masks and denoise:** subject and sky selection are classical heuristics; - - **HDR, video and the Classic Print / Book / Map modules.** -- **What's next:** see [where we're going](ROADMAP.md#where-were-going). + - **on-screen HDR display, video and the Classic Print / Book / Map modules.** +- **What's next:** see [the ranked gaps](docs/gaps.md) and [the roadmap](docs/roadmap.md). | Area | Status | |---|---| @@ -302,12 +320,13 @@ LightCraft is young and moving fast. **Where we honestly stand** (details in the | Masking: brush, linear, radial, luminance/colour range, add/subtract/intersect | ✅ (AI subject/sky use classical heuristics for now) | | Crop, straighten tool + auto straighten, flip, rotate, aspect ratios, overlays | ✅ | | Profiles (Color, Neutral, Vivid, Landscape, Portrait, Monochrome: our own looks), presets, versions, history, copy/paste/sync settings | ✅ | -| Camera colour: DNG files use their own matrices | ✅ DNG · 🟡 own Sony/Fujifilm profiles; measured calibration database missing | +| Camera colour: DNG files use their own matrices | ✅ DNG · 🚧 own Sony/Fujifilm profiles; measured calibration database missing | | Native macOS menu bar (generated from the command registry), control channel + every widget addressable, headless UI snapshots | ✅ | -| RAW: DNG, CR2, CR3 (lossless CRX Bayer and version 0x100/0x200 C-RAW), ARW, NEF (uncompressed + lossless/lossy compressed), Fujifilm RAF (uncompressed + lossless/lossy compressed, Bayer + X-Trans), Panasonic RW2 / Leica RWL / Panasonic RAW (every raw format, DMC-LX1 to DC-S1RM2), Pentax PEF, Olympus ORF (uncompressed); embedded previews for every format incl. CR3 | 🟡 · CR3, compressed ORF decode ⬜ | -| Detail: sharpening, luminance + colour noise reduction | ✅ · AI Denoise, Super Resolution ⬜ | +| RAW: DNG, CR2, CR3 (lossless CRX Bayer and version 0x100/0x200 C-RAW), ARW, NEF (uncompressed + lossless/lossy compressed, incl. lossy after split), Fujifilm RAF (uncompressed + lossless/lossy compressed, Bayer + X-Trans), Panasonic RW2 / Leica RWL / Panasonic RAW (every raw format, DMC-LX1 to DC-S1RM2), Pentax PEF, Olympus ORF (uncompressed, 12.8 bpp), Samsung SRW (uncompressed); embedded previews for every format incl. CR3 | 🚧 · other CR3 variants, compressed ORF / SRW and Nikon HE NEF decode ⬜ ([raw parity](docs/raw-parity.md)) | +| Detail: sharpening, luminance + colour noise reduction | ✅ · AI Denoise 🚧 (Bayer, user-installed weights), Super Resolution ⬜ | | Remove / Heal / Clone spots (auto source), Visualize Spots, Red Eye and Pet Eye (auto pupil detection, catchlight) | ✅ · content-aware fill, spot pin editing 🚧 | -| Export: JPEG / PNG / TIFF / WebP / AVIF / DNG / original, sizing, file-size limit, output sharpening, naming templates, batch, metadata policy, text or image watermark | ✅ · HDR export ⬜ | +| Export: JPEG / PNG / TIFF / WebP / AVIF / DNG / original, sizing, file-size limit, output sharpening, naming templates, batch, metadata policy, text or image watermark | ✅ | +| HDR: HDR editing with a headroom limit, SDR rendition, Visualize HDR; export as ISO 21496-1 gain map JPEG, PQ AVIF or 32-bit float TIFF | ✅ · HDR display ⬜ | | Library persistence (crash-safe op log + snapshots, background compaction, failed saves reported), disk thumbnail cache | ✅ | | Import: Add in place / Copy / Move, rename and folder templates, devices, duplicate detection, watched folders; Local folder browsing | ✅ | | MCP server (headless or live app, persistent libraries), CLI, control channel | ✅ | @@ -315,7 +334,7 @@ LightCraft is young and moving fast. **Where we honestly stand** (details in the | Optics (distortion, vignetting, auto + manual CA, defringe, lens corrections embedded in DNG files and Panasonic / Leica RW2 / RWL distortion data), Geometry (transforms, Constrain Crop), Upright (Auto/Level/Vertical/Full/Guided) | ✅ · camera lens profiles (our own) ⬜ | | Photo Merge: HDR (auto-align, deghost), Panorama (spherical/cylindrical/perspective, boundary warp, auto crop), HDR Panorama → DNG | ✅ | | GPU pipeline (wgpu compute, CPU-exact within 1/255), CPU fallback on device limits / errors | ✅ · WebGPU in the browser 🚧 | -| AI: segmentation masks, AI denoise, super resolution, faces; HDR editing; video | ⬜ (see [roadmap](ROADMAP.md#where-were-going)) | +| AI: segmentation masks, AI denoise, super resolution, faces; video | 🚧 (see [gaps](docs/gaps.md)) | | Web build (same UI in the browser via WASM): persistent library in OPFS/IndexedDB, Web Worker rendering, export downloads | ✅ · WebGPU, Safari/Firefox testing 🚧 | ✅ works today · 🚧 in progress · ⬜ not started @@ -353,7 +372,7 @@ CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p lightcraft Without it LightCraft builds and runs the same, but Chinese and Japanese text have no glyphs. Fonts are never committed to this repo; see [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). -**Interface language:** **Edit → Language** (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Deutsch) or **Settings → General → +**Interface language:** **Edit → Language** (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Español, Deutsch, Русский, Français) or **Settings → General → Language**; the choice applies immediately and persists. See [docs/localization.md](docs/localization.md). **Logs:** the desktop app writes its log to standard error and to `logs/lightcraft.log` in its settings folder @@ -443,7 +462,9 @@ Questions, ideas or a bug you'd like to talk through first? Bring them to [Disco | arm64 (Snapdragon and other ARM PCs) | `lightcraft--windows-arm64.msi` | `lightcraft--windows-arm64-portable.zip` | | x86 (32-bit) | `lightcraft--windows-x86.msi` | `lightcraft--windows-x86-portable.zip` | -Installers and executables are code-signed. +Installers and executables are code-signed. The installer asks where to install (`C:\Program Files\LightCraft` by +default; upgrades stay in the folder you chose) and ends on a page confirming LightCraft was installed, with an +option to start it. For unattended installs: `msiexec /i lightcraft--windows-x64.msi /qn INSTALLFOLDER="D:\Apps\LightCraft\"`. ### macOS diff --git a/ROADMAP.md b/ROADMAP.md index 5e793e8b..c1cfbcf5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,234 +1,166 @@ # LightCraft roadmap -Milestones toward full Adobe Lightroom parity (cloud Lightroom first, then every Lightroom Classic module), with wall-clock -estimates for continuous (24/7) agent-driven development with 4–6 parallel agents. Estimates are calibrated on the sibling -projects (DrawCraft reached its first four milestones in ≈ 4½ h) and are revised as milestones land. +**Stage: alpha** · next: beta, ~14 points and ~340–600 h away -## Where we stand +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Lightroom Classic 15.5 and the installed Lightroom 9.6; restructured to the progress-docs standard) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 -*Honest assessment, 2026-10-08. Agents: read this before picking work. The checklist in -[`docs/parity.md`](docs/parity.md) counts features that **exist**; this section is about whether a photographer can -**switch** from Lightroom. Update it when a gap below closes.* +One-page summary of how close LightCraft is to Adobe Lightroom. Agents: read this, then pick work +from [docs/gaps.md](docs/gaps.md). The full assessment, method and evidence are in +[docs/target-app-parity.md](docs/target-app-parity.md). -**In one line:** the checklist says **79.7%** (P0 98.2%, P1 95.6%, P2 41.6%), but measured by whether a working -photographer could replace Lightroom without noticing, we are at roughly **60–70%**. The remaining gap is mostly -**quality of results and camera coverage**, not missing buttons. +## Headline numbers -Caveat on the checklist: ✅ is set by whoever lands a feature, and nobody has systematically checked rows against -Lightroom's behaviour or output. Bugs keep turning up in ✅ areas (CR2 colour-filter phase on some Canon models #85, -duplicate Local entries #22, black GPU exports on an Intel iGPU #78). - -### By dimension - -| Dimension | Estimate | What's true today | Biggest gaps | -|---|---:|---|---| -| **Feature checklist** | 79.7% | P0 core and P1 nearly complete: import (Add / Copy / Move, templates, devices), library, grid/loupe/compare/survey, every Edit slider, curves, colour grading, masking tools, crop/Upright, heal/clone, presets/profiles, versions/history, sync, export, menus, shortcuts | P0: full-resolution zoom detail; P1: lens-profile database, content-aware fill (patch synthesis), video playback/trim | -| **RAW coverage** (formats people shoot) | ~55% | DNG (all kinds), CR2, CR3 lossless Bayer and version 0x100/0x200 C-RAW (M50/R100/R8 pixel-exact corpus), ARW, NEF (uncompressed + Huffman lossless/lossy), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans), uncompressed ORF, RW2 / RWL / Panasonic RAW (every raw format, checked on 178 files from 118 Panasonic and Leica bodies), PEF; every container's embedded preview (incl. CR3) | **CR3** (every Canon since ~2018), compressed ORF, Nikon lossy-after-split, Canon sRAW, HEIC/AVIF. NEFs labelled compressed but stored uncompressed (Z 6 packed 14-bit, D850 12-bit uncompressed). Per-model verification is thin (~55 corpus files vs >1,000 models) | -| **Colour & image quality** | ~55–65% | Pipeline is complete and fast; GPU path CPU-exact within 1/255 | **No measured camera calibration database**: ARW, NEF, RW2, RAF, CR3, CR2 and PEF have guarded camera-JPEG colour estimates, with bundled ILCE-7M4, X-H2S and X-T4 profiles (docs/camera-preview-colour.md); other non-DNG raws and rejected estimates use a neutral matrix. Colour fidelity remains incomplete. No lens-profile database. No measured fidelity against Lightroom (tone, highlights, texture/clarity, NR, sharpening are tuned by eye) | -| **AI & computational** | ~15–20% | Assisted culling (focus, bursts), auto tone, HDR/panorama merge; subject/sky/background masks as classical heuristics | Real segmentation masks (subject, sky, people, objects, landscape, depth), AI denoise, super resolution, lens blur, generative remove, faces/people, natural-language search. **Blocked on a model strategy** (licensable weights or our own training; pure-Rust inference is feasible) | -| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering | -| **Classic modules** | ~30% | Geotagging from GPX track logs, soft proofing (partial), slideshow (basic) | **Map view, Book, Print, Slideshow module, Web, publish services**: ~40 tracker rows ⬜ | -| **HDR & video** | 0% | | HDR edit/display/export; video play/trim/edit/export | -| **Platform & robustness** | ~70% | macOS native; Windows/Linux builds; web via WASM; no-panic lints workspace-wide, `unsafe` confined to `crates/sysmem`; failed saves are reported; GPU errors fall back to CPU | Windows installer UI unverified on Windows (PR #79); GPU path proven only on Apple + user reports; English, Simplified/Traditional Chinese, Japanese, Brazilian Portuguese and German UI (see docs/localization.md); remaining technical errors; Traditional Chinese uses the Simplified Chinese font until craft-fonts has a TC face; accessibility partial; headless UI tests time out under machine load | - -### By kind of user - -| User | Readiness | What blocks them | +| | Value | Kind | |---|---:|---| -| JPEG / DNG shooter, single machine | ~85% | Fidelity polish, AI masks | -| Nikon / Sony / Panasonic / older-Canon raw shooter | ~65% | Camera colour fidelity and coverage (ARW, NEF and RW2 preview estimates are only a starting point); Panasonic compacts and kit zooms also miss their embedded distortion correction | -| Fujifilm raw shooter | ~65% | Uncompressed and lossless/lossy compressed RAF decoded; camera colour calibration remains missing. Verified on 13 bodies, including 14/16-bit GFX; see `docs/raf-compression.md` | -| Canon CR3 / Olympus compressed-raw shooter | ~45% | CR3 lossless Bayer and version 0x100/0x200 C-RAW develop from sensor data (M50, R100, R8 verified pixel for pixel); other CRX variants and compressed ORF open as embedded JPEG previews. Camera colour and broader model verification remain | -| Lightroom Classic power user | ~45% | Print, Book, Map, publish, tethering | -| Relies on AI (masks, denoise) | ~25% | Object / Describe masks via optional SAM 3 (download not yet hosted); no AI denoise | +| **Feature breadth** (checklist) | **81.1%** of 517 rows (P0 98.0%, P1 95.1%, P2 46.9%) | measured: [parity-checklist.md](docs/parity-checklist.md) recount | +| **Feature breadth** (weighted by use) | **~77%** | estimated | +| **Ready for real work** (full target) | **~61%** (56–66%) | estimated: weighted sum over the dimensions | +| **Mainstream practitioner** | **~49%** (44–54%) | estimated | +| **Essentials user** | **~64%** (59–69%) | estimated | +| Remaining to **beta** | **~340–600 h** (Opus 5.5 agent hours, ~60% parallel) | estimated | +| Remaining to **full parity** | **~1,000–1,800 h** (~70% parallel) | estimated | -## Where we're going +Each audience with its own hours (Opus 5.5 agent wall-clock hours to ~95%): -Priorities, in order. Each points at tracker rows in [`docs/parity.md`](docs/parity.md) → *Top gaps*. +| Audience | Ready | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | +|---|---:|---:|---| +| Full target (ready for real work) | **~61%** | **1,000–1,800 h** (~70% parallelizes) | raw colour, lens and camera data; AI models; Classic output modules; localization; plug-in ecosystem | +| Mainstream practitioner | **~49%** | **450–840 h** (~60% parallelizes) | camera colour and render fidelity; CR3 / ORF / HE NEF; lens database; subject / sky masks; Lightroom XMP exchange; stability | +| Essentials user | **~64%** | **160–300 h** (~50% parallelizes) | default raw look for popular cameras; launch stability; basic-slider feel; opening CR3 / HEIC files people send | -1. **Camera colour calibration of our own** (LR-PROF-CAMERACOLOR, P0): fit each camera to its own embedded JPEG, use - matrices the files carry themselves, then chart shots. Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3/CR2 - and Pentax PEF have the file-local fit (matrix + tone curve from their own JPEG; CR2 and PEF since issue #310, not yet - checked on the corpus); then ORF, then validate fidelity. -2. **Raw formats, clean-room** (LR-IMP-FORMATS, P0): Remaining **unverified CR3 CRX** variants, compressed ORF, NEF - lossy-after-split, sRAW. Decided 2026-10-05: write our own decoders from prose descriptions (never decoder source, - no LGPL dependency); compressed NEF (#86) is the template. -3. **Verified camera coverage** (LR-IMP-CAMERA-COVERAGE, P0): a CC0 sample per model in the corpus, each decoded and - checked for plausible colour; fix per-model bugs (#85). -4. **Render fidelity suite** (LR-BEHAV-RENDER-FIDELITY, P1): measure our output against Lightroom on the same CC0 raws - (references stay in the local `plan/`), then tune against the numbers. -5. **Lens profiles of our own** (LR-EDIT-OPTICS-PROFILE, P1). -6. **AI model strategy** (maintainer decision): which permissively licensed models (or our own training) for - segmentation masks and denoise; then pure-Rust inference. Unblocks M12 and Enhance. Object / Describe masks now - run Meta's SAM 3 in pure Rust (optional, user-consented download under the non-OSI SAM License; CDN mirrors still - to be set up, docs/ai-masks.md); Subject / Sky / People and Enhance are still open. -7. **Then:** HDR (Q), the Classic output modules (Print first, then Map view, Book, Slideshow), video (R), localisation - and accessibility. +The gap between breadth and readiness is quality and coverage, not missing buttons: camera colour is +estimated rather than measured, CR3 decodes on three verified Canon bodies, compressed ORF and HE NEF +open as previews, rendering is tuned by eye, AI masks are heuristics, there is no lens database, and +tests run in CI on FreeBSD only. **Why alpha:** core workflows run end to end, but readiness is below +the ~75% beta bar (it is above the ~40% alpha bar) and Lightroom's main inputs, raw files, still have blocking gaps (Canon, ~30% of +raw users, decodes CR3 on three verified bodies). It passes the alpha gate: all six core workflows +(import, cull, develop, save and reopen, batch looks, export) work end to end on macOS +([docs/roadmap.md](docs/roadmap.md#alpha-gate)). The three readiness numbers and their weights and discounts are in +[target-app-parity.md](docs/target-app-parity.md#ready-for-real-work-mainstream-practitioner-and-essentials-user). +Hours are calibrated on this repo's PR history; see +[target-app-parity.md](docs/target-app-parity.md#how-hours-are-calibrated). -Already closed in the week of 2026-10-03: compressed NEF (#10), Move import (#29), per-library smart-preview folder, -Local roots / cleanup, grid and catalog performance at 85k photos (#35, #37), failed-save reporting, GPU export -hardening (#78), copyright metadata (#51), GPX geotagging (#60), import tag help and folder templates (#31, #32). +## By dimension -Image navigation now supports native pinch zoom and two-finger pan in Detail, Compare and Reference views. -The whole-image preview still has a configurable size cap (2,560 px default), so 100% on larger photos does not -yet guarantee original-pixel detail; visible-region rendering remains a gap (LR-VIEW-ZOOM). +| Dimension | Ready | Breadth | Remaining | Doc | +|---|---:|---:|---:|---| +| Features | ~61% | ~77% (checklist 81.1%) | 600–1,100 h | [target-app-parity](docs/target-app-parity.md#features) | +| ↳ RAW cameras, colour, lens profiles | ~56% | 81% of 1,446 Adobe models decode, 16% verified | 255–480 h | [raw-parity](docs/raw-parity.md) | +| UI/UX fidelity | ~70% | ~85% | 40–70 h | [ui-parity](docs/ui-parity.md) | +| File formats (non-raw) | ~70% | ~85% | 30–55 h | [file-format-parity](docs/file-format-parity.md) | +| Hardware | ~40% | ~50% | 40–80 h | [hardware-parity](docs/hardware-parity.md) | +| Localization | ~52% of the 12 key languages | 7 of 12 present | 130–210 h (+145–255 h new scripts) | [localization-parity](docs/localization-parity.md) | +| Performance | ~70% | ~80% | 30–50 h | [target-app-parity](docs/target-app-parity.md#by-dimension) | +| Stability | ~60% | — | 30–60 h | [target-app-parity](docs/target-app-parity.md#by-dimension) | +| Platforms | ~60% | macOS, Windows, Linux, FreeBSD, web | 25–45 h | [target-app-parity](docs/target-app-parity.md#by-dimension) | +| Ecosystem / plug-ins | ~15% | ~25% | 40–80 h | [target-app-parity](docs/target-app-parity.md#by-dimension) | +| AI features | ~20% | ~30% | 120–220 h (overlaps features) | [target-app-parity](docs/target-app-parity.md#ai) | -## Milestones +## Features -**Status legend:** ✅ done · 🚧 in progress · ⬜ not started - -| # | Milestone | Scope (summary) | Estimate (h) | Status | -|---|---|---|---|---| -| M0 | Skeleton + visual shell | workspace, xtask CI + layering, geom/color/raster, develop model, pipeline v0, catalog v0, engine commands, Lightroom-look UI (grid, loupe, filmstrip, Edit panel), control channel, MCP, web build | 3–5 | ✅ | -| M1 | Library core | import (JPEG/PNG/TIFF/WebP), EXIF/XMP, persistent catalog (op log + snapshots), albums, ratings/flags/labels, filter/search/sort, thumbnail cache, 100k-photo grid | 6–10 | ✅ (100k-photo grid scale test pending) | -| M2 | Pipeline v1 (quality) | WB temp/tint, profiles, local tone mapping (highlights/shadows), curves, HSL, point colour, colour grading, texture/clarity/dehaze, vignette, grain, B&W, auto tone/WB, histogram, before/after | 10–15 | ✅ (look tuning vs our references ongoing) | -| M3 | RAW I | TIFF/DNG (LJ92, deflate, tiles, opcodes), demosaic (AHD/PPG/bilinear), highlight recovery, DNG colour model, CR2, NEF, ARW, embedded previews | 10–15 | 🚧 (DNG, CR2, ARW, NEF uncompressed + Huffman lossless/lossy, embedded previews ✅; NEF lossy-after-split ⬜) | -| M4 | Crop, geometry, optics | crop tool + overlays, straighten, Upright (auto/level/vertical/full/guided), manual transforms, CA, defringe, manual lens corrections | 6–10 | ✅ | -| M5 | Performance | source pyramids, wgpu compute pipeline (CPU oracle), draft/full renders, prefetch, budgets (16 ms slider updates on 24 MP) | 10–15 | 🚧 (stage cache, source pyramid, wgpu pipeline, prefetch, memory budget ✅; colour NR at half resolution, GPU histogram ⬜) | -| M6 | Masking | brush, linear/radial gradients, colour/luminance/depth range, add/subtract/intersect/invert, all local adjustments, masks panel | 8–12 | 🚧 (brush/linear/radial/colour/luminance range, add/subtract/intersect, masks panel ✅; depth range, AI masks ⬜) | -| M7 | Detail | sharpening + masking preview, luminance/colour NR, Denoise, Raw Details, Super Resolution | 6–10 | 🚧 (sharpening, luminance/colour NR ✅; AI Denoise, Raw Details, Super Resolution ⬜) | -| M8 | Heal / Remove | content-aware remove (PatchMatch), heal, clone, brush spots, visualize spots, red/pet eye | 6–10 | 🚧 (heal, clone, auto source, visualize spots, red/pet eye ✅; PatchMatch remove ⬜) | -| M9 | Presets, profiles, versions, sync | preset browser + amount, create/import presets, profile browser, versions, history, copy/paste/sync settings | 5–8 | ✅ | -| M10 | Export & share | export dialog (JPEG/PNG/TIFF/DNG/AVIF/JXL/original), sizing, sharpening, metadata, watermark, naming, batch jobs, XMP sidecars, HDR export | 6–10 | 🚧 (all formats incl. DNG/original, sizing, presets, background jobs ✅; JXL encode, HDR export ⬜) | -| M11 | RAW II | CR3, RAF (X-Trans), ORF, RW2, PEF, SRW, 3FR, IIQ + long tail; camera calibration DB; HEIC/AVIF/JXL import | 20–35 | 🚧 (CR3 lossless CRX and C-RAW, RAF uncompressed and lossless/lossy compressed, RW2 (every raw format), PEF, ORF uncompressed ✅; **camera colour calibration** 🚧 (guarded ARW, NEF, RW2 and RAF preview fitting; measured database still missing), CR3, compressed ORF ⬜) | -| M12 | AI & smart features | subject/sky/background/people/object masks, semantic search, faces/People (permissively licensed models, pure-Rust inference) | 20–40 | ⬜ | -| M13 | Merge | HDR merge (deghost), panorama (projections, boundary warp, fill edges), HDR panorama | 10–15 | ✅ | -| M14 | Video | import/playback/trim via FilmCraft crates, global edits + presets on video, video export | 6–10 | ⬜ | -| M15 | Classic modules | Map, Book, Slideshow, Print, Web; smart collections, stacks, virtual copies, publish services, tethering | 25–40 | 🚧 (smart albums, stacks, virtual copies, compare/survey ✅; Map/Book/Slideshow/Print/Web ⬜) | -| M16 | 1.0 polish | preferences, shortcut editor, accessibility, localization, packaging (dmg/msi/AppImage/web), hardening | 10–20 | 🚧 (settings, keyboard shortcuts sheet, packaging basics ✅; English/Chinese (Simplified, Traditional)/Japanese/Brazilian Portuguese/German localisation 🟡; accessibility and further locales ⬜) | - -## Parity estimate (feature count updated 2026-10-08; effort estimate from 2026-10-02) - -**By feature count** — from `docs/parity.md` (one row per Lightroom feature, menu item and shortcut; `cargo xtask -parity` prints this line on every run, so it stays current): - -| Scope | Weighted completion | Rows | 2026-10-02 | +| Area | Ready | Breadth | Remaining | |---|---:|---:|---:| -| P0 (core) | **98.2%** | 200 | 98.7% | -| P1 (important parity) | **95.6%** | 149 | 94.4% | -| P2 (later / AI / niche, incl. Classic modules) | **41.6%** | 160 | 29.4% | -| **All in-scope rows** | **79.7%** | 509 | 75.6% | +| Import and library | 80% | 90% | 25–45 h | +| RAW decoding and camera coverage | 76% | 81% | 120–220 h | +| Colour, camera profiles, render fidelity | 35% | 65% | 80–150 h | +| Global develop adjustments | 70% | 93% | 30–60 h | +| Optics, lens profiles, crop and geometry | 50% | 80% | 40–80 h | +| Masking | 45% | 67% | 60–110 h | +| Remove, heal, Enhance | 30% | 45% | 70–130 h | +| Presets, profiles, versions, sync | 85% | 95% | 8–15 h | +| Export | 75% | 89% | 15–30 h | +| Merge and HDR | 60% | 85% | 20–35 h | +| Views, compare, zoom, culling | 70% | 90% | 15–30 h | +| Classic output modules (Map, Book, Slideshow, Print, publish, tethering) | 10% | 15% | 90–150 h | +| Video | 0% | 0% | 25–45 h | -The 2026-10-05 count includes three new 🟡 rows that make quality gaps visible (camera colour calibration, verified -camera coverage, render fidelity), which is why P0 dipped slightly. +Weights and evidence: [target-app-parity.md](docs/target-app-parity.md#features). -✅ counts 1, 🟡 ½, ⬜ 0; out-of-scope rows (cloud sharing, Adobe accounts…) are left out. +## Languages -**By remaining effort** — rows are not equal: a shortcut and the whole Book module are one row each, and what is -left is the heavy part (AI, video, Classic output modules, undocumented raw codecs). Remaining work in **Opus agent-hours** -(one agent working continuously; calibrated on this project — a single lead agent closed ≈ 45 tracker rows of -UI/feature work in ≈ 5 h on 2026-10-01, and ≈ 60 more (preset import incl. `.lrtemplate` / DNG / zip and masks, smart-album -rule editor, auto sync, keyword and label sets, import options and DNG conversion, smart previews, external-editor round -trip, slideshow, auto import…) in ≈ 12 h on 2026-10-02; four to six parallel agents built M0–M13 in ≈ 25 active hours): +Measured from `crates/ui-egui/locales` (share of 2,304 catalog messages translated); none is `full` +while lower-layer errors stay English. Detail: [localization-parity.md](docs/localization-parity.md). -| Work package | Tracker rows | Agent-hours | Risk | +| Language | Code | Translated | Status | |---|---|---:|---| -| Remaining P0/P1 UI and library features (folder rename/move, keyword painter, people view…) | ≈ 8 | 5–10 | low | -| Raw codecs: CR3 (CRX), compressed ORF, NEF lossy-after-split, HEIC/AVIF decode, JPEG XL DNG | LR-IMP-FORMATS | 40–80 | **high** — clean-room black-box analysis, no permissive specs | -| Lens-profile database of our own (calibration targets, fitting, data) | LR-EDIT-OPTICS-PROFILE | 15–30 | data collection | -| Video: playback, trim, edits, export (pure-Rust decode, ideally shared with FilmCraft) | R. Video | 20–40 | medium | -| AI: subject / sky / background / people / object masks, object-aware remove, AI denoise, super resolution, lens blur, people & faces, natural-language search, culling | ≈ 30 | 80–150 | **high** — permissively licensed weights, pure-Rust inference, maybe training | -| HDR editing, display, visualisation and export | Q. HDR, LR-EXP-HDR | 15–25 | medium | -| Classic modules: Map, Book, Slideshow module, Print, publish services, tethering, soft proofing | ≈ 50 | 60–100 | medium (large, well-understood) | -| Smaller P2 items: Enhance dialog, export to Photos, help / what's new, accessibility, localisation, sidecar variants | ≈ 15 | 12–25 | low | -| Look tuning, performance budgets (colour NR at half resolution on CPU + GPU, 100k-photo library), packaging, hardening | — | 25–45 | medium | -| **Total remaining** | | **≈ 270–505** | | +| English | en | 100% | full (source) | +| Simplified Chinese | zh-hans | 87% | partial | +| Spanish | es | 86% | partial | +| Hindi | hi | 0% | none (no Devanagari shaping) | +| Arabic | ar | 0% | none (no RTL / shaping) | +| French | fr | 88% | partial | +| Portuguese (Brazil) | pt-br | 85% | partial | +| Indonesian | id | 0% | none | +| Japanese | ja | 87% | partial | +| German | de | 91% | partial | +| Korean | ko | 0% | none (no Hangul face) | +| Vietnamese | vi | 0% | none | -Spent so far ≈ 105–145 agent-hours, so **by effort the project is ≈ 20–35% of the way to complete Lightroom + Classic -parity**, and ≈ 45–60% of the way for cloud-Lightroom parity without AI and the Classic modules (remaining ≈ 130–255 h, -most of it the raw codecs, lens data, video and HDR). +Also shipped: Traditional Chinese (87%), Russian (74%), Ukrainian (99%). -**Wall clock:** ≈ 270–505 h for one agent working alone; with 4–6 parallel agents (≈ 70% parallel efficiency, merges and -CI under load cost the rest) **≈ 65–125 h of continuous work**. The AI package and the raw codecs carry most of the -uncertainty: they can finish faster if suitable permissive models / documentation turn up, or stall on licensing. +## Upcoming -## Totals +Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/gaps.md](docs/gaps.md). -Remaining from 2026-10-02 evening (see *Parity estimate* above for the breakdown): +1. Camera colour of our own: measured calibration for the popular models (50–90 h) +2. CR3 for every Canon body (25–45 h) +3. Render-fidelity suite against Lightroom, then tuning (30–60 h) +4. Full-resolution zoom (8–15 h) +5. AI masks: Subject / Sky / People (40–70 h + model decision) +6. Compressed ORF and HE NEF (28–55 h) +7. Lens profile database of our own (40–80 h) +8. Tests in CI on macOS, Windows and Linux (6–12 h) +9. Per-model raw verification (20–35 h) -| Target | Remaining agent-hours | Wall clock, 4–6 parallel agents | -|---|---:|---:| -| Cloud-Lightroom parity without AI or Classic modules | ≈ 130–255 h | ≈ 35–65 h | -| Full parity incl. AI, video and the Classic modules | ≈ 270–505 h | ≈ 65–125 h | +## Progress log -The milestone estimates in the table above were made before work started and are kept for calibration (M0–M13 took -≈ 25 active hours with parallel agents against an estimate of ≈ 110–170 h for those milestones). +- 2026-10-10 (RAW): RAW measured model by model against Adobe's 1,446-model list (234 verified, 943 + unverified, 233 preview-only, 36 unsupported; ~84% of photographers' cameras decode, ~40% on a + verified body). Full ~61%, mainstream ~49%, essentials ~64%; beta ~14 points away. +- 2026-10-10 (final): full ready is a weighted sum over the dimensions (~59%; method aligned with + the standard); mainstream practitioner ~51% (450–840 h), essentials user ~63% (160–300 h); + alpha gate passes. +- 2026-10-10: full re-measure for the progress-docs standard: stage alpha, ready ~55%, checklist + 81.1%; new docs (target-app-parity, gaps, roadmap, architecture, raw / file-format / hardware / UI + / localization parity); `docs/parity.md` renamed `docs/parity-checklist.md`. Landed today: Nikon + lossy-after-split NEF (#633), ORF 12.8 bpp (#651), HDR gain map JPEG / PQ AVIF / float TIFF export + (#646), Lightroom-like Auto (#628), activity stack. +- 2026-10-09: Sony ILCE-7CR camera colour: bundled profile from 125 photos; on 26 withheld photos mean + ΔE vs the camera JPEG 4.07 → 3.29. HEIC in release builds (#609), Samsung uncompressed SRW, + contact-sheet PDF export, Ukrainian and French UI, release v0.5.0. +- 2026-10-08: independent Rust CR3 lossless and version 0x100/0x200 C-RAW decoding, six exact + full-sensor regressions on M50 / R100 / R8; guarded CR3 camera-JPEG colour fitting + ([cr3.md](docs/cr3.md)). +- 2026-10-08: Fujifilm lossless / lossy compressed RAF: pure-Rust striped decoder, full sensor arrays + verified on 17 CC0 and 7 supplied files (13 bodies). Fujifilm camera colour: bundled X-H2S / X-T4 + profiles from 201 / 545 photos; on 150 withheld files mean ΔE X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34. + AI RAW denoise (#384), Lightroom catalog import, German, Spanish and Russian UI. +- 2026-10-07: Panasonic RW2 / Leica RWL / Panasonic RAW decode in every raw format, established on + 178 CC0 files from 118 bodies; RW2 file-local camera look (140 of 174 files). Faces: recognition in + pure Rust and People view. +- 2026-10-03 – 10-05: community PRs (#42–#51, #60) and 25+ issues: compressed NEF, Move import, + folder templates, Local roots and cleanup, 85k-photo grid and catalog performance, background + compaction, failed-save errors, GPU export hardening. Added the honest *Where we stand* assessment + and tracker rows for camera colour, camera coverage and render fidelity. +- 2026-10-02: parity estimate added (`cargo xtask parity` prints weighted completion); milestones + refreshed. +- 2026-10-01: RAW II formats: RAF (uncompressed Bayer + X-Trans), RW2 (packed), PEF (incl. Huffman), + ORF (uncompressed); embedded previews for every container incl. CR3; raw corpus test with 37 CC0 + samples. +- 2026-09-30 (later): app running with the full Lightroom-style UI; pipeline v0; DNG / CR2 / ARW; + README showcase. ≈ 8 h elapsed. +- 2026-09-30: roadmap created; M0 in progress; research docs (Lightroom reference, Rust imaging + ecosystem) complete. -## Risks that coding hours alone don't retire +## Revision history -- **AI features** (subject/sky/people masks, generative remove) need model weights with licences we can ship; classical - fallbacks first. No permissively licensed sky-segmentation or raw-denoise model was found — we may need to train our own. -- **Camera colour and lens data** is a data problem: we never use Adobe's matrices, DCPs or LCPs. DNG-embedded data first, - then our own calibration; long-tail camera/lens coverage grows over time. -- **Raw-format sources:** decided 2026-10-05: decoders are written from *prose* format descriptions (even ones - published alongside GPL code); decoder source is never read. Still open: freedom-to-operate review for local - Laplacian filters, PatchMatch and HEVC (HEIC). -- **Look parity** with Adobe's default rendering is tuned by eye today; the planned fidelity suite (LR-BEHAV-RENDER-FIDELITY) - turns it into measured comparisons against local-only Lightroom references. - -## Raw format coverage and known gaps - -Sony ILCE-7M4 downsized lossless ARW (subsampled YCbCr 4:2:0 / 4:2:2 tiles) now decodes to linear RGB; -private 3:2 & 4:3 examples verified through native rendering & full-resolution 16-bit export. - -CR3 lossless Bayer and version 0x100/0x200 C-RAW sensor decoding is verified pixel-exactly on Canon M50, R100 and R8; any other CR3, or one that fails to decode, opens from its embedded JPEG as before. See [`docs/cr3.md`](docs/cr3.md) for coding limits, reference provenance and colour limitations. - -Decoded (CC0 corpus from raw.pixls.us, `cargo xtask corpus --download`, `crates/raw/tests/corpus.rs`): DNG (uncompressed, -LJ92, lossy JPEG / Smart Previews, Deflate, float, linear), CR2, ARW (uncompressed, ARW2, LJ92; as-shot white balance and black level of pre-2017 bodies from the enciphered -maker-note `Tag2010` and the encrypted `SR2SubIFD`, both recovered by black-box analysis, `crates/raw/src/vendor/arw.rs`), NEF/NRW uncompressed and Huffman-compressed (lossless, lossy type 1/2, 12/14-bit), RAF uncompressed and lossless/lossy compressed (Bayer and -X-Trans, 14/16-bit real-file verification), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed formats 4 and 6, the prefix-coded strips of format 8, -packed formats 2, 5 and 7, the 16-bit words of the 2005–2007 bodies; all recovered by black-box analysis of 178 CC0 files from -118 bodies, `crates/raw/src/vendor/rw2.rs`), PEF (uncompressed and Huffman), ORF uncompressed (16-bit and 12-bit packed). Supported containers yield their embedded JPEG preview when present (including CR3). The engine uses it for RAW variants it cannot decode yet; HEVC-only CR3 previews are not decoded. - -Not decoded yet — preview only (no permissively licensed description; black-box analysis incomplete): -- **Nikon "lossy after split" NEF** (non-zero split row in maker note `0x0096`, e.g. some D3400/D5000/D5200/D5500/D5600 - files): rows above the split decode with the regular lossy table; from the split row on a different code is used - that our black-box analysis has not recovered yet (none of the four regular tables fits, also not with byte - alignment or reset predictors at the split row). The other Nikon Huffman variants are decoded (`crates/raw/src/vendor/nefc.rs` documents the analysis). -- **Olympus compressed ORF**, **Canon CR3 unverified CRX variants** (M11.1), **Canon sRAW/mRAW**, lossy DNG. - -**Camera colour matrices:** ARW, NEF, RW2, RAF, CR3, CR2 and PEF files can use guarded, separate chromaticity and tone estimates from their own embedded JPEG (see `docs/camera-preview-colour.md`); this is a per-file camera-look estimate with relative WB, not measured calibration or absolute-Kelvin WB. Other non-DNG raws and rejected fits use the documented neutral fallback (camera RGB ≈ linear sRGB, flagged -`matrix_is_fallback`) with the file's as-shot white-balance multipliers. Clean sources to evaluate next: manufacturer -matrices stored in the files themselves (Olympus ImageProcessing `ColorMatrix`, Pentax equivalents; Panasonic RW2 files carry none) and our -own chart-based calibration (M11.4). Adobe matrices are never used. - -## Lightroom Classic catalog migration - -File → Import Lightroom Catalog… reads `.lrcat` plus committed WAL pages directly through a pure-Rust -SQLite reader. Originals stay in place; ratings, flags, labels, XMP metadata, hierarchical keywords, -collections/sets, virtual copies and supported develop settings migrate into LightCraft. Existing edits -are preserved by default, reimport identities are persistent, and source settings/history/snapshots are -archived before catalog mutation. Missing originals remain available for relinking. - -Rendering is approximate: unsupported Adobe profiles/AI/process settings are reported and archived; -history/snapshots remain source data, and smart collections import current membership. The Lightroom -database is read-only; LightCraft owns subsequent edits. See `docs/lightroom-catalog-import.md`. - -## Log -- 2026-09-30: roadmap created; M0 in progress; research docs (Lightroom reference, Rust imaging ecosystem) complete. -- 2026-09-30 (later): app running with the full Lightroom-style UI; pipeline v0; DNG/CR2/ARW; README showcase. ≈ 8 h elapsed. -- 2026-10-02: parity estimate added (xtask parity prints weighted completion); milestone statuses refreshed. -- 2026-10-08: Fujifilm camera colour: guarded JPEG matching and relative WB, plus bundled X-H2S/X-T4 profiles from 201/545 photos. On 150 withheld files the pooled profiles improve mean ΔE slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); dark X-H2S samples 8.74 → 2.25. Measured calibration remains open. -- 2026-10-08: Fujifilm lossless/lossy compressed RAF: pure-Rust striped decoder; full sensor arrays verified against a black-box reference on 17 CC0 files and 7 supplied X-H2S / X-T4 files (13 camera bodies, 14/16-bit Bayer and X-Trans). Camera colour calibration remains a separate gap. -- 2026-10-01: RAW II formats: RAF (uncompressed Bayer + X-Trans), RW2 (packed), PEF (incl. Huffman), ORF (uncompressed); embedded previews for every container incl. CR3; raw corpus test with 37 CC0 samples. -- 2026-10-03 – 10-05: community PRs (#42–#51, #60) and 25+ issues landed: compressed NEF, Move import, folder templates, - Local roots and cleanup, 85k-photo grid and catalog performance, background compaction, failed-save errors, GPU - export hardening, clippy 1.99. Added the honest *Where we stand* assessment and *Where we're going* priorities; added - tracker rows for camera colour, camera coverage and render fidelity. -- 2026-10-07: Panasonic RW2 / Leica RWL / Panasonic RAW decode in every raw format (compressed 4/6/8, packed 2/5/7, - 16-bit words), black levels, in-camera aspect crops and mapped-out defects established on 178 CC0 files from 118 - bodies; RW2 gets the file-local camera look (accepted on 140 of 174 files with a preview). -- 2026-10-08: independent Rust CR3 lossless and version 0x100/0x200 C-RAW decoding, six exact full-sensor regressions on M50/R100/R8; any CR3 decode error falls back to the embedded JPEG; guarded CR3 camera-JPEG colour fitting with independent framing validation (`docs/cr3.md`). Unverified CRX variants and broader model verification remain. - -## Chinese and Japanese interfaces, and text watermarks - -English, Simplified Chinese, Traditional Chinese (Taiwan) and Japanese interface languages are persisted in UI state, and the -language table (`crates/ui-egui/src/i18n.rs`) drives the menus, settings and fonts, so another -language is a table entry plus two catalogs (docs/localization.md). Core menus and panels are -translated; untranslated text retains English. CJK glyphs (Chinese: Noto Sans CJK SC; Japanese UI: -BIZ UDPGothic; watermarks: BIZ UDMincho) come from storytold/craft-fonts, embedded by builds made -with the optional `CRAFT_FONTS_DIR` input (all releases), so no system fonts are needed. Text watermarks now accept -`vertical: true` in export JSON/presets and expose an orientation selector. Japanese -characters stay upright in top-to-bottom columns, with newlines starting columns to the -left. This is basic lettering, without tate-chu-yoko, ruby, kinsoku, or general vertical -OpenType shaping. The same coverage renderer serves 8/16/32-bit exports. +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | minor | RAW measured model by model; RAW decode and colour as explicit rows: full 59% → 61%, mainstream 51% → 49%, essentials 63% → 64%; beta ~14 points | +| 2026-10-10 | minor | Full ready restored to the additive weighted sum (~59%; method aligned with the standard, no new evidence); hours per audience; beta ~16 points away | +| 2026-10-10 | minor | Mainstream practitioner and essentials user numbers; full ready 55% → 46% (method written down); beta 340–600 h | +| 2026-10-10 | minor | Alpha gate added (core-workflow check): passes, stays alpha | +| 2026-10-10 | major | Restructured to the progress-docs standard; re-measured; *Where we stand*, *Parity estimate* and *Totals* moved to docs/target-app-parity.md, milestones and risks to docs/roadmap.md, raw coverage to docs/raw-parity.md, catalog migration to docs/file-format-parity.md, CJK notes to docs/localization-parity.md | +| 2026-10-08 | minor | *Where we stand* by dimension and by kind of user | +| 2026-10-02 | major | Parity estimate and effort table | +| 2026-09-30 | major | Created | diff --git a/apps/lightcraft-cli/Cargo.toml b/apps/lightcraft-cli/Cargo.toml index 3f60fe32..6471ab86 100644 --- a/apps/lightcraft-cli/Cargo.toml +++ b/apps/lightcraft-cli/Cargo.toml @@ -25,7 +25,14 @@ serde_json = { workspace = true } dhat = { workspace = true, optional = true } [features] +default = ["denoise"] +denoise = ["lightcraft-engine/denoise"] +rawnind-model = ["denoise", "lightcraft-engine/rawnind-model"] # Heap profiling: `cargo run --release -p lightcraft-cli --features dhat-heap -- snapshot …` counts # every allocation (live/peak bytes in `library.memory` / `ui.inspect`) and writes dhat-heap.json # (open it in DHAT's viewer: allocation sites by bytes at the peak). Slows allocation-heavy work. dhat-heap = ["dep:dhat"] + +# Open HEIC/HEIF photos (the optional lightcraft-heif crate). Not default so distributors choose +# it (HEVC patents); build with `--features heif` to include it. +heif = ["lightcraft-codecs/heif"] diff --git a/apps/lightcraft-cli/src/main.rs b/apps/lightcraft-cli/src/main.rs index 6c5e6e44..592a534a 100644 --- a/apps/lightcraft-cli/src/main.rs +++ b/apps/lightcraft-cli/src/main.rs @@ -342,7 +342,7 @@ fn mcp(args: &[String]) -> Result<(), String> { eprintln!("lightcraft-cli mcp: serving MCP on stdio ({})", backend.describe()); let mut server = Server::new(backend).with_command_tools(!compact); let stdin = std::io::stdin(); - server.serve(BufReader::new(stdin.lock()), std::io::stdout().lock()).map_err(|e| e.to_string()) + server.serve(BufReader::new(stdin), std::io::stdout()).map_err(|e| e.to_string()) } fn merge(args: &[String]) -> Result<(), String> { @@ -391,7 +391,7 @@ fn merge(args: &[String]) -> Result<(), String> { } i += 1; } - let mut s = Session::new().with_fs(); + let mut s = Session::new().with_fs().with_default_denoise_models().with_system_clock(); let paths = expand_paths(&files); let r = s.execute("library.import", &json!({"paths": paths})).map_err(|e| e.to_string())?; let mut ids: Vec = r["imported"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default(); @@ -652,7 +652,7 @@ fn render(args: &[String]) -> Result<(), String> { } let input = input.ok_or("render: missing input file")?; let output = output.ok_or("render: missing -o OUTPUT")?; - let mut s = Session::new().with_fs(); + let mut s = Session::new().with_fs().with_default_denoise_models().with_system_clock(); let abs = expand_paths(std::slice::from_ref(&input)); let r = s.execute("library.import", &json!({"paths": abs})).map_err(|e| e.to_string())?; let id = r["imported"][0].as_u64().ok_or_else(|| format!("{input}: not a readable photo"))?; @@ -728,19 +728,19 @@ fn snapshot(args: &[String]) -> Result<(), String> { if script.is_none() && output.is_none() { return Err("snapshot: give -o OUT.png and/or --script FILE".into()); } - if !(scale > 0.0 && size[0] >= 1.0 && size[1] >= 1.0) { - return Err("snapshot: bad --size/--scale".into()); - } + lightcraft_ui_egui::headless::viewport_pixels(size, scale).map_err(|e| format!("snapshot: bad --size/--scale: {e}"))?; let t0 = Instant::now(); let mut session = match &library { Some(dir) => { - let mut s = Session::new().with_fs(); + let mut s = Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models(); s.open_library(dir, false).map_err(|e| library_error(dir, e))?; s } - None if files.is_empty() => Session::with_demo().with_fs(), - None => Session::new().with_fs(), + None if files.is_empty() => Session::with_demo().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models(), + None => Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models(), }; + // Apply compute policy before imports or the first scripted query can discover an adapter. + session.execute("app.gpu", &json!({"enabled": false})).map_err(|e| e.to_string())?; if !files.is_empty() { let ti = Instant::now(); let r = session.execute("library.import", &json!({"paths": expand_paths(&files)})).map_err(|e| e.to_string())?; @@ -755,7 +755,11 @@ fn snapshot(args: &[String]) -> Result<(), String> { })), ..Default::default() }; - let app = lightcraft_ui_egui::LightcraftApp::new(session, services); + let mut app = lightcraft_ui_egui::LightcraftApp::new(session, services); + // Snapshot sessions promise no GPU: disable photo compute as well as the compositor + // before the first frame can start background adapter discovery. + // This UI state is session-local: the CLI neither loads nor saves desktop ui.json. + app.ui.settings.gpu = false; let mut h = Headless::new(app, size, scale); let timeout = Duration::from_secs(60); let mut shots = 0usize; diff --git a/apps/lightcraft-cli/tests/cli.rs b/apps/lightcraft-cli/tests/cli.rs index d1b178e4..ecda1102 100644 --- a/apps/lightcraft-cli/tests/cli.rs +++ b/apps/lightcraft-cli/tests/cli.rs @@ -7,6 +7,46 @@ use serde_json::{Value, json}; const BIN: &str = env!("CARGO_BIN_EXE_lightcraft-cli"); +#[test] +fn contact_sheet_exports_pdf_and_protects_originals() { + let input = tmp("contact-sheet-input.png"); + gradient_png(&input); + let out = tmp("contact-sheet.pdf"); + let (ok, lines, err) = run_cli( + &[ + "--import", + input.to_str().unwrap(), + "export.contactSheet", + &format!("path={}", out.display()), + "paper=letter", + "landscape=true", + "columns=2", + "rows=3", + ], + None, + ); + assert!(ok, "{err}: {lines:?}"); + assert_eq!(lines[0]["result"]["pages"], 1); + assert_eq!(lines[0]["result"]["photos"], 1); + let bytes = std::fs::read(&out).unwrap(); + assert!(bytes.starts_with(b"%PDF-1.4")); + assert!(String::from_utf8_lossy(&bytes).contains("/MediaBox [0 0 792.00 612.00]")); + let before = std::fs::read(&input).unwrap(); + for target in [input.clone(), input.with_extension("xmp")] { + if target != input { + std::fs::write(&target, b"existing sidecar").unwrap(); + } + let (ok, _, _) = run_cli(&["--import", input.to_str().unwrap(), "export.contactSheet", &format!("path={}", target.display())], None); + assert!(!ok, "overwrote protected file"); + } + assert_eq!(std::fs::read(&input).unwrap(), before); + assert_eq!(std::fs::read(input.with_extension("xmp")).unwrap(), b"existing sidecar"); + let prior = std::fs::read(&out).unwrap(); + let (ok, _, _) = run_cli(&["--import", input.to_str().unwrap(), "export.contactSheet", &format!("path={}", out.display()), "columns=0"], None); + assert!(!ok); + assert_eq!(std::fs::read(&out).unwrap(), prior, "failed export changed prior output"); +} + fn tmp(name: &str) -> std::path::PathBuf { let d = std::env::temp_dir().join(format!("lightcraft-cli-test-{}", std::process::id())); std::fs::create_dir_all(&d).unwrap(); @@ -233,6 +273,55 @@ fn snapshot_script_failure_exits_non_zero() { assert_eq!((d.width, d.height), (320, 200)); } +#[test] +fn snapshot_ui_zoom_keeps_requested_pixel_dimensions() { + let script = tmp("snap-ui-zoom.jsonl"); + std::fs::write(&script, format!("{}\n{}\n", json!({"method": "ui.zoomFactor", "params": {"factor": 1.1}}), json!({"method": "ui.inspect"}))) + .unwrap(); + for (size, scale, expected) in [("400x240", "1", (400, 240)), ("345x200", "0.9", (311, 180))] { + let out = tmp(&format!("snap-ui-zoom-{scale}.png")); + let o = Command::new(BIN) + .args(["snapshot", "--script", script.to_str().unwrap(), "-o", out.to_str().unwrap(), "--size", size, "--scale", scale]) + .output() + .unwrap(); + assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); + let replies: Vec = String::from_utf8_lossy(&o.stdout).lines().map(|line| serde_json::from_str(line).unwrap()).collect(); + assert!(replies.iter().all(|reply| reply["ok"] == true), "{replies:?}"); + assert!(replies[1]["result"]["pixelsPerPoint"].as_f64().unwrap() > scale.parse::().unwrap()); + let decoded = lightcraft_codecs::decode(&std::fs::read(out).unwrap(), Default::default()).unwrap(); + assert_eq!((decoded.width, decoded.height), expected); + } +} + +/// Headless snapshots default to CPU photo rendering as well as CPU UI rasterization. +#[test] +fn snapshot_defaults_to_cpu_without_gpu_environment_overrides() { + let script = tmp("default-cpu.jsonl"); + std::fs::write( + &script, + format!("{}\n{}\n", json!({"method": "engine.execute", "params": {"command": "app.gpu"}}), json!({"method": "ui.screenshot"})), + ) + .unwrap(); + let image = tmp("default-cpu.png"); + let output = Command::new(BIN) + .env_remove("LIGHTCRAFT_GPU") + .env_remove("LIGHTCRAFT_GPU_BACKEND") + .env_remove("WGPU_BACKEND") + .args(["snapshot", "--demo", "--script", script.to_str().unwrap(), "-o", image.to_str().unwrap(), "--size", "480x320"]) + .output() + .unwrap(); + assert!(output.status.success(), "status {}: {}", output.status, String::from_utf8_lossy(&output.stderr)); + let replies: Vec = String::from_utf8_lossy(&output.stdout).lines().map(|line| serde_json::from_str(line).unwrap()).collect(); + assert!(replies.iter().all(|reply| reply["ok"] == true), "{replies:?}"); + let gpu = &replies[0]["result"]; + assert_eq!(gpu["enabled"], false, "{gpu}"); + assert_eq!(gpu["available"], false, "{gpu}"); + assert_eq!(gpu["adapter"], Value::Null, "no device is created: {gpu}"); + assert!(gpu["reason"].as_str().is_some_and(|reason| reason.contains("preference")), "{gpu}"); + let decoded = lightcraft_codecs::decode(&std::fs::read(image).unwrap(), Default::default()).unwrap(); + assert_eq!((decoded.width, decoded.height), (480, 320)); +} + /// Issue #136: with the GPU switched off from the environment the UI starts and renders on the CPU, /// without creating a GPU device (no driver is loaded), and says why. #[test] @@ -412,3 +501,79 @@ fn warnings_are_logged_on_stderr() { let off = run("off"); assert!(!off.contains("names no known backend"), "{off}"); } + +#[test] +fn snapshot_rejects_invalid_dimensions_before_creating_an_image() { + for (index, (size, scale)) in + [("NaNx100", "1"), ("100x100", "inf"), ("9000x9000", "1"), ("9000x100", "2"), ("100x100", "0.001")].into_iter().enumerate() + { + let image = tmp(&format!("invalid-viewport-{index}.png")); + let output = Command::new(BIN).args(["snapshot", "--size", size, "--scale", scale, "-o"]).arg(&image).output().unwrap(); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("snapshot: bad --size/--scale"), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(!image.exists()); + } +} + +#[test] +fn snapshot_preserves_fractional_scale_rounding() { + let image = tmp("fractional-scale-rounding.png"); + let output = + Command::new(BIN).env("LIGHTCRAFT_GPU", "0").args(["snapshot", "--size", "345x200", "--scale", "0.9", "-o"]).arg(&image).output().unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let decoded = lightcraft_codecs::decode(&std::fs::read(image).unwrap(), Default::default()).unwrap(); + assert_eq!((decoded.width, decoded.height), (311, 180)); +} + +/// Snapshot's CPU setting must never replace the desktop preference, including with --library. +#[cfg(target_os = "linux")] +#[test] +fn snapshot_leaves_desktop_gpu_preferences_untouched() { + let dir = tmp("snapshot-desktop-preferences"); + let library = dir.join("library"); + let mut session = lightcraft_engine::Session::new().with_fs(); + session.open_library(&library, true).unwrap(); + drop(session); + let config = dir.join("config"); + let app_config = config.join("lightcraft"); + std::fs::create_dir_all(&app_config).unwrap(); + let ui_path = app_config.join("ui.json"); + let mut ui = lightcraft_ui_egui::UiState::default(); + ui.settings.gpu = true; + let saved = serde_json::to_vec_pretty(&ui).unwrap(); + std::fs::write(&ui_path, &saved).unwrap(); + let script = dir.join("preferences.jsonl"); + std::fs::write( + &script, + format!("{}\n{}\n", json!({"method": "engine.execute", "params": {"command": "app.gpu"}}), json!({"method": "ui.inspect"})), + ) + .unwrap(); + for gpu_environment in [None, Some("1")] { + let image = dir.join(format!("{}.png", gpu_environment.unwrap_or("default"))); + let mut cmd = Command::new(BIN); + cmd.env("XDG_CONFIG_HOME", &config).env_remove("LIGHTCRAFT_GPU").env_remove("LIGHTCRAFT_GPU_BACKEND").env_remove("WGPU_BACKEND"); + if let Some(value) = gpu_environment { + cmd.env("LIGHTCRAFT_GPU", value); + } + let output = cmd + .args(["snapshot", "--library"]) + .arg(&library) + .args(["--script"]) + .arg(&script) + .args(["-o"]) + .arg(&image) + .args(["--size", "480x320"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let replies: Vec = String::from_utf8_lossy(&output.stdout).lines().map(|line| serde_json::from_str(line).unwrap()).collect(); + assert!(replies.iter().all(|reply| reply["ok"] == true), "{replies:?}"); + assert_eq!(replies[0]["result"]["enabled"], false); + assert_eq!(replies[0]["result"]["available"], false); + assert_eq!(replies[0]["result"]["adapter"], Value::Null); + assert_eq!(replies[1]["result"]["ui"]["settings"]["gpu"], false); + assert_eq!(std::fs::read(&ui_path).unwrap(), saved, "snapshot changed the desktop preferences"); + assert!(!app_config.join("ui.json.tmp").exists()); + assert!(image.exists()); + } +} diff --git a/apps/lightcraft-web/Cargo.toml b/apps/lightcraft-web/Cargo.toml index a3331a60..aaa14a64 100644 --- a/apps/lightcraft-web/Cargo.toml +++ b/apps/lightcraft-web/Cargo.toml @@ -71,3 +71,9 @@ web-sys = { workspace = true, features = [ "WorkerType", "WritableStream", ] } + +[features] +# Open HEIC/HEIF photos (the optional lightcraft-heif crate). Not default so distributors choose +# it (HEVC patents); build with `--features heif` to include it. On wasm32 panics abort, so +# heic-rs's `catch_unwind` guard does not apply there. +heif = ["lightcraft-codecs/heif"] diff --git a/apps/lightcraft-web/index.html b/apps/lightcraft-web/index.html index bc0ad203..b088ad18 100644 --- a/apps/lightcraft-web/index.html +++ b/apps/lightcraft-web/index.html @@ -24,13 +24,21 @@