# Conflicts:
#	crates/engine/src/export.rs
#	crates/engine/src/tests_export.rs
#	crates/mcp/src/headless.rs
#	crates/mcp/src/tools.rs
#	crates/ui-egui/locales/de-formats.json
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es-formats.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fr-formats.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/ja-formats.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br-formats.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru-formats.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/uk-formats.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/zh-hans-formats.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant-formats.json
#	crates/ui-egui/locales/zh-hant.json
#	docs/parity-checklist.md
#	docs/whats-new.md
This commit is contained in:
Claude committed 2026-10-11 12:43:20 +00:00
commit 9978dc739d
42 files changed
+2762 -58

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
# Add to Apple Photos (macOS)
An export can hand the files it wrote to Apple Photos, optionally into an album (issue #236, parity row
`LR-EXP-PHOTOS`). macOS only: on other platforms the option is hidden in the Export dialog and refused by
`app.export`. The web build doesn't include it.
> **Tested end to end** on macOS 26.6.2 with Photos 11.0, using the ad-hoc-signed packaged app and a new, empty
> Photos library: the [manual test](#manual-test) below, except the two `lightcraft-cli` steps (11 and 12). Please
> run it again after changing the script or the packaging.
## Using it
- **Export dialog:** tick **Add to Apple Photos** under *If file exists* and, if you like, type an **Album**
name (empty = no album; see [Which album](#which-album)). The files are exported as usual, into the folder
you chose, and then added to Photos. The toast says how many Photos took, or why it took none.
- **Export with Previous** and saved export presets repeat it (the setting is part of the export options).
- **Agents and scripts:** `app.export {…, addToPhotos: true, photosAlbum?: "Name"}` (desktop app, MCP `export`
tool, `lightcraft-cli run`); `lightcraft-cli render in.dng -o out.jpg --opt addToPhotos=true --opt
photosAlbum=Name`. The export's result carries `applePhotos` (below). The files are written either way: a
failed Photos step doesn't fail `app.export` (its `applePhotos` has the `error`), while `lightcraft-cli render`
says the file was written and exits with the error.
- **Files already on disk:** `export.addToPhotos {paths: [absolute paths], album?, wait?}`.
- **How an import went:** `export.photosImports` lists this session's imports, oldest first;
`export.photosImports {job}` gives one.
### Waiting, or not
An import can take minutes, and the first one waits for the user to answer macOS's permission prompt. Each
import is a job: `{job, running, requested, album}`, plus, once Photos is done, `{imported, ids, warning?}`,
`{error}` or `{skipped}` (`exporting: true` while an export that will add its files is still writing them).
While Photos works, the activity stack ([background-tasks.md](background-tasks.md)) shows it. An export adding its
files keeps its own row, which reads "Adding to Apple Photos…" and loses its ✕, since Photos can't be stopped once
asked. An import of its own (`export.addToPhotos`, or the Photos step left running after `app.export` without
`background`) gets a row of kind `applePhotos`, "Adding to Apple Photos", with the album as its detail, no ✕ and no
count, as Photos reports none until it is done.
- In the **desktop app** nothing waits for Photos on the window's thread. The Export dialog's background export
adds the files on its worker and reports them in its toast. `app.export` without `background` and
`export.addToPhotos` answer at once with the running job (`running: true`); the app shows a toast when Photos
is done, and `export.photosImports {job}` gives the outcome. (`export.addToPhotos {wait: true}` is refused there:
the window's thread never waits for Photos.)
- **Without a window** (the MCP server, `lightcraft-cli`, the headless snapshot) everything waits for Photos by
default: `app.export` and `export.addToPhotos` return the finished job, so a one-shot command never ends in
the middle of an import. `export.addToPhotos {wait: false}` returns the running job instead (for a
long-running MCP server that polls `export.photosImports`); a process that is about to end (the CLI after
its last command, the MCP server at end of input, a snapshot script, also when they end with an error)
first waits for such an import to finish. The wait follows the import: while an export is still writing the
files it will add, up to an hour more of writing; once Photos is asked, the import's own time limit (see the
table below) and half a minute; never more than three hours and one minute in all.
- **One import at a time.** An export that will add to Photos reserves the import before it writes anything
and holds it until it hands its files over (or is cancelled, fails or writes nothing). While an import is
reserved or running, another (`export.addToPhotos`, or an export asking for Photos, refused before it writes
anything) is refused with the number of the one in the way, so a retry never sends the same files twice and
an export is never turned away after writing its files. When LightCraft stops waiting (see the table below),
Photos may still be importing: look in Photos before trying again.
### Which album
The album is looked for by its exact name (case included) among the albums at the **top level** of the
Photos library. Albums inside folders are never used: when the only album of that name is in a folder, a new
top-level album is made. When none is there, a new top-level album is made. When **more than one** top-level
album has the name, nothing is imported and LightCraft says so: rename one of them in Photos, or use another
name. (Photos' own list of albums includes those in folders, in no particular order, so the script checks
each one's parent folder.)
Photos either copies the files into its library or references them where they are, as set in Photos ▸
Settings ▸ General ▸ *Copy items to the Photos library*. When it references them, keep the exported files.
## How it works
LightCraft runs `/usr/bin/osascript` with a constant AppleScript (`SCRIPT` in
`crates/engine/src/apple_photos.rs`) that asks Photos, through its scripting interface, to import the files:
`import … into album … skip check duplicates false`. The album is found or made first ([Which
album](#which-album)). The script returns the new media items' ids. Files go to Photos 200 at a time (each path is an osascript argument, and
macOS limits a program's arguments to 1 MiB), one run after another within the import's time limit; the first
run makes the album when it is missing.
This is Apple Events to Photos' own import command. It is not the OS-level synthetic input that AGENTS.md
forbids: nothing is typed or clicked, and no other window is touched.
The album name and the file paths are never written into the script. They follow it as separate arguments
(`on run argv`), each passed to osascript unchanged, so quotes, backslashes, line breaks or a leading `-` in a
name or path can't change what the script does. A constant first argument keeps osascript from reading a
name that starts with `-` as one of its own options.
The first time, macOS asks whether LightCraft may control Photos. The answer is kept in System Settings ▸
Privacy & Security ▸ Automation. macOS asks about the app that started LightCraft's work: the LightCraft app
when it was opened from the Finder or with `open`, but the **terminal** for `lightcraft-cli` and for a
LightCraft started from a terminal (`cargo run`).
**Packaged app.** The release app is signed with the hardened runtime. For it to be allowed to ask, it carries
the `com.apple.security.automation.apple-events` entitlement (`packaging/macos/entitlements.plist`, also used
for `lightcraft-cli`), and its `Info.plist` has the `NSAppleEventsUsageDescription` text macOS shows in the
prompt (`packaging/macos/Info.plist.in`). Without them macOS refuses the events (`-1743`) without asking.
`cargo run` doesn't test this configuration; the manual test below uses the packaged app.
**Who can add what.** `export.addToPhotos {paths}` adds any file LightCraft can read, given its absolute path,
not only files it exported. So any client of the control port or the MCP server can put any readable image or
video on the Mac into the user's Photos library, and from there into iCloud Photos when that is on. macOS asks
for the Automation permission once per app, not per import, so after the first Allow nothing asks again. The
control port listens on 127.0.0.1 only and has no authentication ([control-protocol.md](control-protocol.md)),
so any program on this Mac that can connect to it can do this, including programs of other users logged in to
the Mac. The MCP server talks only to the program that started it (stdio). Treat giving an agent the MCP server
or the control port as giving it this too, and turn the control port on only when you need it.
## When it doesn't work
| What you see | Why, and what to do |
|---|---|
| "LightCraft isn't allowed to control Photos…" | Permission refused (`-1743`). Turn on Photos under LightCraft (or your terminal app) in System Settings ▸ Privacy & Security ▸ Automation, then try again. |
| "Apple Photos couldn't be found…" | Photos isn't installed or couldn't be opened (`-600`, `-10810`, `-10814`, or no `/System/Applications/Photos.app`). |
| "The import was cancelled in Photos." | Someone pressed Cancel in a Photos dialog (`-128`). |
| "More than one album at the top level of Photos is named …" | Several top-level albums have that name; nothing was added. Rename one in Photos, or use another name. |
| "Photos didn't finish adding the photos within N s…" / "didn't answer in time" | LightCraft waits ten minutes plus five seconds a file (at most two hours), then stops osascript. The ten minutes leave time to answer macOS's permission prompt and Photos' duplicates question. Photos may still be importing: look in Photos before trying again. |
| "Apple Photos is still adding N file(s) from an earlier request (import K)…" / "An export is about to add its files to Apple Photos (import K)…" | One import at a time: wait for import K (`export.photosImports {job: K}`), then try again. |
| "Photos added 2 of 3 files…" | A partial import: Photos skipped files it found to be duplicates (it may ask first) or couldn't read. |
| "Apple Photos is only available on macOS" | The setting came from a Mac (a preset or Export with Previous); the Export dialog drops it on other platforms. |
## Manual test
Run this once on a Mac whose Photos library you are happy to add a few test images to (or switch Photos to a
new empty library first: hold ⌥ while opening Photos ▸ Create New).
1. Package the app (ad-hoc signed unless `MACOS_SIGN_IDENTITY` is set): `packaging/macos/package.sh --arch
aarch64` (or `x86_64` on an Intel Mac). Check its entitlement: `codesign -d --entitlements -
target/macos-package/LightCraft.app` lists `com.apple.security.automation.apple-events`. With a Developer ID
build, also run the notarized DMG from `dist/release/`.
2. Open it from the Finder, or with `open target/macos-package/LightCraft.app --args --demo` (not by running
the binary in a terminal, which makes the terminal the app macOS asks about).
3. Select three photos, choose File ▸ Export…, set a folder (e.g. `~/Pictures/LightCraft Exports`), tick **Add
to Apple Photos**, type the album `LightCraft test "1"` and press OK.
4. macOS asks whether **LightCraft** may control Photos, showing the usage text from `Info.plist`: allow it.
Expected: the toast reads "Exported 3 of 3 photos · 3 added to Apple Photos album “LightCraft test "1"”",
and a top-level album of that name holds the three images.
5. Export the same photos again with the same settings. Expected: Photos asks about duplicates or imports them
again; the toast reports the count Photos returned (a partial import shows its warning).
6. Albums: in Photos make a folder holding an album `Nested`, then export into `Nested`. Expected: a new
top-level album `Nested` gets the images, the one in the folder doesn't. Then make a second top-level album
`Nested` and export again. Expected: nothing is added, and the toast says more than one album has the name.
7. Permission refused: in System Settings ▸ Privacy & Security ▸ Automation turn off Photos under LightCraft,
export again. Expected: the files are written, and the toast says to allow LightCraft there. Turn it back
on.
8. No album: keep Add to Apple Photos ticked, clear the Album field and export. Expected: the images land in the
library, in no album.
9. Hostile names: album `-e`, then `a\b`, and an export folder whose name contains `"` and `'`; a name with a
line break through the CLI: `lightcraft-cli run --demo app.export dir=/tmp/lc-photos addToPhotos=true
"photosAlbum=$(printf 'two\nlines')"`. Expected: albums named exactly so; nothing else happens.
10. The window stays responsive: start the app with `open target/macos-package/LightCraft.app --args --demo
--control 18999`, then send `{"method": "engine.execute", "params": {"command": "export.addToPhotos",
"params": {"paths": ["/tmp/lc-photos/<file>.jpg"]}}}` to port 18999 (`docs/control-protocol.md`).
Expected: an immediate `{"job": …, "running": true}`; the app keeps responding; a toast when Photos is done;
`export.photosImports` then shows the outcome.
11. Agents: `lightcraft-cli run --demo app.export dir=/tmp/lc-photos addToPhotos=true photosAlbum=CLI` (allow
the terminal under Automation when asked). Expected: `applePhotos.imported` in the printed result.
12. Files on disk: `lightcraft-cli run export.addToPhotos 'paths=["/tmp/lc-photos/<file>.jpg"]' album=CLI`.
Expected: the command waits for Photos and prints `{"job": …, "running": false, "imported": 1, …}`.
Report the toast or JSON, and the macOS and Photos versions.
## Limitations
- Albums inside folders are never used ([Which album](#which-album)).
- Photos' duplicate check stays on, so Photos may ask about duplicates; what it skips shows as a partial import.
- Cancelling a running export cancels the Photos step too; a Photos import already started can't be stopped
from LightCraft.
- One import runs (or is reserved by an export) at a time; a session remembers its last 32.
- Quitting the desktop app while Photos is importing doesn't wait for it: Photos may still finish the import,
but LightCraft won't show the outcome.
+18
View File
@@ -255,6 +255,7 @@ lightcraft-cli run --demo --script steps.jsonl --keep-going
lightcraft-cli render in.dng -o out.tif --opt colorSpace=displayP3 --opt bitDepth=16 --opt percent=50
lightcraft-cli render in.dng -o out.jpg --set light.exposure=0.5 --set light.contrast=20 --size 2048
lightcraft-cli render in.jpg -o out.png --settings look.json --preset <presetId>
lightcraft-cli render in.dng -o out.jpg --opt addToPhotos=true --opt photosAlbum=Trip # macOS: then into Apple Photos
lightcraft-cli commands [--json] # the command registry
lightcraft-cli controls [--json] # develop control ids and ranges
lightcraft-cli calibrate --max 300 ~/Pictures/2026 # camera colour profiles (docs/camera-preview-colour.md)
@@ -268,6 +269,23 @@ lightcraft-cli calibrate --max 300 ~/Pictures/2026 # camera colour profiles (d
- `apps/lightcraft-cli/tests/cli.rs` — spawns `lightcraft-cli mcp` with real pipes; `render`;
`commands`.
## Add to Apple Photos (macOS)
`app.export` (MCP `export` tool, `lightcraft-cli run`, the desktop app) takes `addToPhotos: true` and
`photosAlbum` to hand the files it wrote to Apple Photos, into that album (a top-level album of exactly that
name, made when missing). The result's `applePhotos` is the import job: `{job, running, requested, album}`
plus `{imported, ids, warning?}`, `{error}` or `{skipped}` once Photos is done; the files are written either
way. Headless (this server, `lightcraft-cli`) waits for Photos by default; the desktop app (connect mode)
answers with the running job and `export.photosImports {job}` gives the outcome. `export.addToPhotos {paths,
album?, wait?}` does the same for files already on disk (`wait` defaults to true headless, false in the desktop
app, which refuses `wait: true`; with `wait: false` headless, the server waits for the import before it exits at end of input). One import
runs at a time, and an export adding to Photos reserves it before it writes: another is refused, naming the
job in the way. On other platforms `addToPhotos: true` is refused before anything is written, and
`export.addToPhotos` is disabled with the reason. The first time, macOS asks the user to allow LightCraft (or
the terminal running `lightcraft-cli`) to control Photos. After that, any client can add any file LightCraft can
read to the user's Photos library (and so to iCloud Photos when that is on), not only exported ones: see [Who
can add what](apple-photos.md#how-it-works).
## Export progress and cancellation
Headless direct `export`, `command_run` / `run_command` with `app.export`, and `cmd_app_export`
+2 -2
View File
@@ -44,7 +44,7 @@ cited here still exists. Formerly docs/parity.md.
| P. Enhance (ENH) | 0 | 0 | 2 | 0 | — | — |
| Q. HDR (HDR) | 2 | 3 | 0 | 0 | — | — |
| R. Video (VID) | 0 | 0 | 5 | 1 | — | 0/2 (0%) |
| S. Export (EXP) | 15 | 2 | 1 | 0 | 7/7 (100%) | 7/7 (100%) |
| S. Export (EXP) | 15 | 3 | 0 | 0 | 7/7 (100%) | 7/7 (100%) |
| T. Share (SHARE) | 0 | 0 | 0 | 4 | — | — |
| U. Map & location (MAP) | 0 | 1 | 1 | 0 | — | 0/1 (0%) |
| V. Preferences (PREF) | 6 | 2 | 2 | 2 | 1/1 (100%) | 4/5 (80%) |
@@ -399,7 +399,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| LR-EXP-PREVIOUS | Export with previous settings | P0 | ✅ | `cmd:app.exportPrevious`, `cmd:dialog.export` | last options persist in prefs.json; dialog prefilled; no named export presets yet |
| LR-EXP-DNGOPT | DNG options | P2 | 🟡 | `cmd:app.export` (`dngCompression`), `crates/engine/src/export.rs` | compression: lossless JPEG (default), ZIP or none; no embedded JPEG preview size, no lossy DNG output |
| LR-EXP-ORIGINAL | Original + XMP | P1 | ✅ | `cmd:app.export` (`format: original`), `crates/engine/src/tests_export.rs` | file copied byte for byte, sidecar named after the output and subject to the conflict policy |
| LR-EXP-PHOTOS | Export to the system photo library | P2 | ⬜ | | |
| LR-EXP-PHOTOS | Export to the system photo library | P2 | 🟡 | `cmd:app.export` (`addToPhotos`, `photosAlbum`), `cmd:export.addToPhotos`, `cmd:export.photosImports`, `crates/engine/src/apple_photos.rs`, `crates/ui-egui/src/panels/dialogs.rs`, `packaging/macos/entitlements.plist`, `docs/apple-photos.md` | macOS: the exported files go to Apple Photos through Apple Events to its import command (a constant AppleScript run by `/usr/bin/osascript`; album names and paths are separate arguments), into a top-level album of exactly that name, made when missing (several of that name: refused); the Export dialog's Add to Apple Photos, Export with Previous, presets, MCP and `lightcraft-cli` all reach it; imports are jobs, off the UI thread in the desktop app and awaited in one-shot headless runs, one at a time, reserved by an export before it writes; permission, missing Photos, timeouts and partial imports are reported; the packaged app carries the Apple Events entitlement and usage text. Tested end to end with the packaged app on macOS 26.6.2 / Photos 11.0 (manual steps in `docs/apple-photos.md`). Other platforms' photo libraries are not covered |
| LR-EXP-PSD | Round trip to an external editor | P2 | ✅ | `cmd:photo.editExternal`, `cmd:photo.editInPhotoCraft`, `cmd:photo.editInExternal`, `crates/engine/src/cmd/convert.rs`, `apps/lightcraft/src/editors.rs` | an `-Edit` copy of each selected photo (up to 50 at once, issue #920) with the edits (PSD or TIFF, 16- or 8-bit, Adobe RGB / ProPhoto / P3 / sRGB, chosen per editor in Settings ▸ General ▸ External Editing) next to the original, added stacked on top of it and opened in PhotoCraft (found where its installers put it; a PSD by default, which PhotoCraft saves back to in place) or the other editor set there (or the system default); also on the photo's right-click menu (Edit In). Re-read when LightCraft is focused again. No "Edit a copy / Edit original" choice for non-raw files yet |
## T. Share (SHARE)
+9
View File
@@ -101,6 +101,15 @@
Appearance lists the modes and themes. On Linux, Auto follows the desktop portal's colour scheme
as it changes, without polling.
### Add to Apple Photos (macOS)
- The Export dialog has **Add to Apple Photos**, with an optional album. The files are exported to your folder as
usual and then added to Photos, into a top-level album of that name (made when it's missing). The toast says how
many Photos added, or why it added none. Export with Previous and export presets repeat it.
- The first time, macOS asks whether LightCraft may control Photos. Photos may ask about duplicates; files it skips
show as a partial import.
- Agents: `app.export {addToPhotos, photosAlbum}`, `export.addToPhotos {paths}` for files already on disk, and
`export.photosImports`. See [Add to Apple Photos](apple-photos.md).
### Activity stack
- Long-running tasks show in one place, top left under the top bar, as in Lightroom Classic (issue #345): imports and
folder scans, exports (and the contact sheet PDF), Synchronize Folder, Build / Smart Previews, Lightroom catalog import, Photo Merge, model downloads, the face scan,