Photo Date Editor

Version 0.4.0

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
  • In-place overwrite: no browser download and no _original file
  • 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+Tab between 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_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, 0.2, or 0.3

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

  1. The browser reads the selected local JPEG.
  2. It sends the file and entered fields to /api/process.
  3. ExifTool modifies a temporary file inside the container using -overwrite_original.
  4. The backend returns the modified JPEG.
  5. The browser writes the returned bytes over the selected original file with createWritable().
  6. 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

  • Enter or Ctrl+Enter: Save & Next
  • Ctrl+S: Save the current photo and remain on it
  • Left / Right: Previous / next photo when not typing
  • S: Mark the photo as skipped and open the next photo when not typing
  • C: Copy previous values when not typing
  • Shift+Tab: Switch to the previous right-side metadata tab; normal Tab still advances through fields

Location behavior

The Location tab supports four ways to set GPS metadata:

  1. Search for an address or place and choose a result.
  2. Click directly on the map.
  3. Drag the existing marker.
  4. 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.

Leaflet is downloaded into the container during the Docker build. The default map tiles and address search are external OpenStreetMap services. Search is user-triggered rather than autocomplete, proxied through the backend, rate-limited to one uncached request per second, and cached in memory. Both the tile and geocoder URLs are 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 default map and address search require internet access. Opening the map sends tile requests for the viewed area; address searches are sent to the configured geocoder.
  • 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

  1. PNG — direct browser preview and in-place EXIF/XMP/IPTC metadata writing.
  2. TIFF/TIF — in-place metadata writing, with a server-generated preview where the browser cannot display the file directly.
  3. 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.
  4. DNG — in-place metadata writing and preview extraction/rendering.
  5. Canon RAWCR2 and CR3, using per-format tag rules and embedded/server-generated previews.
  6. Nikon RAWNEF and NRW, 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.

S
Description
No description provided
Readme
709 KiB
Languages
JavaScript 38.7%
Python 36.9%
CSS 11.7%
HTML 11.5%
Dockerfile 1.2%