mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Merge PR #643
# 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:
42 files changed
+2762
-58
No files matched your search
@@ -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
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user