Capture date and place, with a map picker - #905
Merged
Merged
Conversation
The Metadata panel authored the gear and the process but not when or where the frame was shot, and the export inherited the scan file's DateTimeOriginal, so a 1998 negative sorted by its 2026 scan date. A Capture card adds both. The date is a truncated ISO-8601 string, because a film date is usually partial: 1998, 1998-07, a full date, or a date and time with an optional offset. EXIF pads the parts the user left out, XMP photoshop:DateCreated keeps the truncated form, and negpy:CaptureDatePrecision says which it was. The scan's own timestamp moves to DateTimeDigitized, which is what it recorded. The place is a WGS-84 position plus city, state and country, picked in a map dialog: a hand-rolled OpenStreetMap tile widget (paintEvent plus urllib, no new dependency and no browser in the bundle), Nominatim search and reverse lookup, and a field that also takes a pasted coordinate pair or map link. Everything works with no network except the map itself. Coordinates go to the EXIF GPS IFD and XMP exif:GPS*, the names to photoshop:City/State/Country. Setting a place now clears the source GPS block whole, so a picked position cannot ship with the scan's altitude or heading beside it. The source position is read back too: it shows in the preview as Scan place and frames the picker's opening view, but is never adopted as the capture place, because where a frame was digitized is not where it was shot. Both facts are searchable as shot: and place:. shot: is ordered by the existing prefix comparison, so shot:>=1998-07 needs no date parsing, and a year-only date deliberately does not match a month bound. Dropping GPS from the TIFF extratags loop fixes a latent bug: a source GPS IFD was written as top-level TIFF tags 1-4, which are not TIFF tags. TIFF now carries the location in XMP, which is what DAMs read. Closes #899
The search hits filled a list under the field, which took layout space from the map and needed a button press to appear. They are now a QCompleter dropdown on the search field, filled from the geocoder and shown unfiltered, because filtering the server's hits again locally would hide "Tokio" for "tokyo". A 500 ms timer holds the request until typing pauses, and a query under three characters never searches, which keeps the app inside Nominatim's one-request-a-second policy. Enter still searches at once. The lookups also ask for the user's language now: without it Nominatim answers in the local script, and that name goes into XMP.
marcinz606
marked this pull request as ready for review
August 19, 2026 15:59
It applies to every field in the panel, not to scanning, and it was folded away in a collapsed card. It now sits under Protect original metadata, where the other panel-wide switch is, and greys out with the rest when Protect is on, which makes the panel's fields moot anyway. The sync path itself needed nothing: all three call sites hand over the active frame's MetadataConfig whole, so the capture date and place ride along with the gear. A test pins that, since a future narrowing to a field list would silently drop whatever was added last.
"Map…" and "Clear" took most of the Place row, leaving the field it belongs to too narrow to read a coordinate pair. Both are _icon_action buttons now, the sidebar's helper for one-shot icon buttons, with the label moved into the tooltip.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #899.
The Metadata panel authored the gear and the process, but not when or where the frame was shot. Worse, the export inherited the scan file's
DateTimeOriginal, so a 1998 negative sorted in Lightroom by its 2026 scan date.Capture date
A truncated ISO-8601 string, not a date widget: a film date is usually partial, and
QDateTimeEditwould invent a day.1998·1998-07·1998-07-14·1998-07-14 16:30· with an optional+02:00. The string's length is the precision. An impossible date turns the field red and is not saved.DateTimeOriginalpads the missing parts (plusOffsetTimeOriginalwhen given).photoshop:DateCreatedkeeps the truncated form;negpy:CaptureDatePrecisionrecords which it was, so a round-trip knows1998was a year and not January 1st.DateTimeDigitized— which is what it recorded — and tonegpy:ScanDateTime.{{ capture_date }}and{{ capture_year }};{{ date }}is still the export date.Capture place
A WGS-84 position plus city, state and country, set in a modal picker.
slippy_map.py— a pan/zoom OpenStreetMap tile view:paintEventplusurllibon aQThreadPool, disk tile cache undercache/map_tiles, identifying User-Agent, ODbL attribution painted, a missing tile paints flat and nothing blocks. No new dependency; QtWebEngine would have put a browser in the PyInstaller bundle for one dialog.location_picker_dialog.py— Nominatim search, reverse lookup on each pin drop, editable city/state/country (the geocoder proposes, the user decides), and a coordinate field that takes a pasted pair or an OSM/Google Maps link. Every path except the tiles themselves works offline.exif:GPS*for the position,photoshop:City/State/Countryfor the names.Closing the picker used to hang the app. A
QThreadPoolowned by a widget joins its queue from the C++ destructor with the GIL held, which a PythonQRunnableneeds to return — a permanent freeze with nothing in the log. Fixed by joining explicitly (waitForDonereleases the GIL), astoppedflag checked atrun()entry, and a cap on pending tiles. Measured: a stubbed 3 s tile fetch turned a hang into a 3.00 s close; against live OSM, 0.06 s.Source GPS
Search
shot:andplace:join the existing facets, in the one parser both the filter box and the library search use.shot:is ordered by the existing prefix comparison, soshot:>=1998-07andshot:<2000need no date parsing, and a year-only date deliberately does not match a month bound.place:tokyoandplace:japanhit the same frame.Fixed in passing
_exif_bytes_to_extratagsiterated("0th", "Exif", "GPS")and emitted GPS tags as top-level TIFF tags 1–4, which are not TIFF tags. Already wrong for any geotagged source; writing our own GPS would have made it routine. TIFF now carries the location in XMP, which is what DAMs read anyway.Verification
make all— 4481 passed, lint, format and ty clean.New tests cover the date table (accept, reject, pad, truncate), coordinate and map-link parsing, GPS DMS round-trip through both writer and reader, tile math, the writer's date demotion and GPS strip (and byte-identical behaviour when the date is unset), the picker and map widget offscreen with every request patched, the sidebar's Capture card, the config JSON round-trip, and the two search facets.
Live-checked against OpenStreetMap: tiles load, Nominatim returns hits. Not run: a manual GUI pass, and the
exiftoolread-back —exiftoolis not installed here, so the tags are asserted withpiexifinstead.