Photo Date Editor
Version 0.4.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 JPEG at a time to the Docker backend for ExifTool processing, and overwrites the same local file after processing.
Current scope
- JPG and JPEG
- 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 sorting
- Segmented progress bar: saved is green, skipped is yellow, failed is red, and pending remains grey
- Existing metadata is read from the JPEG when a photo is 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
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 first 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
MAX_UPLOAD_MB=150
LOG_LEVEL=INFO
# 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.
Upgrade from 0.1 through 0.4.1
Replace the project files with this version and rebuild:
docker compose up -d --build
The existing .env can be kept. Browser progress from 0.1 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 JPEG.
- 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 JPEG.
- 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.
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 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.
On save, coordinates are written to EXIF and XMP GPS fields. The optional location label is written to XMP IPTC Core Location. Use Clear location and save to remove GPS coordinates and the app-managed location label.
The default map tiles and address search are external OpenStreetMap services. Both are requested by the Docker backend rather than directly by the browser. This avoids Brave third-party blocking and keeps all browser requests same-origin through Caddy. Tiles are cached in container memory. Search is user-triggered rather than autocomplete, rate-limited to one uncached request per second, and cached in memory. The tile source and geocoder URLs remain configurable in .env for later self-hosting or another provider.
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 rotation is visual only in this MVP; it does not rotate image pixels or write orientation metadata.
- The Docker host needs outbound HTTPS access for the default map tiles and address search. Browser requests remain same-origin through the app.
- Test with copies first, then use your own normal backup routine for the originals.
Planned format expansion
Format support needs two separate capabilities: safe metadata writing and a preview the browser can display. ExifTool gives the backend a strong metadata foundation, while formats that browsers do not reliably preview will use a temporary server-generated preview without converting or replacing the original file.
Proposed order
- PNG — direct browser preview and in-place EXIF/XMP/IPTC metadata writing.
- TIFF/TIF — in-place metadata writing, with a server-generated preview where the browser cannot display the file directly.
- HEIC/HEIF/HIF — in-place EXIF/XMP writing and a server-generated preview. This phase should use a recent ExifTool release and test normal, HDR, and motion-photo samples.
- DNG — in-place metadata writing and preview extraction/rendering.
- Canon RAW —
CR2andCR3, using per-format tag rules and embedded/server-generated previews. - Nikon RAW —
NEFandNRW, using per-format tag rules and embedded/server-generated previews.
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.
RAW support will remain in-place by default to match the JPEG workflow. Before enabling it, the app needs format-specific round-trip tests and clear warnings because proprietary camera originals deserve a stricter safety bar than scans.
Future video support
MP4 is feasible after the planned photo formats, but it will be a separate media phase rather than just another image extension. Browsers can preview MP4 directly, while ExifTool can write selected QuickTime/MP4 metadata such as creation dates, descriptive fields, and static GPS coordinates. Videos do not use the same EXIF model as JPEGs, so the app will map the existing fields to compatible QuickTime and XMP tags.
The current browser-to-Docker-to-browser workflow transfers the complete file for every save. That is acceptable for photos but inefficient for large videos. Before enabling MP4, the app should add streamed/chunked transfer, larger configurable upload limits, clear progress reporting, and round-trip tests against common players and photo libraries. MP4 support is planned after PNG, TIFF, HEIC/HEIF, DNG, Canon RAW, and Nikon RAW.