diff --git a/crates/engine/src/tests/libops.rs b/crates/engine/src/tests/libops.rs index b35167c1b..05eb5dbb8 100644 --- a/crates/engine/src/tests/libops.rs +++ b/crates/engine/src/tests/libops.rs @@ -467,6 +467,41 @@ fn tracklog_auto_tag() { let _ = std::fs::remove_dir_all(&dir); } +#[test] +fn tracklog_auto_tag_decodes_character_references_and_undoes_once() { + use lightcraft_catalog::Op; + + const PLAIN: &str = r#" + + + "#; + const ENCODED: &str = r#" + + + "#; + + let mut s = Session::with_demo(); + let Some(id) = s.visible_cloned().first().copied() else { + panic!("the demo library has a photo"); + }; + s.commit("test capture time", Op::SetCaptured { id, captured: Some("2026-01-01T00:01:00".into()) }).unwrap(); + let original_gps = s.catalog.photo(id).unwrap().meta.gps; + let params = |gpx: &str, dry_run| json!({"gpx": gpx, "ids": [id.0], "dryRun": dry_run}); + + let plain = s.execute("photo.autoTagTracklog", ¶ms(PLAIN, true)).unwrap(); + let encoded = s.execute("photo.autoTagTracklog", ¶ms(ENCODED, true)).unwrap(); + assert_eq!(plain, encoded); + assert_eq!(encoded["photos"][0]["gps"], json!([46.0, 8.0])); + assert_eq!(s.catalog.photo(id).unwrap().meta.gps, original_gps, "dry run leaves GPS unchanged"); + + let undo_depth = s.undo.len(); + s.execute("photo.autoTagTracklog", ¶ms(ENCODED, false)).unwrap(); + assert_eq!(s.undo.len(), undo_depth + 1); + assert_eq!(s.catalog.photo(id).unwrap().meta.gps, Some((46.0, 8.0))); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.catalog.photo(id).unwrap().meta.gps, original_gps); +} + // ---- random sort: Given the library, When the user picks Random, Then the grid is a stable shuffle #[test] diff --git a/crates/meta/src/gpx.rs b/crates/meta/src/gpx.rs index 204e39116..817d59d65 100644 --- a/crates/meta/src/gpx.rs +++ b/crates/meta/src/gpx.rs @@ -118,10 +118,15 @@ pub fn parse_gpx(s: &str) -> Result { b"trkpt" if parent == Some(b"trkseg") => { let mut p = Pending::default(); for a in e.attributes().with_checks(false).flatten() { - let v = String::from_utf8_lossy(&a.value); match local(a.key.as_ref()) { - b"lat" => p.lat = v.trim().parse().ok(), - b"lon" => p.lon = v.trim().parse().ok(), + b"lat" => { + let v = a.decode_and_unescape_value(reader.decoder()).map_err(|e| GpxError::Xml(e.to_string()))?; + p.lat = v.trim().parse().ok(); + } + b"lon" => { + let v = a.decode_and_unescape_value(reader.decoder()).map_err(|e| GpxError::Xml(e.to_string()))?; + p.lon = v.trim().parse().ok(); + } _ => {} } } @@ -141,6 +146,16 @@ pub fn parse_gpx(s: &str) -> Result { } Event::Text(t) => text.push_str(&t.decode().map_err(|e| GpxError::Xml(e.to_string()))?), Event::CData(t) => text.push_str(&String::from_utf8_lossy(&t)), + Event::GeneralRef(r) => { + if let Some(c) = r.resolve_char_ref().map_err(|e| GpxError::Xml(e.to_string()))? { + text.push(c); + } else { + let name = r.decode().map_err(|e| GpxError::Xml(e.to_string()))?; + let entity = format!("&{name};"); + let value = quick_xml::escape::unescape(&entity).map_err(|e| GpxError::Xml(e.to_string()))?; + text.push_str(value.as_ref()); + } + } Event::End(_) => { let Some(name) = path.pop() else { continue }; let parent = path.last().map(Vec::as_slice); @@ -351,6 +366,60 @@ mod tests { assert_eq!(log.points[1].time - log.points[0].time, 60.0); } + fn point_document(lat: &str, lon: &str, ele: &str, time: &str) -> String { + format!( + r#"{ele}"# + ) + } + + #[test] + fn character_reference_elevation_is_not_truncated() { + let log = parse_gpx(&point_document("45", "7", "123", "2026-01-01T00:00:00Z")).unwrap(); + assert_eq!(log.points.first().and_then(|p| p.elevation), Some(123.0)); + } + + #[test] + fn character_reference_latitude_is_not_dropped() { + let log = parse_gpx(&point_document("45", "7.5", "123", "2026-01-01T00:00:00Z")).unwrap(); + let point = log.points.first().copied(); + assert_eq!(log.points.len(), 1); + assert_eq!(point.map(|p| (p.latitude, p.longitude)), Some((45.0, 7.5))); + } + + #[test] + fn character_reference_time_is_not_shifted() { + let encoded = parse_gpx(&point_document("45", "7", "123", "2026-01-01T00:00:30Z")).unwrap(); + let plain = parse_gpx(&point_document("45", "7", "123", "2026-01-01T00:00:30Z")).unwrap(); + assert_eq!(encoded.points.first().map(|p| p.time), plain.points.first().map(|p| p.time)); + assert_eq!(encoded.points.first().map(|p| p.time), Some(1_767_225_630.0)); + } + + #[test] + fn predefined_entities_are_decoded_once_and_cdata_stays_literal() { + let with_predefined_entity = point_document("45", "7", "1&23", "2026-01-01T00:00:00Z"); + let log = parse_gpx(&with_predefined_entity).unwrap(); + assert_eq!(log.points.first().and_then(|p| p.elevation), None); + + let cdata = point_document("45", "7", "", "2026-01-01T00:00:00Z"); + let log = parse_gpx(&cdata).unwrap(); + assert_eq!(log.points.first().and_then(|p| p.elevation), None); + } + + #[test] + fn unknown_and_malformed_character_references_are_rejected() { + let unknown = point_document("45", "7", "&undeclared;", "2026-01-01T00:00:00Z"); + assert!(matches!(parse_gpx(&unknown), Err(GpxError::Xml(_)))); + + let malformed = point_document("45", "7", "&#xZZ;", "2026-01-01T00:00:00Z"); + assert!(matches!(parse_gpx(&malformed), Err(GpxError::Xml(_)))); + + let unknown_attribute = point_document("4&undeclared;", "7", "123", "2026-01-01T00:00:00Z"); + assert!(matches!(parse_gpx(&unknown_attribute), Err(GpxError::Xml(_)))); + + let malformed_attribute = point_document("&#xZZ;", "7", "123", "2026-01-01T00:00:00Z"); + assert!(matches!(parse_gpx(&malformed_attribute), Err(GpxError::Xml(_)))); + } + #[test] fn non_finite_elevation_is_dropped() { for ele in ["inf", "-inf", "NaN", "infinity"] { diff --git a/docs/gaps.md b/docs/gaps.md index 671f3089c..8c56024a4 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps: where LightCraft falls short of Lightroom -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (gap 8: lens profile work is tracked in #329; gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (LR-MAP-TRACKLOG decodes GPX character references in coordinates, elevations and timestamps; malformed references error; CDATA stays literal; ranked gaps unchanged; gap 8: lens profile work is tracked in #329; gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF) Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is @@ -65,6 +65,10 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w - 2026-10-22: PSD/PSB imports and metadata readers now expose embedded EXIF resource 1058 and XMP resource 1060 for capture dates, GPS and All Metadata (#2519); imported PSD edit copies retain their capture date, and the `removeLocation` Original-export guard sees embedded PSD/PSB GPS. No ranked gap or headline feature count closes. +- 2026-10-22: GPX track points decode XML character references in coordinate attributes and + element text before parsing. Malformed or undeclared references return an error; CDATA stays + literal. Track positions and timestamps no longer silently change or lose points. + - 2026-10-20: the import review album picker follows the sidebar folder hierarchy and searches album paths (#2362, LR-IMP-ADD-DIALOG), retaining None/New album and existing destination IDs. This closes a usability subgap; no ranked gap or headline feature count closes. - 2026-10-12: original-pixel detail at 100% (gap 4, LR-VIEW-ZOOM, #323): soft proofing gets zoom windows like @@ -95,6 +99,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w | 2026-10-22 | patch | XMP namespace-prefix collisions no longer overwrite foreign metadata; ranked gaps and estimates unchanged | | 2026-10-22 | patch | Issue #2519: PSD/PSB EXIF resource 1058 and XMP resource 1060 now use the common metadata extractor, and `.psb` files are accepted by import; no ranked gap or readiness count changes | | 2026-10-22 | minor | Gap 30 added: multi-select metadata editing — Info panel and All Metadata dialog show one photo's values with no mixed-value indication | +| 2026-10-22 | patch | LR-MAP-TRACKLOG: GPX coordinate attributes and point text decode XML references; malformed / unknown references return errors; CDATA remains literal; ranked gaps unchanged | | 2026-10-21 | minor | Gap 28 shrinks: F8 shows / hides the right panel (`view.rightPanel`); rating `[` `]`, flag cycling and F1 help were already bound, so the entry lists only filter-bar keys, Tab / ⇧Tab and F5; 2–4 h (was 4–8) | | 2026-10-20 | minor | #2362: import destination albums use the shared hierarchy/search picker instead of a flat name list; existing import workflows retained; ranked gaps/readiness counts unchanged | | 2026-10-19 | minor | Gap 9 shrinks: `.github/workflows/ci.yml` runs `cargo xtask ci` on Linux x64 for every PR and push to main; macOS and Windows remain | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index e002d0a7c..46594ed1e 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-28 · **Change:** minor (LR-BEHAV-ACCESS: the Thumbnail size control is a slider to screen readers; LR-BEHAV-ACCESS: the Detail photo is named with its file name; LR-VIEW-PHOTOGRID: the grid's empty-view message names its shortcut as this platform writes it and as it is bound; LR-BEHAV-ACCESS: the grid's date headers are named as they read; LR-BEHAV-ACCESS: the sidebar's disclosure triangles name their row; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌘⌥⇧K Set Keyword Shortcut and ⇧K; KEYC-METADATA: ⌘⇧K / Ctrl+Shift+K Edit Keywords; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-28 · **Change:** minor (LR-MAP-TRACKLOG decodes GPX character references in point fields; malformed references error; CDATA stays literal; LR-BEHAV-ACCESS: the Thumbnail size control is a slider to screen readers; LR-BEHAV-ACCESS: the Detail photo is named with its file name; LR-VIEW-PHOTOGRID: the grid's empty-view message names its shortcut as this platform writes it and as it is bound; LR-BEHAV-ACCESS: the grid's date headers are named as they read; LR-BEHAV-ACCESS: the sidebar's disclosure triangles name their row; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌘⌥⇧K Set Keyword Shortcut and ⇧K; KEYC-METADATA: ⌘⇧K / Ctrl+Shift+K Edit Keywords; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -737,7 +737,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06- | LRC-MAP-VIEW | Map view | P2 | ⬜ | | | | LRC-MAP-GEOTAG | Drag photos onto the map | P2 | ⬜ | | | | LRC-MAP-LOCATIONS | Saved locations | P2 | ⬜ | | | -| LRC-MAP-TRACKLOG | GPS track logs | P2 | ✅ | `cmd:photo.autoTagTracklog`, `cmd:photo.tagFromTracklog`, `crates/meta/src/gpx.rs` | Photo ▸ Auto-Tag from Tracklog…: a GPX 1.0 / 1.1 track log sets the GPS of the selected photos by capture time — interpolated between the points of a track segment, else the nearest point within `maxGap` (10 min); never across segment breaks. The camera's time zone comes from the photo (Exif offset) or is asked for; photos that already have a location keep it unless `replace`; `dryRun` previews; one undo step. No track drawn on a map (no Map module) | +| LRC-MAP-TRACKLOG | GPS track logs | P2 | ✅ | `cmd:photo.autoTagTracklog`, `cmd:photo.tagFromTracklog`, `crates/meta/src/gpx.rs` | Photo ▸ Auto-Tag from Tracklog…: a GPX 1.0 / 1.1 track log sets the GPS of the selected photos by capture time — interpolated between the points of a track segment, else the nearest point within `maxGap` (10 min); never across segment breaks. XML character references in coordinate attributes and point text decode before parsing; malformed or unknown references return an error; CDATA stays literal. The camera's time zone comes from the photo (Exif offset) or is asked for; photos that already have a location keep it unless `replace`; `dryRun` previews; one undo step. No track drawn on a map (no Map module) | | LRC-MAP-FILTER | Location filter bar | P2 | ⬜ | | | | LRC-MAP-REVGEO | Reverse geocoding | P2 | 🟡 | `cmd:metadata.reverseGeocode` | offline from GeoNames (City / State / Country, no sublocation); the list holds towns of 15 000 people or more, so a smaller town gets the nearest larger one (the tooltip and command help say so); `maxDistance` leaves City empty past a distance and names the town as `nearestCity` (issue #2242); no online provider | | LRC-BOOK-SETTINGS | Book settings | P2 | ⬜ | | | @@ -819,6 +819,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-22 | patch | LR-META-XMP: namespace URI determines ownership; foreign namespaces using recognized prefixes survive saves and cannot supply native settings or ratings; status and counts unchanged | | 2026-10-22 | patch | LR-EXP-PSD, LR-META-EXIF-FULL: PSD/PSB imports and image resources 1058/1060 now supply EXIF and XMP to metadata readers and All Metadata; Original with Remove Location Info refuses detected PSD/PSB GPS; statuses and feature counts unchanged | | 2026-10-21 | patch | LR-LIB-CAPTURETIME: a capture time that doesn't fully read is left as it is and counted (`unreadable`) instead of shifted into a wrong one; it can't anchor a set that moves other photos; status unchanged | +| 2026-10-22 | patch | LR-MAP-TRACKLOG: character references in GPX coordinate attributes and point text decode correctly; malformed / unknown references fail cleanly; CDATA stays literal; command dry-run, tagging and undo covered | | 2026-10-21 | patch | LR-IMP-ADD-DIALOG: reconcile #2362 with current main; deleted-album guard and hierarchical picker retained; counts unchanged | | 2026-10-21 | patch | KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the slider's row; leaving it (even between two frames) or Tab focus alone does not arm them (issue #1355); status unchanged | | 2026-10-21 | minor | KEYC-PANELS: F8 shows / hides the right panel (`view.rightPanel`), with F6 / F7 already bound; ⬜ → 🟡 | diff --git a/docs/whats-new.md b/docs/whats-new.md index febd7f770..3bb8c129e 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -144,6 +144,12 @@ - Photo Grid and Square Grid now show the grid-cycle shortcut (G by default) in the View menu. The hints follow shortcut changes; clicking a menu row still selects that grid (issue #2500). +### GPX track logs read escaped values correctly +- Coordinates, elevations and times in a GPX track can contain XML character references such as + `5`. LightCraft now decodes those characters before parsing values, reports malformed or + undeclared references as an error, and keeps CDATA literal. Auto-Tag places photos at the track + position represented by their capture time. + ## 2026-10-21 ### Settings window