LR-META-XMP: keep saved Upright in the photo's frame, put sidecar crops there too (#205)

Review fixes for the saved Upright import.

- The saved matrix is placed when the sidecar is merged, in the frame the file's orientation and
  then the photo's own Rotate Left / Right give (Orientation::then). Read Metadata after a
  rotation no longer keeps it in the old frame (up to 21 px off on DSC_1457). Rotate Left / Right
  turns a stored Upright correction with the photo (upright::turned), LightCraft's own as well.
- Sidecar crops go through the Lightroom catalog import's conversion (catalog_crop, moved to
  crs::crop_in_frame unchanged) whenever the file's orientation is known. A portrait raw's crop
  used to cut the wrong side (DSC_1486 rendered 2548 x 4812 for Lightroom's 3200 x 3808; now
  0.12 px mean from Lightroom's export after the NEF frame inset), and straightened crops from a
  real catalog's settings came out 8459 x 3827 for Lightroom's 7725 x 5150. A straightened crop
  on top of a saved Upright isn't measured: the mode is solved again, with the warning.
- Lightroom catalogs don't read the saved Upright until a real catalog verifies its encoding and
  frame: the mode is solved again and the import reports it.
- Guided Upright guides are reported whether or not automatic matrices are present.

The new regressions fail on the previous commit and pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alex ChambersandClaude Opus 5.5 committed 2026-10-12 14:12:15 +02:00
1 parent aaf3094c8d
commit 7bb11f4604
15 files changed
+428 -130

No files matched your search

+4 -2
View File
@@ -42,8 +42,10 @@ WITHOUT ROWID or virtual table produces an explicit error, handled as a warning
Develop settings reuse the existing XMP/preset mapper. Supported sliders, curves and supported
mask structures remain editable, but this is approximate rendering: camera profiles, Adobe AI
models, some masking/retouch fields and process-version algorithms are not reproduced. Unmapped
fields are reported. Lightroom's saved Upright correction is kept in the stored image's frame, like the crop (see
[xmp-interop.md](xmp-interop.md), *Saved Upright geometry*). Lightroom's `ProcessVersion` is not mapped: newly imported photos are on
fields are reported. Lightroom's saved Upright correction isn't read from catalogs yet: how a catalog stores it, and
whether the catalog's orientation agrees with the decoded file, haven't been checked on a real catalog, so the mode
is solved again and the import reports `Upright` (XMP sidecars keep it: [xmp-interop.md](xmp-interop.md), *Saved
Upright geometry*). Lightroom's `ProcessVersion` is not mapped: newly imported photos are on
LightCraft's latest rendering process, and photos already in the library keep theirs
([process-versions.md](process-versions.md)). When a recovery archive is saved, it retains source settings/history/snapshots
and original smart-collection rules. Smart collections become regular albums with current
+3 -3
View File
@@ -167,7 +167,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| LR-META-COPYRIGHT-DEFAULT | Default copyright on import | P1 | ✅ | `cmd:library.preferences` (`import.copyright`, `import.creator`), `crates/ui-egui/src/panels/settings.rs` | Settings → Import → Metadata; fills only photos without their own |
| LR-META-LOCATION | Location editing | P2 | 🟡 | `cmd:photo.setMeta` (`location`, `city`, `state`, `country`, `gps`), `cmd:metadata.reverseGeocode` | place fields and GPS (decimal or degrees / minutes / seconds) edited in Info, read/written as IPTC Core / Photoshop / EXIF XMP; Show on Map opens OpenStreetMap; Fill Location fills empty City / State / Country from GPS offline (GeoNames, downloaded on request: `cmd:metadata.downloadPlaceNames`); no embedded map |
| LR-META-COPYPASTE | Copy / paste metadata | P2 | ✅ | `cmd:photo.copyMetadata`, `cmd:photo.pasteMetadata` | title, caption, alt text, extended description, copyright (notice, status, usage terms, info URL), creator, place fields, keywords; `fields` picks a subset |
| LR-META-XMP | XMP read/write | P0 | ✅ | `cmd:photo.saveMetadataToFile`, `cmd:photo.readMetadataFromFile`, `cmd:library.xmpPreferences`, `crates/engine/src/sidecar.rs`, `docs/xmp-interop.md` | sidecar wins for metadata; a sidecar capture time (`exif:DateTimeOriginal` / `photoshop:DateCreated` / `xmp:CreateDate`) fills in only when the file has none; saving merges into an existing sidecar (`crates/meta/src/xmp_merge.rs`): another app's `crs:`, `xmpMM:History` and unknown namespaces are kept byte for byte, an unreadable sidecar is backed up first; with stem naming, files sharing a stem (raw + JPEG) get separate sidecars (`<file>.xmp` for all but the raw); a `crs:` custom white balance on a relative-WB raw is shifted from `crs:AsShotTemperature` rather than read as Kelvin, or kept As Shot and reported when the packet has no as-shot white (issue #510); heal and clone spots (`crs:RetouchAreas`, legacy `crs:RetouchInfo`) are read into our spots (`crates/engine/src/crs_masks.rs`, see `docs/xmp-interop.md` → Spot removal); Lightroom's saved Upright correction for the selected mode (`crs:UprightTransform_*`) is kept instead of being solved again, matching three Lightroom Upright Auto exports to under 1 px (issue #205, `crates/engine/src/tests_upright.rs`, see `docs/xmp-interop.md` → Saved Upright geometry), and `crs:PerspectiveUpright` 2, 3, 4 read as Full, Level, Vertical |
| LR-META-XMP | XMP read/write | P0 | ✅ | `cmd:photo.saveMetadataToFile`, `cmd:photo.readMetadataFromFile`, `cmd:library.xmpPreferences`, `crates/engine/src/sidecar.rs`, `docs/xmp-interop.md` | sidecar wins for metadata; a sidecar capture time (`exif:DateTimeOriginal` / `photoshop:DateCreated` / `xmp:CreateDate`) fills in only when the file has none; saving merges into an existing sidecar (`crates/meta/src/xmp_merge.rs`): another app's `crs:`, `xmpMM:History` and unknown namespaces are kept byte for byte, an unreadable sidecar is backed up first; with stem naming, files sharing a stem (raw + JPEG) get separate sidecars (`<file>.xmp` for all but the raw); a `crs:` custom white balance on a relative-WB raw is shifted from `crs:AsShotTemperature` rather than read as Kelvin, or kept As Shot and reported when the packet has no as-shot white (issue #510); heal and clone spots (`crs:RetouchAreas`, legacy `crs:RetouchInfo`) are read into our spots (`crates/engine/src/crs_masks.rs`, see `docs/xmp-interop.md` → Spot removal); Lightroom's saved Upright correction for the selected mode (`crs:UprightTransform_*`) is kept from XMP instead of being solved again, matching three Lightroom Upright Auto exports to under 1 px (issue #205, `crates/engine/src/tests_upright.rs`, see `docs/xmp-interop.md` → Saved Upright geometry; not yet from Lightroom catalogs), `crs:PerspectiveUpright` 2, 3, 4 read as Full, Level, Vertical, and sidecar crops go through the file's orientation like catalog crops (portrait raws were cut on the wrong side) |
| LR-META-EXIF-FULL | Full EXIF/IPTC [Classic] | P1 | ✅ | `cmd:photo.allMetadata`, `cmd:dialog.allMetadata`, `crates/meta/src/tags.rs`, `crates/meta/src/exif.rs`, `crates/meta/src/iptc.rs` | read and written on export; Info ▸ All Metadata…: every TIFF / EXIF / GPS / interop tag (named, common values spelled out) and the XMP fields, searchable; metadata presets |
## F. Edit panel — global adjustments (EDIT)
@@ -215,7 +215,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
)
| LR-EDIT-OPTICS-DEFRINGE | Defringe | P1 | ✅ | `ctl:optics.defringe*` | no fringe eyedropper |
| LR-EDIT-OPTICS-MANUAL | Manual distortion / vignetting | P1 | ✅ | `ctl:optics.distortion`, `ctl:optics.vignetting`, `ctl:optics.vignettingMidpoint` | |
| LR-EDIT-GEOM-UPRIGHT | Upright | P1 | ✅ | `cmd:geometry.upright`, `cmd:geometry.guides` | LightCraft's own solver; Lightroom's saved Upright from XMP and catalogs is kept as it is (LR-META-XMP) |
| LR-EDIT-GEOM-UPRIGHT | Upright | P1 | ✅ | `cmd:geometry.upright`, `cmd:geometry.guides` | LightCraft's own solver; Lightroom's saved Upright from XMP is kept as it is (LR-META-XMP); Rotate Left / Right turns a stored correction with the photo |
| LR-EDIT-GEOM-MANUAL | Manual transform | P1 | ✅ | `ctl:geometry.*` | |
| LR-EDIT-GEOM-CONSTRAIN | Constrain crop | P1 | ✅ | `crates/ui-egui/src/panels/right.rs` (checkbox → `cmd:develop.merge`) | |
| LR-EDIT-GEOM-GRID | Grid while transforming | P2 | ✅ | `crates/ui-egui/src/panels/detail.rs` | a fine grid over the photo while a geometry slider is dragged |
@@ -787,7 +787,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | LR-META-XMP: Lightroom's saved Upright correction for the selected mode is kept from XMP and catalogs instead of being solved again (issue #205); `PerspectiveUpright` 2, 3, 4 read as Full, Level, Vertical; statuses unchanged |
| 2026-10-12 | minor | LR-META-XMP: Lightroom's saved Upright correction for the selected mode is kept from XMP instead of being solved again (issue #205; catalogs fall back and report it); `PerspectiveUpright` 2, 3, 4 read as Full, Level, Vertical; sidecar crops follow the file's orientation; LR-EDIT-GEOM-UPRIGHT: Rotate turns a stored correction; statuses unchanged |
| 2026-10-11 | minor | KEY-SLIDER-TYPE: honor immediate Return/Escape and keep typed keys in the value editor; add regression references |
| 2026-10-11 | minor | LR-PROF-CAMERACOLOR: raws with a black-and-white camera JPEG take its tone curve (issue #577) |
| 2026-10-11 | minor | LR-VIEW-ZOOM and LR-VIEW-COMPARE: Compare and Reference are sharp at 1:1 (zoom windows per pane, two originals kept decoded while they are open); statuses unchanged |
+2 -2
View File
@@ -157,7 +157,7 @@ essentials replaces its guessed 45% raw look with 84% × 0.75 (+1 point).
|---|---:|---:|---|
| Interaction fidelity / UI clarity | ×0.95 | ×0.95 | brush live preview gaps and feathering (#517, #518); crop artefacts and limits (#731, #742, #770, #771); Compare white-balance selector (#743–#745); low-res slider previews (#330); dialogs taller than the window (#781); mouse-wheel scroll (#714); 100% zoom capped (gap 4) |
| Stability on real machines | ×0.92 | ×0.92 | launch and quit crashes or silent startup failures (#250 macOS 12 Intel, #315 Windows Intel GPU, #431 Intel Mac, #260, #462, #783 Linux / Nouveau, #790 Windows installer); tests run in CI on FreeBSD only (gap 9) |
| File exchange with Lightroom users (mainstream) / opening files people send (essentials) | ×0.92 | ×0.95 | Adobe XMP sidecars change colours (#290), XMP CameraProfile ignored (#204), Guided Upright from XMP not carried over (#205), catalog import fails (#599), same Light values render differently (#523, #209), no write-back to `.lrcat`; for casual users: CR3 beyond verified bodies, compressed ORF, HE NEF open as previews (#473, #485, #556), HEIC trouble (#693) |
| File exchange with Lightroom users (mainstream) / opening files people send (essentials) | ×0.92 | ×0.95 | Adobe XMP sidecars change colours (#290), XMP CameraProfile ignored (#204), Guided Upright and catalog Upright not carried over (#205), catalog import fails (#599), same Light values render differently (#523, #209), no write-back to `.lrcat`; for casual users: CR3 beyond verified bodies, compressed ORF, HE NEF open as previews (#473, #485, #556), HEIC trouble (#693) |
| **Product** | **×0.804** | **×0.830** | |
### Mainstream practitioner: ~49%
@@ -297,7 +297,7 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h.
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | patch | File-exchange evidence: Lightroom's saved Upright correction is kept from XMP and catalogs (#205); only Guided Upright isn't carried over; the ×0.92 / ×0.95 discount and all numbers are unchanged |
| 2026-10-12 | patch | File-exchange evidence: Lightroom's saved Upright correction is kept from XMP sidecars (#205); Guided Upright and catalogs aren't carried over yet; the ×0.92 / ×0.95 discount and all numbers are unchanged |
| 2026-10-11 | minor | EOS R5 Mark II CR3 verified (raw-parity.md): use-weighted share whose camera decodes 84% → 85%, RAW decoding 76% → 77%; full 60.6% → 60.7%, mainstream depth 71.8% → 72.1%, essentials depth 77.4% → 77.5%; the rounded numbers (~61%, ~49%, ~64%) are unchanged |
| 2026-10-10 | minor | RAW measured model by model (raw-parity.md): RAW decoding (76%) and colour (35%) are explicit rows of the full sum (59% → 61%); mainstream uses the use-weighted 84% plus a ×0.85 colour discount (51% → 49%); essentials uses 84% × colour 0.75 (63% → 64%); beta ~14 points |
| 2026-10-10 | minor | Full number restored to an additive weighted sum over the dimensions (~59%; method aligned with the standard, no new evidence); hours to ~95% for each of the three audiences; beta distance ~16 points |
+6 -2
View File
@@ -5,8 +5,12 @@
### Lightroom Upright comes across
- Photos edited in Lightroom with Upright Auto, Full, Level or Vertical now open framed as they are in Lightroom
(issue #205). LightCraft keeps the correction Lightroom saved with the edit instead of analysing the photo again.
This works for XMP sidecars, DNG files with the edit inside and Lightroom Classic catalogs. Photos imported
earlier get it with Photo ▸ Read Metadata from File.
This works for XMP sidecars and DNG files with the edit inside; photos imported earlier get it with Photo ▸ Read
Metadata from File. Lightroom Classic catalogs still have it worked out again for now, and the import says so.
- Rotate Left and Rotate Right turn an Upright correction with the photo. It used to stay as it was, so the photo
came out skewed after a turn.
- A crop read from an XMP sidecar now lands where Lightroom put it on photos shown on their side (portrait raws),
and straightened crops keep Lightroom's frame. A portrait crop used to cut the wrong side of the photo.
- Lightroom's Upright values 2, 3 and 4 now read as Full, Level and Vertical. They used to come in as Level,
Vertical and Full.
- Guided Upright doesn't come across yet (Lightroom's guides aren't read), and the import report says so. The
+9 -7
View File
@@ -141,7 +141,7 @@ application's renderer and is ignored: the photo keeps LightCraft's own process,
| `ShadowTint`, `RedHue/Saturation`, `GreenHue/Saturation`, `BlueHue/Saturation` | `calibration.shadows_tint`, `calibration.red_hue/red_sat`, … | Calibration panel, 1:1 |
| `PerspectiveVertical/Horizontal/Rotate/Scale/Aspect/X/Y` | `geometry.vertical/horizontal/rotate/scale/aspect/offset_x/offset_y` | |
| `PerspectiveUpright` | `geometry.upright`, `geometry.upright_transform` | 0 off, 1 auto, 2 full, 3 level, 4 vertical, 5 guided; Lightroom's saved correction for the selected mode is kept as it is when it can be (see *Saved Upright geometry*), otherwise LightCraft works the mode out itself (Guided needs guides drawn in LightCraft: Lightroom's aren't read) and photo import says so |
| `HasCrop`, `CropLeft/Top/Right/Bottom`, `CropAngle` | `crop.geometry` | normalized edges → rect; angle in degrees; `HasCrop="False"` → no crop |
| `HasCrop`, `CropLeft/Top/Right/Bottom`, `CropAngle` | `crop.geometry` | Lightroom's edges are on the stored image (before its orientation) and its straighten angle turns the other way: both go into the frame the photo is shown in (through the file's orientation and the photo's Rotate Left / Right), as the Lightroom catalog import does; when the file's orientation isn't known (Synchronize Folder, a raw shown from its preview) the edges are taken as written; `HasCrop="False"` → no crop |
Values pass through our control specs, so anything outside our slider ranges gets clamped.
@@ -158,23 +158,25 @@ photo is framed as in Lightroom instead of being analysed again, when
- `UprightVersion` is `151388160` (the only version seen so far; any other is solved again);
- `UprightTransformCount` includes the mode's matrix, and the matrix is nine finite numbers that make a sensible
correction (no part of the photo at or beyond the horizon, no mirror, no collapse);
- the file's orientation is known: on import, Read Metadata from File and Lightroom catalog import (Synchronize
Folder solves the mode again).
- the file's orientation is known: on import and Read Metadata from File (Synchronize Folder solves the mode again,
and Lightroom catalog import doesn't read saved Upright yet: see [lightroom-catalog-import.md](lightroom-catalog-import.md));
- the crop has no straighten angle (a straightened crop on top of a saved Upright isn't measured yet).
The matrix's convention was measured on Lightroom's exports, not assumed (issue #205): a forward homography (photo
before correction to photo after it, column vectors) in coordinates normalised to the stored image (x / width,
y / height, origin top left, y down, before the EXIF orientation), applied before the crop, with Lightroom's scale
to fill the frame already in it. LightCraft turns it with the file's orientation into its own frame
(`geometry.upright_transform`, centred coordinates of the oriented image) and applies the crop on top, as
Lightroom does. On three public NEF raws exported by Lightroom Classic 12 with Upright Auto (one shown in portrait,
(`geometry.upright_transform`, centred coordinates of the oriented image, the photo's own Rotate Left / Right
included) and applies the crop on top, as Lightroom does. Rotate Left / Right later turns the kept correction with
the photo, as it does LightCraft's own Upright. On three public NEF raws exported by Lightroom Classic 12 with Upright Auto (one shown in portrait,
one cropped with Constrain Crop on), LightCraft's geometry with the kept matrix matches the exports to about half a
pixel on average and under a pixel at worst, at full size; without it, the frames differ by 22 to 36 px on average.
Not measured yet: the Transform sliders (Vertical, Horizontal, Rotate, Aspect, Scale, X / Y Offset) together with a
saved Upright (LightCraft applies its own slider mapping on top, as with its own Upright), Level, Vertical and Full
exports (their matrices have those modes' shapes, but no export of them with its raw was available), photos with
Lightroom lens profile corrections, and an angled crop on top of Upright. Guided Upright's guides
(`UprightFourSegments_*`) aren't read.
Lightroom lens profile corrections, and a straightened crop on top of Upright (solved again, with the warning). Guided
Upright's guides (`UprightFourSegments_*`) aren't read: import says so whenever a packet has them.
**Not mapped:** camera profiles and looks (`CameraProfile`, `Look`; we have our own profile set), red eye, lens blur,
process-version 2010 field names, and AI features. Local adjustments and spot removal are read as described below.