Photo Date Editor
Version 0.9.2
A self-hosted browser UI for manually dating scanned photographs. The browser receives temporary read/write access to a local computer or Chromebook folder, sends one image at a time to the Docker backend for ExifTool processing, and overwrites the same local file after processing.
Current scope
- JPG/JPEG, PNG, TIFF/TIF, HEIC/HEIF/HIF, DNG, Canon CR2/CR3, Nikon NEF/NRW, and Sony ARW/ARQ
- Local directory picker in supported Chromium browsers
- Three-column photo workflow
- Exact date, month/year, year-only, approximate year
- Optional time; unknown time defaults to noon
- Description and keywords
- GPS geotagging with address search, map click, draggable pin, manual coordinates, and location removal
- Shared favorite locations persisted in a Docker volume for all family members and browsers
- In-place overwrite: no browser download and no
_originalfile - Saved/skipped/pending/failed state stored in browser local storage
- Filename search, status filtering, and filename/status/format sorting
- Normalized JPG, PNG, TIFF, HEIF, DNG, CR2, CR3, NEF, NRW, ARW, and ARQ format badges in the photo list
- Segmented progress bar: saved is green, skipped is yellow, failed is red, and pending remains grey
- Existing metadata is read from supported files when selected
- Files previously edited by this app are recognized from their XMP marker, even in a different browser
- Keyboard navigation, including
Shift+Tabbetween right-side tabs
Preview behavior
JPG and PNG are displayed directly by the browser. Chromium browsers do not reliably display TIFF, HEIF-family images, DNG, CR2, CR3, NEF, NRW, ARW, or ARQ, so those formats are temporarily uploaded to /api/inspect, which returns both metadata and a browser-friendly JPEG preview from one upload. The response uses a compact length-prefixed binary envelope rather than Base64 JSON. TIFF uses Pillow, HEIF uses pillow-heif, and RAW formats stop at the first usable embedded preview in a container-specific priority order. If none is usable, LibRaw renders a half-size preview through rawpy. The preview exists only in memory and is never written over the source file.
When a server-preview format is saved, ExifTool updates the original container and the browser overwrites the same local file through the streamed save path. The original is not converted to JPEG and no additional copy is intentionally left behind.
Requirements
- Docker with Docker Compose
- A current Chromium browser with the File System Access API enabled, such as Chrome, Edge, or Brave with the relevant flag enabled
- HTTPS when accessed from another device
- A backup of irreplaceable scans before testing any metadata editor
The browser File System Access API is required. Firefox and Safari are not supported by this version.
Install
cp .env.example .env
nano .env
docker compose up -d --build
Example .env:
APP_URL=https://photos.example.internal
APP_PORT=8080
APP_NAME=Photo Date Editor
APP_TITLE=Mork Photo Date Editor
APP_SUBTITLE=Family photo metadata editor
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon,nikon,sony
MAX_UPLOAD_MB=150
LOG_LEVEL=INFO
# Optional TIFF/HEIF/RAW preview limits
PREVIEW_MAX_EDGE=2400
RAW_PREVIEW_MAX_EDGE=1600
PREVIEW_MAX_PIXELS=300000000
# Optional map defaults and service overrides
MAP_TILE_SOURCE_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png
MAP_ATTRIBUTION=© OpenStreetMap contributors
DEFAULT_MAP_LAT=64.5
DEFAULT_MAP_LON=11.0
DEFAULT_MAP_ZOOM=5
GEOCODER_URL=https://nominatim.openstreetmap.org/search
Open the configured HTTPS URL through Caddy, click Open photo folder, and grant read/write access.
Enabled formats
ENABLED_FORMATS accepts a comma-separated list of format groups:
.env group |
Formats enabled | Preview and metadata behavior |
|---|---|---|
jpg |
JPG/JPEG: .jpg, .jpeg |
Browser-native preview; EXIF, XMP, and IPTC |
png |
PNG: .png |
Browser-native preview; EXIF and XMP |
tiff |
TIFF: .tif, .tiff |
Server JPEG preview; EXIF, XMP, and IPTC |
heif |
HEIF family: .heic, .heif, .hif |
Lazy HEIF decoder; EXIF and XMP |
dng |
Adobe DNG: .dng |
Embedded preview or lazy LibRaw; EXIF, XMP, and IPTC |
canon |
Canon RAW: .cr2, .cr3 |
Embedded preview or lazy LibRaw; CR2 uses EXIF/XMP/IPTC, CR3 uses EXIF/XMP |
nikon |
Nikon RAW: .nef, .nrw |
Embedded preview or lazy LibRaw; EXIF, XMP, and IPTC |
sony |
Sony RAW: .arw, .arq |
Embedded preview or lazy LibRaw; EXIF, XMP, and IPTC |
The default enables every group:
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon,nikon,sony
For a JPEG-only installation:
ENABLED_FORMATS=jpg
Disabled formats are omitted from folder scans and rejected by the backend. The page subtitle and empty-folder message reflect the enabled groups. Unknown group names stop application startup with a clear configuration error instead of silently enabling or disabling the wrong format.
HEIF and RAW decoders load lazily. A JPEG-only process does not import them. After the first applicable preview, the decoder remains loaded until the container restarts; repeatedly unloading it would add avoidable delay and memory churn. For DNG, Canon, Nikon, and Sony RAW, LibRaw is loaded only if no usable embedded JPEG preview is available.
Upgrade from earlier releases
Replace the project files with this version and rebuild:
docker compose up -d --build
The existing .env can be kept. When ENABLED_FORMATS is omitted, all current format groups remain enabled. APP_TITLE and APP_SUBTITLE are optional. When APP_TITLE is omitted, the visible page title continues to use APP_NAME; when APP_SUBTITLE is omitted, it is generated from the enabled formats. Browser progress from earlier releases remains compatible. A normal refresh should load the new service-worker cache; use a hard refresh if an old UI remains visible.
Caddy example
Caddy may run in another container or host. Proxy to the Docker host and exposed port:
photos.example.internal {
@lan remote_ip private_ranges
handle @lan {
reverse_proxy 192.168.1.20:8080
}
respond 403
}
The browser directory picker requires a secure context. Use a certificate trusted by the client computer. A real domain with internal DNS is usually easier than deploying a private CA certificate to every family device.
What happens when Save is clicked
- The browser reads the selected local file in any enabled format.
- It sends the file and entered fields to
/api/process. - ExifTool modifies a temporary file inside the container using
-overwrite_original. - The backend returns the modified file using its original media type.
- The browser writes the returned bytes over the selected original file with
createWritable(). - The backend temporary directory is deleted automatically.
No second photo is intentionally left on the client computer or Docker host.
RAW saves still require the complete original to travel to Docker and the complete rewritten file to return to the browser. ExifTool must also rewrite the metadata-bearing RAW container. The optimized opening path removes duplicate uploads, but the full save round trip is inherent while the folder belongs to the browser rather than the Docker host.
During a save, the backend streams the processed file in 256 KiB chunks and the browser writes each chunk directly into its protected temporary writable file. The UI shows transferred bytes and percentage. The original is replaced only after the complete stream closes successfully; failures abort the temporary writable file. Browsers without response streaming use the previous buffered Blob workflow.
After a streamed save, the status panel and popup show total time, time to first byte, and combined stream/write time. Container logs also show end-to-end request duration and break inspection work into temporary-copy, preview, and metadata stages.
Metadata behavior by format
- JPG/JPEG: EXIF, XMP, and IPTC fields are written.
- TIFF/TIF: EXIF, XMP, and IPTC fields are written.
- PNG: EXIF and XMP fields are written. PNG metadata support varies more between third-party viewers than JPEG/TIFF support, so test the applications that will consume the files.
- HEIC/HEIF/HIF: EXIF and XMP fields are written. IPTC-IIM is not used for these ISO Base Media File Format containers. Date precision, description, keywords, GPS coordinates, and the location label remain represented through EXIF/XMP fields.
- DNG: EXIF, XMP, and IPTC fields are written into the original TIFF-based DNG container. Raw sensor data and embedded previews are not regenerated or replaced.
- CR2: EXIF, XMP, and IPTC fields are written into the original TIFF-based Canon RAW container.
- CR3: EXIF and XMP fields are written into the original ISO Base Media File Format container. IPTC-IIM is not used for CR3.
- NEF/NRW: EXIF, XMP, and IPTC fields are written into the original TIFF-based Nikon RAW container.
- ARW/ARQ: EXIF, XMP, and IPTC fields are written into the original TIFF-based Sony RAW container.
The app writes its precision marker to XMP for all supported formats. GPS coordinates are written to EXIF and XMP. The optional location label is written to XMP IPTC Core Location.
Date behavior
EXIF date fields require a complete timestamp:
| Selected precision | EXIF value | XMP DateCreated |
|---|---|---|
| Exact date | Chosen date | YYYY-MM-DD |
| Month and year | First day of month | YYYY-MM |
| Year only | January 1 at noon | YYYY |
| Approximate year | January 1 at noon | YYYY |
The chosen precision is also written into XMP Photoshop Instructions.
Keyboard shortcuts
EnterorCtrl+Enter: Save & NextCtrl+S: Save the current photo and remain on itLeft/Right: Previous / next photo when not typingS: Mark the photo as skipped and open the next photo when not typingC: Copy previous values when not typingShift+Tab: Open the previous right-side metadata tab and place focus in its first field; normalTabstill advances through fields
Location behavior
Favorite locations are loaded automatically when the page opens and are shared by everyone using the same Docker stack. Choose a saved place and click Use, or place a pin/set coordinates and click Add current location in the favorite-locations card. The favorite list is stored in the named Docker volume photo-date-editor-data, so it survives image rebuilds and container recreation. Deleting a favorite only removes it from the reusable list; it does not alter photos that were already geotagged.
The Location tab supports five ways to set GPS metadata:
- Choose a shared favorite location.
- Search for an address or place and choose a result.
- Click directly on the map.
- Drag the existing marker.
- Enter decimal latitude and longitude manually.
The default map tiles and address search are external OpenStreetMap services. Both are requested by the Docker backend rather than directly by the browser. Tiles are cached in container memory. Search is user-triggered, rate-limited, and cached in memory. The tile source and geocoder URLs remain configurable in .env.
Important limitations
- The app cannot silently access a local folder. The user must choose it and approve read/write access.
- Browser permission may need to be granted again after closing/reopening the browser.
- Saved and skipped progress is stored per browser and folder name. The app also reads its own XMP marker from edited files so saved metadata can be recognized on another browser.
- Overwriting a file through the browser generally changes its filesystem modified time to the time of the save. EXIF/XMP photo dates are independent of that filesystem timestamp.
- Preview rotations are physically applied when saving JPG, PNG, and TIFF files. Orientation metadata is normalized after the pixels are rotated.
- JPEG rotation uses lossless
jpegtranwhen the dimensions permit a perfect transform. Otherwise it performs one high-quality re-encode and reports that in the save notification. - PNG rotation is lossless. Animated PNG files are deliberately rejected rather than silently dropping frames.
- TIFF rotation preserves and rotates all pages, uses lossless TIFF compression, and restores metadata before applying the edited fields.
- HEIF, DNG, CR2, CR3, NEF, NRW, ARW, and ARQ rotation remains preview-only. Their pixels and orientation metadata are never changed by the rotation controls.
- Multi-page TIFF files display the first page/frame in the editor. Metadata is written to the TIFF container, not to separate pages.
- HEIF image sequences display their primary/first image in the editor. Metadata is written to the HEIF container.
- DNG, CR2, CR3, NEF, NRW, ARW, and ARQ use the first usable embedded JPEG in a format-specific priority order before falling back to a half-size LibRaw render.
RAW_PREVIEW_MAX_EDGEdefaults to 1600 pixels to keep temporary previews responsive. It never resizes the RAW original.- Very large TIFF, HEIF, or RAW files may require raising
MAX_UPLOAD_MBorPREVIEW_MAX_PIXELS. - RAW rendering depends on the camera and compression variant being supported by the bundled LibRaw version. Metadata editing can still be supported by ExifTool even when a particular RAW file cannot be rendered.
- Canon, Nikon, and Sony maker notes, sensor data, embedded previews, and camera-specific metadata are preserved rather than intentionally rewritten. Always validate with copies from the camera models in your archive.
- ExifTool documents that some Sony Imaging Edge versions may reject ARW/ARQ files after ExifTool edits even though Adobe, Apple, Capture One, Affinity Photo, and LibRaw-based software can open them. Test Sony copies in the exact software you intend to use.
- HEIF metadata compatibility varies between operating-system galleries and photo-management applications. Verify the fields in the software that will consume your library.
- The Docker host needs outbound HTTPS access for the default map tiles and address search.
- Test with copies first, then use your normal backup routine for the originals.
Planned format expansion
Format support needs both safe metadata writing and a browser-friendly preview. Formats that browsers do not reliably display will use a temporary server-generated preview without converting or replacing the original file.
Remaining considerations
Older Sony SR2 remains a possible compatibility addition. SRF is read-only in ExifTool and remains excluded.
RAW is not treated as one universal format. Each camera family will be enabled and tested explicitly.
Not planned
- DWG — CAD drawing format rather than a scanned-photo format.
- BMP/DIB — ExifTool can read it but cannot write the metadata this app needs.
- SVG, PDF, EPS, WebP, AVIF, EXR, XCF — intentionally outside the scope of this photo workflow.
Future video support
MP4 remains feasible after the planned photo formats, but it will be a separate media phase rather than just another image extension. The existing fields can be mapped to compatible QuickTime and XMP metadata, but large videos need streamed/chunked transfer, progress reporting, larger limits, cancellation, and compatibility testing before in-place support is enabled.