* PERF-CULL-0: ui.inspect reports how the loupe showed each photo it was stepped to
perf.loupeSteps records, per step, the ms from the step to the first frame
with anything of the photo, with a full-size picture (camera JPEG, cached
or rendered) and with LightCraft's own render, measured on the frames the
app draws, so culling benchmarks exclude the control channel's round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95f0343848fc0a238f80f07ceb292eaf6779aeaa)
* PERF-CULL-1: browsing a folder shows its first photos within a second
The Local view read and hashed every file of the folder before showing any
of them (2,933 EOS R5 C CR3s: 30.6 s to the first photo). It now hands the
files the library lacks straight to the import, ordered the way the view
sorts them (file time for the date sorts), which adds them batch by batch:
first photo 0.9 s, twelve thumbnails 1.4 s, the whole folder in the same
~34 s as before, in the background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b766558e7ef4f802919a144da53e444f70e0186b)
* PERF-CULL-2: the loupe makes the next photos' camera previews ahead of the step
Culling steps through photos, and each step waited for the next raw's
embedded JPEG to be read and decoded (p50 187 ms, p95 332 ms on EOS R5 C
CR3s) and then for a full raw develop. Now:
- the loupe's stand-ins for the next five photos in the direction of
travel and the one behind are made ahead (Slot::QuickAhead, priority
just below the loupe's own render) and taken over at the step, so a
step shows the camera JPEG at once (p50 0 ms);
- a slot keeps its photo while the photo stays wanted, so photos that
shift places as the user steps are not made twice;
- decoded camera JPEGs stay in the memory LRU (never on disk), so going
back shows them again without a decode;
- while an arrow key is held (a photo left within 0.25 s), the raw
develop waits until the photo has stayed 0.12 s, and a render queued
for a photo already left is dropped, so the workers make the previews
instead of renders nobody sees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0430b95636d2b6adce8887af7b9464687c95549e)
* PERF-CULL-3: CR3 previews take the smallest embedded JPEG that covers the view; plain sRGB JPEGs decode straight to 8-bit
Upstream's embedded_preview_covering (14e0d128) picks the smallest covering
JPEG of a TIFF-based raw; this extends it to Canon CR3 and adds the 8-bit
decode:
- A CR3 keeps an 8192 px JPEG track, a 1620 px PRVW box and a 160 px THMB.
embedded_preview_covering now picks among the three (cr3_boxes lists the
boxes), so a grid thumbnail or a loupe up to 1620 px decodes the PRVW.
Loader at 1280 px on 20 EOS R5 C CR3s: 168-322 ms -> 31 ms a photo.
- lightcraft_codecs::decode_jpeg_srgb8_fit decodes a plain 8-bit sRGB
JPEG (DCT-scaled) and box-averages it in linear light straight into
8-bit sRGB, without the float image the general path builds; anything
with an ICC profile, an Adobe RGB declaration, CMYK or 12-bit takes the
general path. 45 MP preview fitted to 2560 px: 182 -> 151 ms (min of
15, loaded machine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a67bb398c2f438cbe16f4db1431ae0270f1c8b04)
* PERF-CULL-4: Camera preview label; thumbnails ahead of the step; 100 % shows the camera JPEG first
- While the loupe shows a raw's embedded JPEG (or its thumbnail), a small
'Camera preview' label at the top right says so, with what that means
on hover (widget loupe:cameraPreview; nine languages), so colour and
tone are not judged on the camera's rendering.
- The look-ahead also asks for the next photos' thumbnails (a small
embedded JPEG each), so a held arrow key has something to show in full
screen too, where no filmstrip asks for them (2400x1500 window, 40 held
steps: camera JPEG on 36, nothing on 2; before 1 and 4). Stepping
through a whole wedding keeps at most 600 thumbnails, those around the
open photo.
- Zoomed in on an unedited raw, its camera JPEG at full size (8192 px for
an EOS R5 C) is drawn under the window render until that arrives:
100 % in ~0.9 s instead of ~5.6 s; released when not zoomed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9f212b1e16815c3c0ca33130319a7ef41fb05a26)
* PERF-CULL-5: lightcraft-cli bench-cull and tools/cull-bench.py time culling a real shoot
bench-cull FOLDER (headless) times reading the folder into the Local view,
the first screen of grid thumbnails on the app's render threads, camera
previews at 1280 / 2560 px and full size, a cold 2560 px loupe render,
rating 50 photos in a row and importing N files in place / copying M,
as 'name: X ms wall' lines (p50, p95, n) and JSON. Libraries are made
under --work; nothing is written next to the photos.
tools/cull-bench.py drives the running app over the control channel
(browse, first photo, steps at rest, a held arrow key, 100 % zoom,
rating) and reads the app's own frame timings (perf.loupeSteps).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit bd2eaac591b72f8258acae4389c67aa4d1da3b27)
* PERF-CULL-5: bench-cull --files K samples a slow folder; e2e test; cull-bench usage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c32c9556924ccdee6f8c103864df51c3e170df98)
* PERF-CULL: docs for culling speed (parity rows, gap 26, preview reads, opening a photo)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fb69b5975b51f882d82b2ca684e0f7ddc8373908)
* PERF-CULL: the whole-file preview decodes a plain sRGB camera JPEG the quick path's way
Upstream's quick-path test wants the bounded read and the whole file to give the same picture
byte for byte; with the branch's straight-to-8-bit decode on the quick path only, a 49 px
preview came out differently. Both paths now take it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0c8acab4c2f3e85d1c51fab5faf62b6e997d7c73)
* PERF-THUMB-FIRST: CR3 thumbnails decode the 1620 px PRVW copy, and grid thumbnails from camera JPEGs are kept in the thumbnail cache
A CR3 grid thumbnail decoded the full-size JPEG track (45 MP, ~150 ms each)
whatever size was asked; it now takes the smallest embedded copy that covers
the cell (PRVW, same shape), and the box search stops at mdat (was a scan of
the whole file). The decoded camera JPEG of an unedited raw is written to
the library's thumbnail cache, so a folder seen before fills from small
files. Measured on 250 R5 C raws (loaded machine): first screen fills in
294 ms (was 553), on a second visit 53 ms (was 610).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 12162f966dd859b7e53ac65b5c4564a8e837b7eb)
* PERF-CULL: cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: docs for culling speed (parity rows LR-IMP-LOCAL, LR-IMP-PREVIEW-READ, LR-VIEW-DETAIL; gap 26)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: review fixes (bench work folder, 8-bit APP1 cap, size sort)
- bench-cull makes its libraries in a new folder of the run's own under
--work and never deletes what is already there (it removed lib-browse,
lib-add and lib-copy under a folder the caller named).
- The 8-bit sRGB JPEG path refuses more than 128 APP1 segments before its
decoders walk them again, as the general decode does.
- Browsing by file size keeps unreadable files last in both directions
(they were sized 0, so first when ascending).
- --files K in the CLI synopsis and help.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the loupe uses built previews; photos are prepared ahead and in the background
Built standard previews were never what the loupe showed: it rendered
every photo again (~1.8 s on a raw), the rendered previews in memory were
capped at 128 MB (~84 photos) whatever the budget, the disk cache was a
fixed 2 GB (a wedding's previews pruned each other), the cache's writer
skipped disk writes when it fell behind, and Build Previews with one photo
open built only that photo though the menu said "Visible".
- The loupe's render job takes a cached view render of the same settings
that is at least as big as asked for, fitted down (no render).
- Memory for rendered previews is a quarter of the cache share; the disk
cache's automatic size makes room for every photo (about 1.5 MB each);
Settings offers Automatic, 50 GB and 200 GB.
- A build skips photos whose previews are there, writes each to disk
before counting it, builds at least the loupe's size, and with one photo
selected builds the view.
- Prepare photos in the background (Settings > Performance), per mode
(culling, library, editing), app-wide (kept in ui.json) or per library:
photos ahead rendered for the loupe (a few at a time, 0 = off) and the
whole view's previews built in the spare time once the user steps
through photos: two lanes, waiting while the photo on screen renders,
nearest first in the direction of travel, one cancellable row, replaced
by a build that is asked for. Commands prefs.backgroundPrep /
prefs.setBackgroundPrep; ui.inspect perf.previewCache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the bench work-folder test removes only a folder it made
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: browsing keeps the whole listing for the folder bookkeeping; cull-bench stops on small folders
The browse worker queues only the files the library lacks, but the folder
bookkeeping (browse times, records of files gone) needs every file listed:
a subfolder of known photos only was not stamped as browsed. The known
files now come along marked as already in the library. tools/cull-bench.py
stops waiting at min(12, photos) thumbnails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: review fixes (smart previews honour id, editing builds by the Library row, lenient prep settings)
- library.smartPreviews with an explicit id works on that photo, not the
whole view.
- While editing, the whole view's background build follows the Library
row (its switch and which photos); the Editing row's scope is for the
photos kept ready ahead.
- The app-wide and per-library preparation settings are read leniently:
a value this build doesn't know is the defaults (app) or none (library),
and the other settings are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: cull-bench counts twelve thumbnails while the folder is still loading
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: a view change stops the background build for the old view
The running background build was only re-aimed at the photo on screen; after
another folder, filter or library it went on building photos no longer
shown. It is now stopped when the view's signature changes (not counted as
stopped by the user), and the view now shown is prepared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: cull-bench picks the thumbnail threshold from the final photo count
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the view signature covers every photo; the view-change test holds the build explicitly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: cull-bench stops waiting for twelve thumbnails 30 s after the folder loaded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: in the marked scope, marking a photo changes what the view's build prepares
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: cull-bench times only the thumbnails the browse adds
Thumbnails already cached before library.browse no longer count towards
the first-thumbnail and twelve-thumbnail times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: count every thumbnail made, for the browse benchmark
thumbTextures counts resident thumbnails, and evicting old ones can hide
new ones. The renderer now also counts each grid thumbnail it stores
(perf.thumbsMade, never lowered by eviction), and cull-bench times the
browse from that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the marked-scope test waits for its builds before cleaning up
Both preview builds finish before the test library is removed, and the
removal is checked, so no worker writes into a folder being deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: stand-ins made ahead keep their order, size and photo
Review fixes for the loupe's stand-ins and the browse benchmark:
- a stand-in still queued ahead moves to its new place in the order
when the user steps;
- one made ahead still shows when the loupe's own request failed;
- one made ahead for a smaller loupe isn't taken over by a larger one;
- quick and full-size camera-preview keys include the photo, so a
virtual copy gets its own;
- perf.thumbsMade counts photos given a thumbnail, not a render
replacing a stand-in.
Each has a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a late stand-in never takes another photo's place in the loupe
A stand-in made ahead takes over after the loupe's own request failed
only while the loupe still wants that photo and has no other request
pending; the test covers stepping on before it arrives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a stand-in made ahead and no longer wanted is dropped when it arrives
Freeing a look-ahead slot whose job is already running now also forgets
its request, so the late result isn't kept as a hidden texture, unless
the loupe is waiting for exactly that stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a running stand-in the loupe waits for keeps its look-ahead slot
When the next look-ahead no longer wants the photo the loupe has just
stepped to, the slot whose job is still making that photo's stand-in stays
reserved instead of going to a new photo, so the result still reaches
the loupe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 10 (browse order ties, library skip, CR3 boxes after mdat, badge once, look-ahead crop key, cli test binary)
- browse_order: import- and edit-date sorts queue new files by name (they
tie on those keys); ties go by name in the view's direction, as the
catalog breaks them.
- Local browsing never lists the library's own folder (browse_files, as
scan_with skips it).
- CR3 PRVW/THMB search skips only the mdat payload: boxes after it (a moov
holding THMB) are still found.
- The camera-preview badge and the loupe timing are settled once, after
the window renders (no double badge with the full-size camera JPEG).
- The loupe look-ahead key includes the crop.
- bench-cull CLI tests use binary() after upstream's test split.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 11 (browse listing errors and library root, rating ties, every CR3 PRVW, checked CR3 box math, look-ahead view key, windows wait while settling)
- browse_files: a folder that can't be listed is an error (the scan ends
as failed, nothing is forgotten as gone), and the library's folder or
one inside it lists nothing.
- New photos tie on rating: queued by name in the view's direction.
- Every valid CR3 PRVW/THMB is a candidate (the smallest covering one is
picked); the box offsets use checked arithmetic.
- The loupe look-ahead is keyed to the visible list's generation (a new
sort, filter or source changes the neighbours).
- While stepping fast, zoomed-in window renders wait like the frame, and a
window drawn before the frame is timed as the render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 12 (loupe timing: the After window, over any stand-in)
In a split view only the After side's window says the raw's render is on
screen; and a window drawn over the camera JPEG counts as the render too
(the source shown is a render only when it is "render" or "cached").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 13 (loupe timing: the Before window when the Before is shown alone)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 14 (loupe timing: a Before window side by side with \ held is not the shown view)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 15 (the split-view timing test asserts its precondition)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 16 (side by side with \ held, each window stays in its pane)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a cancelled browse stops ordering its files; browsing hands back earlier probes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the web build runs the browse job in place as FnMut (it hands the probes back)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a scan hands the probes it took back however it ends (CodeRabbit, #3077 / #3080)
- The browse worker sends a result on every path: the listing, or `Stopped` (the folder couldn't be listed, or
another folder was chosen) with the probes `ScanInput::new` took from the session. A listing failure is never
sent as an empty listing.
- A scan stopped without waiting (its cross, or a browse replaced by another) keeps its channel until the worker
ends, so the probes still come back; a finished scan's probes join the session's instead of replacing them.
- A cancelled scan whose worker ended without a result is not reported as "Scan failed".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: built previews are the ones on disk; stand-ins ahead follow the preparation settings (CodeRabbit, #3077)
- A build counts a photo as built only when its previews are in the disk cache (when it has one): a loupe
render kept only in memory, its deferred write skipped, is built again and written (`stored_dims`).
- `DiskCache::put` and `PreviewCache::put_at` say whether the file was written; a view preview the cache
folder can't take fails the photo instead of counting it as built.
- The loupe's stand-ins ahead honour Settings > Performance for the mode: none for "Only the photo on screen"
or no photos ahead (the queued ones are dropped), only marked photos in the marked scope. The settings are
part of what the look-ahead was made for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: a preview for a discarded cache fails the photo; cache file sizes are read from 4 KiB first (CodeRabbit, #3077 / #3080)
- `PreviewCache::put_at` returns false for a job made before the cache was cleared or retired (nothing is
kept), so a build that overlaps Discard Preview Cache counts that photo as failed, not built. The warning
says which of the two happened.
- `DiskCache::dims` reads a file's first 4 KiB, and up to 64 KiB only when the size marker isn't there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: a photo another build renders counts once it is built; a write pruned away is not kept (CodeRabbit, #3077 / #3080)
- A build that meets a photo another build is rendering waits for that render, then counts the photo as
built only when its previews are there, else renders it itself (it was counted as shared at once).
- `DiskCache::put` returns false when the pruning its own write starts removes that file (a picture bigger
than the room pruning leaves).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: put_memory holds the cache generation through its insertion (CodeRabbit, #3080)
A clear can no longer finish between put_memory's look at the generation and its insertion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: browsing skips the library's folder by any name it has (a symlink, a path through ..) (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: rustfmt the library-folder check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: cancelled builds stop before their test library is removed (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the library folder is skipped by its resolved path only; lib/../outside is browsed, an unresolvable folder reports its error (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the replaced background build is held by TestHold, not a 250 ms deadline (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a browsed folder has one spelling (. and .. taken out by name) for the view, its listing and the catalog's records; an unresolvable root inside the library is still skipped (CodeRabbit, #3055 / #3077 / #3080)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: TestHold counts its holders: one test's release doesn't free a build another test holds (CodeRabbit, #3077 / #3080)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: TestHold's release without the deprecated fetch_update
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: browsing resolves `..` the way the system does, and `.` is the current folder
`lexical_path` took `..` out by name, which is wrong on POSIX when the folder before it is a
symlink (`link/..` is the folder above where the link points). `browse_path` makes the root
absolute first (so `.` and `a/..` name the current folder, not an empty path), resolves a link
just before a `..` on POSIX, and keeps every other spelling as written (a mapped drive stays a
drive letter, matching the catalog's records). Windows takes `..` out by name, as before.
Tests: a folder link followed by `..` lists what the system lists there; `.` is the current folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: browsing resolves the folder on its worker, never on the window's thread
`browse()` called `browse_path` (which may canonicalize a link) and `is_dir()` before starting the
worker, so a link into a stalled network mount could freeze the window. The UI now spells the
folder with `browse_spelling` (`.`/`..` by name, no disk access) and shows it as given; the worker
resolves it with `browse_path`, checks it is a folder (else the scan ends as failed, with the
usual toast), and sends the resolved spelling, which the view takes before the listing arrives.
Tests: a browse whose resolution is held returns at once and waits, then reads the folder; a
missing folder fails on the worker; `browse_spelling` matches `browse_path` without links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the browse worker resolves the path as given, not the view's spelling (CodeRabbit, #3055 / #3077 / #3080)
On Unix `photos/link/../select` with `link` pointing at `real/day` is `real/select`; the spelling with `..`
taken out by name read `photos/select`. The view keeps the spelling until the worker answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: bench-cull measures small folders and never writes its results among the photos (CodeRabbit, #3077)
A folder with no more photos than the grid sample was reported with empty camera-preview and loupe timings: the samples now fall back to the photos it has. A --json path in the source folder is refused before the run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: editing renders the loupe from its source; the loupe holds background work back only while its render is on the way (CodeRabbit, #3077)
In Develop a cached view render left the source undecoded until the first slider draft. foreground() was renewed every loupe frame even with the render shown, so background builds mostly waited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: loupeSteps.total, a step cursor that survives the 256-step list (CodeRabbit, #3077)
tools/cull-bench.py sliced the list by its length, which stops growing at 256 steps, and reported no steps on repeated runs. Also documents perf.thumbsMade and perf.previewCache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the camera preview is located once when the fast decode declines (CodeRabbit, #3077)
The general decode takes the stand-in JPEG the fast path already found instead of parsing the container again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the uncapped preview cache warns once per session when its drive runs low
The automatic preview cache budget grows with the library and has no ceiling. When the library
opens, and at most every five minutes while previews are built, the engine reads the free space of
the drive holding the cache (fs4, MIT OR Apache-2.0, pure Rust over rustix / windows-sys); under
10 % or under 20 GB free it raises one alert per session. The desktop app shows it as a notice with
the drive's free space and the cache's size and a button to Settings > Performance, where a size
limit can be set. A drive that can't say its free space gives no alert (logged). library.info
reports lowDiskSpace; the notice is in ui.inspect's notices like the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the free-space read runs off the UI thread; a queued low-disk notice goes with its library; --work outside the source folder (CodeRabbit, #3077)
- Session::check_disk_space reads the drive on a worker thread and takes the answer on a later
call (take_disk_space_alert, library.info, the next check): a slow network share no longer
holds up a library open or a UI frame. Test: a_slow_drive_does_not_hold_up_the_check.
- The UI keeps the low-disk notice with the library generation it was raised for and drops it
when another library opens. Test: a_queued_notice_goes_when_another_library_opens.
- The real-drive test runs on native targets only (read is None on wasm32).
- bench-cull refuses a --work folder inside the measured folder, also one not made yet, before
anything is created. Test: the_work_folder_never_goes_in_the_source_folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: one free-space read at a time across library switches, the cache counted on its worker; a --json link into the source folder is refused (CodeRabbit, #3077/#3080)
- A library opened while the drive's free-space read still runs keeps that read (marked stale)
instead of starting a second worker beside it; its answer is dropped and the current library's
drive is read once it has come. Switching libraries while a share hangs no longer piles up
workers. Test: switching_libraries_while_a_drive_hangs_starts_no_second_read.
- When the drive is low, the preview cache is counted on the read's worker and the count travels
with the answer: taking it on the UI thread no longer scans a big cache on a slow share.
DiskCache::known_size reads the count without touching the disk.
Test: the_preview_cache_is_counted_off_the_ui_thread.
- bench-cull: a --json path that exists is resolved itself (a link to a photo in the source folder
is that photo), a link that points nowhere yet by the file writing through it would create, a
loop of links is refused. Test: a_json_link_into_the_source_folder_is_refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: bench-cull --json replaces its file instead of writing into it, so a hard link to a photo can't overwrite the photo (CodeRabbit, #3077)
The results go to a new file next to the destination, renamed over it; the
temporary file is removed on error. Test:
a_json_hard_link_to_a_photo_leaves_the_photo_alone (fails with the old write).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: bench-cull refuses a --json entry in the source folder, also a link there to a file outside (CodeRabbit, #3080)
Writing the results replaces the --json entry itself, so a link sitting in the
source folder that points outside it would be replaced, removing that entry.
check_outputs now checks where the entry sits (its folder resolved, the final
link not followed) as well as where it resolves to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a frame without the loupe ends its step timing, so time away from Detail never counts (CodeRabbit, #3055)
LoupeTiming::left was never called: a photo's record stayed open while the grid, Compare, Survey,
Reference, People or the slideshow was up, so dwell / any / full / sharp ms counted time outside
Detail. observe() marks the frame; finish_ui_pass ends the step when the loupe drew nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: Build Standard-Sized Previews keeps 2560 px before a loupe was drawn (CodeRabbit, #3077)
On Automatic with no loupe size known yet (a fresh session) the command left `edge` out, so the
engine built its 2048 px minimum and a typical fit view rendered again. The loupe-aware engine
size still applies once a loupe was drawn; a chosen limit is used as is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: main's merge keeps one Russian "★ Favorites" and its browse test uses the scan's end channel
Both sides had added the same key; main's new Auto Import browse test sends through the scan
channel, which this branch carries as `ScanEnd`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: stepping fast drops the queued Before render too; the thumbnail count is per photo across eviction (CodeRabbit, #3055)
While an arrow is held the Before view's raw render waits like the loupe's own, and one still
queued for a photo left behind is dropped. `thumbsMade` counts each photo once per library, so a
thumbnail evicted and made again, or a library left and opened again, doesn't count twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the free space is read every few minutes, not only while previews are built (CodeRabbit, #3080)
Thumbnails and the loupe's previews fill the cache too, so a drive could run low after the library
opened without the notice: the low-disk check now runs whatever fills the cache (still at most
every five minutes). The engine gains two hidden test hooks (a reader and forgetting the last
read).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: a slow free-space read shows in the activity stack and an idle window wakes for the disk check (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: background preparation advances on every logic pass, hidden ones too (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a photo the loupe opens on is never a fast step; only a step inside the loupe has a predecessor (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the cull benchmark rates one named photo and puts its rating back (CodeRabbit, #3077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the cull benchmark refuses a full step history, says when thumbnails were in memory, fails on a lost key and ends a zoom that needs no region (CodeRabbit, #3055)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: a forced build renders a photo another build was rendering itself (CodeRabbit, #3080)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the cull benchmark stops on a folder with no photos and times a zoom by the region that ended it (CodeRabbit, #3055/#3080)
- browse: a folder that stays loaded with no supported photo ends the browse loop after 3 s
instead of waiting 30 minutes, and the run stops with a clear message before the loupe
section instead of failing on an empty selection;
- zoom to 100 %: the time reported is the moment the loop ended on (the region's arrival, or
the settled whole frame when no region is asked for), so a whole frame that settled before
a region was requested no longer gives a too-short zoom time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the cull benchmark says no photos are visible, keeps an earlier result, and records an unfinished zoom as missing (CodeRabbit, #3055/#3077/#3080)
- an empty selection after browsing means no photo is visible (a filter can hide them), not that
the folder has none: say so and point at the filters;
- LABEL.json is no longer overwritten: an existing one stops the run before it starts unless
--overwrite is given (the final write opens with "x" then);
- a zoom that did not finish within 60 s is written as null, not as a 60 s zoom time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a raw's preview pointer is read with checked bounds on its offset and size (FredApps, #3080)
The JPEGInterchangeFormat pointer and the undefined-tag JPEGs were sliced with `as usize` casts of
the file's own u64 numbers: safe on 64-bit (the slice is `get`, its end cut at the file's end), but
an offset past 4 GiB would wrap to a small, wrong one on a 32-bit build (wasm32). `file_span` now
converts with `try_from` and saturates the end.
Test: `a_preview_pointer_past_the_end_of_the_file_finds_no_preview` (pointers past the end, lengths
past it, u32::MAX for both; a real strip preview beside the lying pointer is still found; every
truncation of the file is safe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: bench-cull refuses a --work or --json inside the photo folder (FredApps, #3055)
Nothing is ever written next to the photos: a --work folder or a --json file inside FOLDER (the
folder itself, a subfolder, or a path that goes there through `..`) is refused before anything is
made. Paths are compared resolved (links followed as far as they exist). The cleanup already
removes only what the run made (its own `bench-cull-run-…` folder's `lib-copy`), never --work.
Test: `work_and_json_inside_the_photo_folder_are_refused`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: a test that building previews writes nothing next to the photos (FredApps, #3077)
`building_previews_writes_nothing_next_to_the_photos`: Build Previews and the background
preparation while culling leave the shoot folder with the same files, byte for byte; previews go
only to the library's own cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: cull-bench.py prints its usage on missing arguments and refuses a dangling LABEL.json link up front (CodeRabbit, #3055/#3077)
- PORT / FOLDER missing (or PORT not a number), or an option with no value or an unreadable one,
stop with the usage line instead of a traceback;
- the early "LABEL.json exists" check uses `os.path.lexists`, so a dangling link there is refused
before the run, not at the final exclusive write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a preview cut short by the end of the file is not taken (CodeRabbit, #3077)
`file_span` keeps a span the file's end cuts short only while it still holds a JPEG end marker
(an overstated length with the whole JPEG inside). A truncated JPEG passed the frame-header check,
won as the largest preview and then failed to decode, so no preview was shown.
Test: `a_preview_cut_short_by_the_end_of_the_file_is_not_taken`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-PREVIEW-PREP: the shoot-folder test waits for the background build to finish (CodeRabbit, #3077)
`building_previews_writes_nothing_next_to_the_photos` used a 5 s capped poll and then went on; it
now asserts `wait_finished` (30 s), so a late write by a slow build can't slip past the check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a cut-short preview must reach its own JPEG end marker; cull-bench refuses bad options up front (CodeRabbit, #3055/#3077/#3080)
- raw: file_span keeps a span the file's end cuts short only when walking the JPEG's marker
segments reaches its end marker inside it, and cuts it there. An FF D9 inside an APP payload no
longer counts (it is skipped by the segment length), and bytes that don't start with FF D8 are
refused without scanning the rest of the file. Tests: the cut-short test gains an APP1 payload
holding FF D9 (taken before this fix) and a whole JPEG followed by other bytes;
the_end_of_a_jpeg_is_its_own_end_marker covers stuffed bytes, restart markers, bad segment
lengths and every truncation.
- tools/cull-bench.py: an unknown option, an option where a value belongs (--label --overwrite),
--steps/--hold-ms below 1, or a port outside 1-65535 stop with the usage line before the run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: cull-bench refuses a run whose steps wouldn't fit the app's loupe history (CodeRabbit, #3055)
The app keeps its last 256 loupe steps. A --steps that can't fit a run (1 + 11 + STEPS + 1) stops
before the run, and each phase checks the room left before it starts, so a hold whose first steps
were dropped is never reported as the whole hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: bench-cull refuses ".." in --json and --work (CodeRabbit, #3544)
A ".." after a folder the run hasn't made yet can't be resolved by the source-folder check, and
the run then makes that folder: --json NEW/../FOLDER/r.json would land in the source folder.
Test: a_dot_dot_in_the_outputs_is_refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a cut-short preview needs a scan before its end and the walks share a budget; cull-bench catches dropped steps and odd arguments (CodeRabbit, #3055/#3077/#3080)
- raw: jpeg_end accepts an end marker only after a start of scan (an end with no image data
before it is refused), and the walks of a file's pointers past its end share scan_budget, as
scan_for_jpeg's do. Tests: a whole-looking JPEG with no scan is not taken;
the_walks_to_a_jpegs_end_share_a_budget.
- tools/cull-bench.py: the port must be a decimal number int() takes (isdecimal, guarded); a
FOLDER that looks like an option stops with usage; --steps goes up to 254 (only the hold's
STEPS + 1 must fit, with one to spare); each phase starts with room to spare, so a full history
means other steps came in and the phase stops instead of reporting a shifted window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: the camera JPEG goes only under the window render, and a stand-in decoded across a cache clear isn't kept (CodeRabbit, #3544)
- ui-egui: zoomed in, the full-size camera JPEG is painted only where the window render will go
(window_cover), so a window capped smaller than the view leaves the developed whole-frame render
in the strips beside it instead of an unlabelled camera JPEG. Test:
the_camera_jpeg_covers_only_where_the_window_goes.
- engine/preview: QuickJob's camera-JPEG cache carries the cache generation it was made in and
writes with put_deferred_at / the new put_memory_at, so a job decoding when Discard Preview
Cache clears the cache can't put its picture back (as render jobs already do). Test:
a_camera_jpeg_decoded_across_a_cache_clear_is_not_kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a bad build request leaves the background build alone, and preparation settings that can't be saved aren't kept (CodeRabbit, #3544)
- library.buildPreviews stops a running background build only after its own ids are known to be
good, and a build whose thread can't start is marked finished instead of left "running".
- prep_tick clears started_for when the background build fails to start, so the view retries.
- set_prep_settings / clear_library_prep put the previous settings back when prefs.json can't be
written. Tests: a_bad_build_request_leaves_the_background_build_running,
a_prep_setting_that_cant_be_saved_is_not_kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a background build that can't start waits 30 s before the view tries again
Clearing `started_for` on a failed start made every tick call library.buildPreviews again while
the cause lasted (CodeRabbit on #3080). The view now waits RETRY_AFTER first. Test
`a_build_that_cant_start_waits_before_trying_again`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a build waiting to retry asks for a frame at its time; a build whose worker can't start leaves the background one running
CodeRabbit on #3077/#3080: a background build that couldn't start retried only on
the next input. The session now says how long until the retry (prep_retry_in) and the
app asks for a frame then.
CodeRabbit on #3544: a build asked for whose worker thread couldn't start cancelled the
background preparation and left nothing running. The background build is now stopped
only once the new one has started; otherwise it stays the session's build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: a retry time that has passed asks for no more frames
CodeRabbit on #3077 / #3080: once the retry deadline passed, prep_retry_in returned
zero, so a view that couldn't be tried (grid, preparation off) asked for a frame at
once on every pass and kept an idle window drawing. It returns None then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit items from #3577 that live in #3077 and #3055
- disk space: a read whose thread can't start (or whose worker ends without
answering) is tried again after 10 s, not after the 5-minute check interval;
a hung read is polled every 5 s after its first 5 s instead of every 250 ms
- the low-disk notice of the library before is not drawn in the frame after a
library switch
- library.smartPreviews reads its ids before stopping the background build
- a forced build renders the grid thumbnail again instead of reading its cache
- the loupe's look-ahead asks for its stand-ins again after a renderer reset
- browse: a broken link before `..` is an error, not the link's parent folder
- the probes of two stopped scans: the later scan's wins whichever ends first
- camera JPEG fast path: a JPEG with an incomplete ICC sequence goes the
general way; a CR3 without a JPEG track picks its box preview by size too
- tests: bench-cull compares the source photos' bytes, the disk notice test
waits for the open's read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-CULL: CodeRabbit round 4 on #3077 and the #3583 items in its code
- A background smart-preview request cancels the running build only once its worker started;
a worker that can't start leaves the earlier build running and the session's (test hook).
- The notice logic looks again at the next disk check (10 s after a read that couldn't start).
- CR3 boxes: largest by pixels; every top-level mdat left out; whole-file fallback takes the
first box of a kind; a copy without an end marker isn't chosen to cover a size; a test
decodes the chosen copy.
- The fitted JPEG decode reads a SEF file as the image before its trailer; its doc says a
header that can't be read is Ok(None).
- Settings: a saved cache size the list doesn't offer shows as its own option.
- bench-cull rejects a second FOLDER; a browse through `..` reads again.
- cull-bench.py: --size checked, baseline waits for renders, --skip-browse claims no folder,
a short rest phase warns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the quick-full draw call
* Loupe stand-in choice moves to its own function
* Render results: the newer-draft and display checks move to their own functions
* Render results: a frame left on the GPU is shown by its own function
* Format the frame's texture entry
* Docs: one row each for the import and performance entries
* Format the quick view's key
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
LightCraft
Your photos. Your pixels. Your machine.
Photo library and raw development; an open-source, non-destructive photo library and raw developer, built in pure Rust.
Native on macOS, Windows and Linux. In the browser via WebAssembly. Drivable end to end by AI agents over MCP.
LightCraft on getartcraft.com · ArtCraft · All Crafting Apps
Ansel Adams, "The Tetons and the Snake River" (1942). Public domain, U.S. National Archives. Developed in LightCraft.
Note
ArtCraft is a community of artists from all walks of life. Digital, generative, music, games — if you make things, you're one of us. Come say hi on Discord.
Editing · Color grading · Before & after · Masking · Presets · Library · Agents & MCP · Performance · Status · Quick start · Roadmap · Downloads · Crafting Apps
Edit like you mean it
LightCraft is a complete darkroom in a single native app. Every adjustment is non-destructive, so your originals are never touched. Every slider renders through a scene-referred, wide-gamut, 32-bit float pipeline: highlights roll off like film, shadows open up without halos, and colour stays clean from capture to export.
☀️ LightExposure, Contrast, Highlights, Shadows, Whites, Blacks, with edge-aware local tone mapping (a guided filter on log-luminance). Pulling −100 Highlights recovers a blown sky without the grey halos you'd get from a naive curve. 🎨 ColorWhite balance by temperature and tint (Kelvin for raw, relative for JPEG) with presets, Auto and a click-to-neutralise eyedropper. Vibrance that protects skin tones, Saturation, an 8-band Color Mixer (hue / saturation / luminance) and 3-way Color Grading wheels with blending and balance. All of it is computed in OkLCh, a modern perceptual colour space. |
✨ EffectsTexture for fine detail, Clarity for mid-tone punch, Dehaze (dark-channel prior with guided refinement; push it negative to add atmosphere), post-crop Vignette with highlight priority, roundness and feather, and resolution-independent film Grain with size and roughness. 📈 Tone CurveParametric region curve with movable splits plus point curves for RGB, Red, Green and Blue. Curves are monotone by construction, so you never get an accidental tone inversion. |
A gentle S on the RGB point curve, right under the Light sliders. Ansel Adams, 1942 (public domain).
Color grading, the cinematic way
Split-tone shadows, midtones and highlights independently with drag-anywhere colour wheels. Below, Dorothea Lange's Migrant Mother gets a warm, print-like tone (highlights at 42°, shadows at 28°) in two drags.
Shadows, midtones and highlights wheels in the Color panel. Dorothea Lange, "Migrant Mother" (1936). Public domain, Library of Congress.
Before & after
Press \ to toggle the Before view, Y for side by side, or Shift+Y for a split view. Every image below is a real screenshot of LightCraft, captured automatically by an agent through the control channel.
Masking that goes where you point
Paint with a Brush (size, feather, flow, density, erase), drop Linear and Radial Gradients with draggable pins, or select by Luminance Range, Color Range, Sky, Subject and Background. Combine components with Add / Subtract / Intersect, invert any of them, and dial in 17 local adjustments per mask (Temp, Tint, Exposure, Contrast, Highlights, Shadows, Whites, Blacks, Texture, Clarity, Dehaze, Hue, Saturation, Sharpness, Noise, Moiré, Defringe), plus an overall Amount.
Desktop Object and Describe masks use SAM 3 for click-based and text-prompt selection, with a separately installed model; see model setup and terms. Subject, Sky and Background selections still use classical heuristics. Existing model-generated masks render and export without the model installed.
A radial "Sun glow" mask (Temp +40, Exposure +0.50) layered over a linear "Sky" mask. Photo from LightCraft's procedurally generated demo library.
Presets, profiles & the Color Mixer
Forty-one hand-built presets ship in the box (Golden Hour, Teal & Orange, Faded Matte, Selenium Tone, Crisp Landscape and more), each with an Amount slider from 0 to 200 %. Save your own from any group of settings, mark favourites, and copy, paste or sync edits across a whole selection with exactly the groups you choose.
The Presets column next to the 8-band Color Mixer. Photo from the demo library.
Organize everything
A library that stays out of your way: All Photos, Recently Added, Picks, By Date, Albums nested in
Folders, and Recently Deleted. Rate with 0–5, flag with P / X /
U, colour-label with 6–9. Search understands fields:
rating:>3 flag:pick iso:>800 camera:x2 date:2026-04 keyword:mountains person:"Jane Doe"; plain words also match
the people named on faces, and quotes keep several words together. Every view sorts by capture date,
import date, edit date, name, rating, size or at random (a stable shuffle; View → Sort → Reshuffle for a new one). The justified Photo Grid and Square Grid views are virtualized,
so they stay smooth whether you have forty photos or forty thousand.
The People view groups named faces, including names read from XMP; face boxes can be edited in the loupe. On desktop, automatic detection and recognition use optional models installed through Settings → Faces; no weights are bundled. See face models and setup.
Built for agents
Every menu item, slider, brush stroke, crop handle and keystroke in LightCraft is a command with a stable id and JSON parameters. The UI, the keyboard, the CLI, a JSON-lines control channel and an MCP server all dispatch through the same entry point. An agent can cull a shoot, develop it, mask a sky and export it, and see the result.
lightcraft --control 7980 ~/Pictures/trip
{"method": "engine.execute", "params": {"command": "photo.flag", "params": {"flag": "pick"}}}
{"method": "engine.execute", "params": {"command": "develop.set", "params": {"values": {"light.highlights": -45, "light.shadows": 38}}}}
{"method": "engine.execute", "params": {"command": "mask.add", "params": {"kind": "radial", "center": [0.62, 0.4], "rx": 0.2, "ry": 0.14}}}
{"method": "ui.clickWidget", "params": {"id": "slider:effects.clarity"}} // drive any widget by name
{"method": "ui.pointer", "params": {"events": [{"kind":"down","x":0.2,"y":0.3}, {"kind":"up","x":0.4,"y":0.3}]}}
{"method": "ui.screenshot", "params": {"path": "after.png"}}
-
Command registry.
engine.commandslists the available commands;develop.controlslists every slider's range, default and current value. -
MCP server.
lightcraft-cli mcpexposes the command registry to Claude (or any MCP client), alongside helpers for import, query, develop, render (returned as an image) and export. Masks have no helper of their own: create and edit them throughrun_command(e.g.mask.add). It runs headless, or attached to the running app with screenshots, clicks and gestures. See docs/mcp.md.cargo build --release -p lightcraft-cli claude mcp add lightcraft -- "$PWD/target/release/lightcraft-cli" mcp ~/Pictures/shoot # headless claude mcp add lightcraft-app -- "$PWD/target/release/lightcraft-cli" mcp --connect 127.0.0.1:7980 # live app -
Scriptable CLI:
lightcraft-cli run --import in.dng develop.set control=light.exposure value=0.7 app.export path=out.jpg longEdge=2048runs 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:
lightcraft-cli run --import ~/Pictures/shoot library.selectAll export.contactSheet path=Contact.pdf paper=a4 columns=3 rows=4 captions=trueexport.contactSheetalso accepts explicitids,landscape=true, andpaper=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. -
Print layouts (native app / CLI): File → Print… opens the Print module's layout editor: Single Image, Contact Sheet, Picture Package (Auto Layout packs cells onto pages) and Custom Package (drag and resize cells, stacking order, snap to cells or a grid), Zoom to Fill / Rotate to Fit, Photo Border and Inner Stroke, paper sizes from 4 × 6 to 13 × 19 and A3 to A5 or custom, margins, rulers in in/cm/mm/pt, page background, 72–720 ppi, and saved templates. Print to PDF… renders in the background. Everything is a
print.*command (print.get,print.set,print.cell.add,print.layout.autoLayout,print.exportPdf,print.template.save…), so the CLI and MCP drive it too:lightcraft-cli run --import ~/Pictures/shoot print.set 'layout={"style":"picturePackage","page":{"paper":"8x10"}}' \ print.cell.add width=5 height=7 print.cell.add width=2.5 height=3.5 print.layout.autoLayout print.exportPdf path=Package.pdfThe Print Job panel supports printer/paper ICC profiles, rendering intent, black point compensation, print adjustment, sharpening and 8/16-bit PDF output. Direct printing through an OS print dialog and page overlays (identity plate, watermark, page numbers) are not there yet.
-
Slideshows: View → Slideshow plays the photos in view full screen, with fades, repeat and random order. Settings → Slideshow configures layouts, templates, backgrounds, overlays and title screens. Native exports support PDF, JPEG sequences and AV1 video in MP4 or WebM; music and a dedicated Slideshow module editor remain missing. See docs/slideshow.md.
-
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/scripts. Protocol reference: docs/control-protocol.md.
Fast, native, private
- 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, JPEG XL, GIF (first frame) and BMP 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 5K display and a 60 MP export look the same.
- 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. 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.
- Background rendering. A worker pool renders the loupe, before/after and every visible thumbnail off the UI thread: drafts during drags, full quality on release.
- Local-first. No account, no cloud, no telemetry, no subscription. Your catalog is an append-only log of human-readable operations you can diff, back up or replay.
Feature status
LightCraft is young and moving fast: alpha. Where we honestly stand (details in the roadmap and docs/target-app-parity.md):
- By feature count we're at ~82% (core features 98%), tracked row by row in docs/parity-checklist.md.
- As a day-to-day tool we're nearer 61% for the whole product, ~51% for a typical working photographer and ~65% 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 for your camera.
- The biggest gaps:
- camera colour calibration: Sony, Nikon, Panasonic, Fujifilm and Canon CR3 and CRW raws have guarded estimates from their camera JPEGs, with built-in Canon EOS D60, ILCA-99M2, ILCE-7CR, ILCE-7M4, ILCE-7RM5, X-H2S, X-T4, Nikon Z 6II, Z 8 and D5300 profiles; measured calibration covers one model so far (chart-measured matrices for the Canon EOS D60), 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;
- AI quality and model setup: Subject and Sky selection are classical heuristics; Object/Describe use an optional SAM 3 model. AI Denoise supports Bayer RAW with separately installed weights, while X-Trans and linear-RGB denoise and Super Resolution remain missing; see AI masks and AI Denoise;
- network libraries: a preview reads the raw's header — or, for a CR3 or a TIFF raw whose preview tag sits past it, exactly the ranges the sparse planner names — and its embedded JPEG only (56.5 MB → 5.9 MB, 10 s → 1 s on the test share), but the developed picture still needs the sensor data over the wire; see docs/raw-preview-reads.md;
- on-screen HDR display, video import/editing, and the Book / Map module views. Print layouts and colour-managed PDF output work; direct printer output and page overlays remain missing. Slideshow playback and export work, with a dedicated module editor and music still missing.
- What's next: see the ranked gaps and the roadmap.
| Area | Status |
|---|---|
| Library: albums, folders, smart albums, stacks (incl. auto-stack), virtual copies, ratings, flags, labels, filter bar, search, sort, grids, filmstrip | ✅ |
| Culling: Compare (synced zoom) and Survey views, auto-advance, instant previews | ✅ |
| Light, Color, Effects (vignette styles), Tone Curve (+ refine saturation, targeted adjustment), Color Mixer (+ targeted), Point Color, Color Grading, Calibration, B&W | ✅ |
| Masking: brush, linear, radial, luminance/colour range, add/subtract/intersect; Object and Describe selections | ✅ · Object/Describe require optional SAM 3; Subject/Sky remain heuristic (details) |
| 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 matrices, guarded camera-JPEG look fits and built-in profiles for selected cameras | 🚧 measured calibration and per-model coverage remain incomplete (raw parity) |
| 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, 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) |
| Detail: sharpening, luminance + colour noise reduction | ✅ · AI Denoise 🚧 (Bayer, user-installed weights; setup), Super Resolution ⬜ |
| Remove / Heal / Clone spots (auto source), non-generative Fill, editable spot pins, Visualize Spots, Red Eye and Pet Eye | ✅ · Fill uses PatchMatch; no generative fill |
| Export: JPEG / PNG / TIFF / flat PSD / WebP / AVIF / JPEG XL / raw or rendered 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; gain map JPEG, PQ/HLG AVIF, PQ PNG/JPEG XL and float TIFF export | ✅ · 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 | ✅ |
XMP sidecars (read/write, auto-write), reading crs: develop settings, preset files (.lcpreset, XMP presets) |
✅ |
| Optics (distortion, vignetting, auto + manual CA, defringe, lens corrections embedded in DNG files, Panasonic / Leica RW2 / RWL distortion data and Nikon Z 6II / Z 8 / Z f 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 🚧 |
| People view, XMP face names and editable face boxes; automatic face detection and recognition | ✅ · detection/recognition require optional models (setup) |
| GPX tracklog auto-tagging and offline reverse geocoding | ✅ · place names require the GeoNames dataset; Map view ⬜ |
| Print layouts, picture packages, templates and colour-managed PDF export | ✅ layouts/PDF · 🚧 direct printer output and page overlays |
| Slideshow playback, layouts, overlays, templates and PDF/JPEG/AV1 export | ✅ playback · 🚧 dedicated module editor and music (details) |
| Video import/editing and Book module | ⬜ |
| 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
Quick start
Workspace builds, including the desktop, web and CLI applications, require Rust 1.95 or newer.
git clone https://github.com/storytold/lightcraft && cd lightcraft
cargo run --release -p lightcraft # opens your library (~/Pictures/LightCraft Library; a new one starts with demo photos)
cargo run --release -p lightcraft -- ~/Pictures/trip # import your photos (folders are scanned, duplicates skipped)
cargo run --release -p lightcraft -- --memory # a throwaway in-memory demo session (writes nothing)
cargo run --release -p lightcraft -- --control 7980 # with the automation channel
cargo xtask web --serve # the same app in the browser: http://127.0.0.1:8080/
cargo run --release -p lightcraft-cli -- render photo.jpg -o out.jpg --set light.exposure=0.5
cargo xtask ci # fmt, clippy, HEIF, tests, parity, layers, assets, WASM
cargo xtask ci --quick # only what your branch's changes can break (minutes)
cargo xtask ci --quick compares with the merge base of origin/main (--base REF for another)
and runs fmt, clippy, the tests of the changed packages, layers and assets, plus HEIF, parity and
the wasm check when the changes touch them; a change to Cargo.toml, Cargo.lock or the lint
config tests everything. GitHub runs the full cargo xtask ci on every pull request
(.github/workflows/ci.yml). Only one ci runs at a time per machine: a second one, from another
worktree say, waits for the first and names it, since several at once take longer in total than in
turn. LIGHTCRAFT_CI_SLOTS=2 allows two; LIGHTCRAFT_CI_LOCK=off skips the lock.
CI defaults to line-table debug information and one build job per 1.5 GB of RAM
available when it starts (at most one per CPU; 4 if memory can't be read), so an
8 GB machine with 6 GB free builds with 4, and a busy machine with little free
builds with fewer. Test threads follow, capped at 4.
Full-debuginfo linkers, one per CPU, otherwise exhaust RAM before any test runs. GPU coverage is
unchanged. Explicit CARGO_PROFILE_DEV_DEBUG, CARGO_BUILD_JOBS, RUST_TEST_THREADS and
NEXTEST_TEST_THREADS settings override these defaults. The tests run under
cargo test; LIGHTCRAFT_NEXTEST=1 runs them under cargo-nextest instead (one
process per test, slower here because each regenerates the demo scenes). Ordinary development commands keep their own settings.
Release builds on memory-constrained machines: #1196
reports Arch/AUR release-link OOM kills on a 16 GB machine when the desktop and CLI build concurrently
with full debug information, thin LTO and one codegen unit. The CI defaults above do not apply to
ordinary cargo build --release. Start with one job; two may be suitable if enough memory is available:
CARGO_BUILD_JOBS=1 cargo build --release -p lightcraft -p lightcraft-cli
# Alternatively: CARGO_BUILD_JOBS=2 cargo build --release -p lightcraft -p lightcraft-cli
If a packaging environment enables full debug information or memory-heavy LTO settings, use command-local overrides for a lower-memory source build:
CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_DEBUG=0 CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 cargo build --release -p lightcraft -p lightcraft-cli
These POSIX-shell assignments affect only this command, not global Cargo configuration or the
repository's release profiles. Disabling LTO may change performance and binary size; disabling debug
information reduces debugging detail. This is build guidance, not a guarantee that any machine has
enough memory. Downstream packagers should serialize large binary builds and avoid full debug
information combined with LTO and one codegen unit on low-memory builders. A PKGBUILD that exports
its own profile settings can override inherited variables; adjust the packaging recipe locally or
contact its maintainer rather than assuming an environment prefix on makepkg will take precedence.
Linux/Arch build troubleshooting: rustc SIGILL with ThinLTO.
In #2811, an Arch Linux x86_64 build on an
AMD Ryzen 9 7900X3D with 64 GB RAM failed with rustc 1.99.0 / LLVM 23.1.1, AUR lightcraft
0.7.1-1 and 0.13.0-1, RUSTFLAGS="-C opt-level=3 -C target-cpu=native", and release settings
opt-level=3, lto=thin, codegen-units=1. cargo build --frozen --release reached the executable
crates before rustc crashed with signal 4 (SIGILL / ILL_ILLOPN). The core dumps point into LLVM's
SelectionDAG / vector-combine optimization path; the exact compiler root cause has not been isolated.
This does not mean every machine with that hardware or toolchain will fail. It is a different observed
failure from the 16 GB OOM kills in #1196 above;
there is no evidence establishing a shared root cause.
For a direct Cargo source build, the equivalent command-local LTO override is:
CARGO_PROFILE_RELEASE_LTO=off cargo build --frozen --release
Reporter @rwarmstr verified the workaround by exporting CARGO_PROFILE_RELEASE_LTO=off inside the
AUR PKGBUILD's _srcenv() function and rebuilding both package versions, without changing
RUSTFLAGS. The reporter tested the AUR recipe, not the direct Cargo invocation above.
A variable prefixed to makepkg alone may be overwritten by the PKGBUILD's own exports; the recipe
must honor or set the override. The AUR !lto opt-out behavior is tracked separately in
#2813.
Cargo's LTO documentation distinguishes
false from off: false can still perform thin local LTO across codegen units (unless
codegen-units=1 or opt-level=0); off disables LTO explicitly. The low-memory recipe above is
unchanged. This workaround is for affected builds, not a new default; disabling LTO may change
performance, binary size and build behavior.
Tests that need a GPU, the corpus, the craft-fonts faces or a model pass without checking anything when
those are missing (libtest has no "skipped" status), so cargo xtask ci lists them: tests record a skip
with lightcraft_testkit::skip!, and the run ends with what the machine offered (OS, CPUs, RAM, GPU
adapters per backend, corpus, fonts) and every skip by reason. It writes target/ci/report.json and
target/ci/summary.md, a block to paste into a PR (--summary-md prints it); --hardware also runs the
GPU tests on every backend that has a hardware adapter. Nothing is uploaded.
Chinese and Japanese text need the shared font repo, an optional build input (official releases always include it):
git clone https://github.com/storytold/craft-fonts ../craft-fonts
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 — what goes there is described in the craft-fonts README.
Interface language: Edit → Language (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Español, Deutsch, Русский, Français, Українська, Polski, Čeština, Italiano, ქართული, Svenska, Suomi, Bahasa Indonesia, Tiếng Việt) or Settings → General → Language; the choice applies immediately and persists. See docs/localization.md.
Logs: the desktop app writes its log to standard error and to logs/lightcraft.log in its settings folder
(Linux $XDG_CONFIG_HOME/lightcraft/logs/, by default ~/.config/lightcraft/logs/; macOS
~/Library/Application Support/LightCraft/logs/; Windows %APPDATA%\LightCraft\logs\), never in the library.
Portable mode: a file named portable.txt (or LightCraft.portable) next to the executable moves the settings
folder to LightCraftData beside it. A
launch from a desktop menu or the Dock has no terminal, so attach this file to a bug report (Help ▸ Open Log
Folder shows it in the file manager). Each start moves the
previous log to lightcraft.1.log and that one to lightcraft.2.log, so the log of a run that crashed survives the
next start; the file stops growing at 16 MiB, --version and --help write none, and runs with
LIGHTCRAFT_NO_PREFS log to standard error only. By default LightCraft's own crates log at info and everything else
at warn. LIGHTCRAFT_LOG=info or debug works as before (that level for LightCraft's own crates, warnings and
errors from the rest; any other value: warnings and errors only) and wins over RUST_LOG, which otherwise replaces
the default with env_logger-style directives such as RUST_LOG=debug or RUST_LOG=warn,lightcraft_pipeline=trace (a
directive ending in * covers every target starting with it, as in lightcraft*=debug). Panics are recorded there
too, and still in lightcraft-panics.log in the temp folder. The logger is apps/lightcraft/src/logging.rs.
The web build needs the wasm32-unknown-unknown target and the matching wasm-bindgen CLI
(cargo xtask web prints the exact install command); see docs/web.md.
Nix builds the desktop app and lightcraft-cli (the Nix build always includes the craft-fonts input, so Japanese
text has glyphs):
nix run github:storytold/lightcraft # the desktop app
nix build github:storytold/lightcraft # → ./result/bin/{lightcraft,lightcraft-cli}
nix develop github:storytold/lightcraft # rust toolchain + native deps + fonts
On NixOS, run a cargo build from nix develop: outside it the app can't find libvulkan.so.1, finds no GPU and
renders on the CPU, much more slowly (it says so once at start-up). nix run and nix build set the path themselves.
In a flake configuration (NixOS, home-manager, nix-darwin):
# flake.nix
inputs.lightcraft.url = "github:storytold/lightcraft";
# optional: build against your own nixpkgs instead of the one LightCraft pins
# inputs.lightcraft.inputs.nixpkgs.follows = "nixpkgs";
# then, in a NixOS or home-manager module (where `inputs` is in scope):
nixpkgs.overlays = [ inputs.lightcraft.overlays.default ]; # makes `pkgs.lightcraft` available
environment.systemPackages = [ pkgs.lightcraft ]; # home-manager: home.packages = [ pkgs.lightcraft ];
nix build installs the same desktop file, hicolor icons and AppStream metadata as the .deb/.rpm, and runs
cargo test --workspace as its check phase (skip it with pkgs.lightcraft.overrideAttrs { doCheck = false; }).
Keyboard: G grid · D detail · E edit · C crop · M masking · Shift+P presets · \ original · Y before/after · Z zoom · J clipping · ⌘Z undo · ⌘/ all shortcuts.
How it's built
An engine-first Cargo workspace of small, tested crates with enforced layering (cargo xtask layers): geom,
color, raster, tiff → raw, codecs, meta, develop → pipeline → catalog → engine → ui-egui. The
egui frontend is one swappable crate; nothing below it knows a UI exists. mcp and the apps (lightcraft,
lightcraft-cli) sit on top of engine.
Contributing
Humans and agents follow the same rules, so read AGENTS.md first. The short version:
- Clean-room. Never read proprietary binaries or GPL raw/photo code (darktable, RawTherapee, LibRaw, rawspeed, dcraw…); work from public specs and black-box observation.
- No proprietary assets, ever: no icons, screenshots, presets, profiles, LUTs or fonts from commercial products. Every image, icon and font in the repo is original, public domain, Creative Commons, OFL or permissively licensed, and has an entry in assets/ATTRIBUTION.md added in the same commit. New fonts go to storytold/craft-fonts, not here.
- Pure Rust, enforced crate layering, everything is a command, and
cargo xtask cigreen before every commit (one task id per commit). - Never crash. Non-test code returns errors instead of panicking: no
unwrap(),expect(),panic!orunsafe, checked indexing on anything derived from input, and a regression test with every crash fix. Details in AGENTS.md.
Questions, ideas or a bug you'd like to talk through first? Bring them to Discord.
Downloads
New to LightCraft? Download it from the LightCraft page on getartcraft.com. That's the easiest way to install it.
Want a specific build or format? On GitHub, the latest release has every build listed below, and all releases has earlier versions and their notes. <ver> in the file names is the version number, and SHA256SUMS.txt lists a checksum for every file.
Windows
| Build | Installer | Portable |
|---|---|---|
| x64 (64-bit Intel/AMD) | lightcraft-<ver>-windows-x64.msi |
lightcraft-<ver>-windows-x64-portable.zip |
| arm64 (Snapdragon and other ARM PCs) | lightcraft-<ver>-windows-arm64.msi |
lightcraft-<ver>-windows-arm64-portable.zip |
| x86 (32-bit) | lightcraft-<ver>-windows-x86.msi |
lightcraft-<ver>-windows-x86-portable.zip |
Installers and executables are code-signed. The installer asks who to install for and where (for your account only, in
%LOCALAPPDATA%\Programs\ArtCraft\LightCraft, by default; "Everyone on this computer" uses
C:\Program Files\ArtCraft\LightCraft (Program Files (x86) for the 32-bit build); 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-<ver>-windows-x64.msi /qn INSTALLFOLDER="D:\Apps\LightCraft\".
A silent install is per-user by default. INSTALLFOLDER chooses the folder, not who it is installed for;
add ALLUSERS=1, from an elevated terminal or deployment tool, for an all-users install.
Update or uninstall asks for an old .msi, or stops with error 1612 or 1714? Windows removes the installed version
with its own cached copy of that version's installer; if the copy is gone, it asks for the original file and stops
(an update then ends with error 1603). Download the release MSI of the version and build you have installed from
all releases and run
msiexec /fv "<path to that .msi>" /qb+ /norestart, approving the permission prompt if Windows shows one. That restores
the cached copy; then retry the update or uninstall. Opening the MSI, or choosing Repair or Uninstall on it, may still
fail. Error 1603 has other causes too; if this doesn't help, open an issue with the error message.
macOS
| Build | File | Notes |
|---|---|---|
| App, universal (Apple silicon + Intel) | lightcraft-<ver>-macos-universal.dmg |
Signed and notarized |
| Command-line tool, universal | lightcraft-cli-<ver>-macos-universal.zip |
Signed and notarized |
Linux
| Format | x86_64 | aarch64 (ARM64) | Notes |
|---|---|---|---|
| AppImage | lightcraft-<ver>-linux-x86_64.AppImage |
lightcraft-<ver>-linux-aarch64.AppImage |
Portable; updates with AppImageUpdate (.zsync files) |
| Flatpak | lightcraft-<ver>-linux-x86_64.flatpak |
lightcraft-<ver>-linux-aarch64.flatpak |
Sandboxed; flatpak install --user <file> |
| Debian/Ubuntu | lightcraft-<ver>-linux-x86_64.deb |
lightcraft-<ver>-linux-aarch64.deb |
|
| Fedora/RHEL/openSUSE | lightcraft-<ver>-linux-x86_64.rpm |
lightcraft-<ver>-linux-aarch64.rpm |
|
| Tarball | lightcraft-<ver>-linux-x86_64.tar.gz |
lightcraft-<ver>-linux-aarch64.tar.gz |
Unpack anywhere |
RISC-V (riscv64): lightcraft-<ver>-linux-riscv64.tar.gz, a tarball only (glibc 2.39 or newer, e.g. Ubuntu 24.04+).
On a Wayland desktop LightCraft runs through XWayland when it is available, because only X11 gives
windows file drag-and-drop; set LIGHTCRAFT_WAYLAND=1 to use native Wayland (sharper fractional scaling, no drop).
FreeBSD
| Build | File |
|---|---|
| x86_64 | lightcraft-<ver>-freebsd-x86_64.tar.gz |
Web (WebAssembly)
| Build | File | Notes |
|---|---|---|
| Static site | lightcraft-web-<ver>.zip |
Runs in a modern browser; host it on any static server |
The Crafting Apps
LightCraft is one of the Crafting Apps: free, open-source creative tools from the ArtCraft team, each written from scratch in Rust and each able to stand on its own.
| App | What it's for | Code | Learn more | |
|---|---|---|---|---|
| PhotoCraft | Image editing: layers, masks, type and real PSD files | GitHub | Website | |
| VectorCraft | Vector illustration | GitHub | Website | |
| FilmCraft | Video editing, color and sound | GitHub | Website | |
| LightCraft | Photo library and raw development | GitHub | Website | |
| PdfCraft | Reading, organizing and protecting PDFs | GitHub | Website | |
| EffectCraft | Motion graphics and visual effects | GitHub | Website | |
| DesignCraft | Page layout and publishing | GitHub | Website | |
| FigCraft | Interface design and prototyping | GitHub | Website | |
| WordCraft | Word processing and documents | GitHub | Website | |
| SoundCraft | Audio recording and editing | GitHub | Website | |
| DeckCraft | Presentations and slide decks | GitHub | Website | |
| CADCraft | Computer-aided design | GitHub | Website | |
| GridCraft | Spreadsheets and data | GitHub | Website | |
| TextureCraft | 3D texture painting | GitHub | Website | |
| Craft Anything | See how any software works, then build your own | GitHub | Website | |
| SolidCraft | Parametric 3D CAD by Tangency Labs (community app) | GitHub |
And ArtCraft itself, our AI image and video studio for artists who want real control.
Come make things with us
Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film, set type, and people still figuring out what they like to make. Share what you're working on, ask for help, tell us what's broken, or tell us what you wish these tools could do. Whatever your medium and however long you've been at it, you're welcome here.
discord.gg/artcraft · getartcraft.com · The Crafting Apps · LightCraft
License and credits
LightCraft is dual-licensed under MIT or Apache-2.0, at your option. Copyright (c) 2026 ArtCraft Team and the LightCraft contributors. Required notices are in NOTICE.
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed with its author, source and license in assets/ATTRIBUTION.md.
Showcase photographs are public-domain works, used via Wikimedia Commons: Ansel Adams, The Tetons and the Snake River (1942, U.S. National Archives); Dorothea Lange, Migrant Mother (1936, Library of Congress); Bill Anders / NASA, Earthrise (1968); NASA, The Blue Marble (1972). The demo library is procedurally generated by LightCraft. UI font: Inter (SIL OFL 1.1). Builds made with craft-fonts (all official releases) also embed its Chinese and Japanese fonts (Noto Sans CJK SC, BIZ UDPGothic, BIZ UDMincho, Shippori Mincho; SIL OFL 1.1), listed in its ATTRIBUTION.md. All icons are original.
The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
part of this repository and LightCraft, under docs/brand/LICENSE-brand.txt.
Forks and modified versions must remove them.
Made by the ArtCraft team and community.
Star history
Library, Tools, and Presets panels can be regrouped, split, floated within the app window, closed, and reopened. Tools keeps the current Edit/Crop/Masking mode; the photo view and filmstrip stay together. Drag a panel header to a target edge or another header. Its context menu offers Float panel, Move to group, and Close panel. Existing panel toggles reopen panels at their saved location; Reset Panel Layout restores the app layout. Floating panels remain inside the current app window.








