Add Nikon and Sony RAW support v0.9

This commit is contained in:
dmo
2026-07-25 08:41:41 +02:00
parent f1704c9822
commit 6e8c30c1d4
6 changed files with 87 additions and 37 deletions
+3 -3
View File
@@ -11,9 +11,9 @@ APP_NAME=Photo Date Editor
APP_TITLE=Mork Photo Date Editor
# Optional text shown directly below the page heading.
APP_SUBTITLE=Family photo metadata editor
# Enabled groups: jpg, png, tiff, heif, dng, canon
# Enabled groups: jpg, png, tiff, heif, dng, canon, nikon, sony
# Use ENABLED_FORMATS=jpg for a JPEG-only workflow.
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon,nikon,sony
MAX_UPLOAD_MB=150
LOG_LEVEL=INFO
@@ -31,4 +31,4 @@ DEFAULT_MAP_LON=11.0
DEFAULT_MAP_ZOOM=5
GEOCODER_URL=https://nominatim.openstreetmap.org/search
# Optional custom identification sent to the geocoder. The default uses APP_NAME, version and APP_URL.
# GEOCODER_USER_AGENT=MorkPhotoDateEditor/0.8.3 (+https://edit.mork.fyi)
# GEOCODER_USER_AGENT=MorkPhotoDateEditor/0.9.0 (+https://edit.mork.fyi)
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## 0.9.0
- Add the `nikon` format group with NEF and NRW support.
- Add the `sony` format group with ARW and ARQ support.
- Add exact NEF, NRW, ARW, and ARQ badges and format-sort compatibility.
- Validate the TIFF headers used by Nikon and Sony RAW containers before processing.
- Prefer embedded JPEG previews before loading the lazy LibRaw fallback.
- Read and write EXIF, XMP, IPTC, dates, descriptions, keywords, GPS coordinates, and location labels.
- Preserve Nikon and Sony maker notes, sensor data, embedded previews, and untouched camera metadata.
- Use the established streamed save path, live transfer progress, and timing diagnostics.
- Document every `ENABLED_FORMATS` group, its extensions, and its behavior in the README.
- Keep older Sony SR2 as a future compatibility decision and exclude read-only SRF.
- Preserve all existing JPG, PNG, TIFF, HEIF, DNG, and Canon RAW behavior.
## 0.8.3
- Stream processed files from the backend in 256 KiB chunks with an explicit content length.
+28 -26
View File
@@ -1,12 +1,12 @@
# Photo Date Editor
Version 0.8.3
Version 0.9.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 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, and Canon CR2/CR3
- 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
@@ -17,7 +17,7 @@ A self-hosted browser UI for manually dating scanned photographs. The browser re
- 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/format sorting
- Normalized JPG, PNG, TIFF, HEIF, DNG, CR2, and CR3 format badges in the photo list
- 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
@@ -25,9 +25,9 @@ A self-hosted browser UI for manually dating scanned photographs. The browser re
## Preview behavior
JPG and PNG are displayed directly by the browser. Chromium browsers do not reliably display TIFF, HEIF-family images, DNG, CR2, or CR3, 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.
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 TIFF, HEIF-family image, DNG, CR2, or CR3 is saved, ExifTool updates the original format and the browser overwrites the same local file. The original is not converted to JPEG and no additional copy is intentionally left behind.
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
@@ -54,7 +54,7 @@ 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
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon,nikon,sony
MAX_UPLOAD_MB=150
LOG_LEVEL=INFO
@@ -78,19 +78,21 @@ Open the configured HTTPS URL through Caddy, click **Open photo folder**, and gr
`ENABLED_FORMATS` accepts a comma-separated list of format groups:
| Group | File extensions |
|---|---|
| `jpg` | `.jpg`, `.jpeg` |
| `png` | `.png` |
| `tiff` | `.tif`, `.tiff` |
| `heif` | `.heic`, `.heif`, `.hif` |
| `dng` | `.dng` |
| `canon` | `.cr2`, `.cr3` |
| `.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:
```dotenv
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon
ENABLED_FORMATS=jpg,png,tiff,heif,dng,canon,nikon,sony
```
For a JPEG-only installation:
@@ -101,9 +103,9 @@ 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 and Canon RAW, LibRaw is loaded only if no usable embedded JPEG preview is available.
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 0.1 through 0.7.1
## Upgrade from earlier releases
Replace the project files with this version and rebuild:
@@ -133,7 +135,7 @@ The browser directory picker requires a secure context. Use a certificate truste
## What happens when Save is clicked
1. The browser reads the selected local JPG, PNG, TIFF, HEIC, HEIF, HIF, DNG, CR2, or CR3.
1. The browser reads the selected local file in any enabled format.
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 file using its original media type.
@@ -157,6 +159,8 @@ After a streamed save, the status panel and popup show total time, time to first
- **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.
@@ -205,11 +209,12 @@ The default map tiles and address search are external OpenStreetMap services. Bo
- Preview rotation is visual only; it does not rotate image pixels or write orientation metadata.
- 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, and CR3 use the first usable embedded JPEG in a format-specific priority order before falling back to a half-size LibRaw render.
- 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_EDGE` defaults to 1600 pixels to keep temporary previews responsive. It never resizes the RAW original.
- Very large TIFF, HEIF, DNG, CR2, or CR3 files may require raising `MAX_UPLOAD_MB` or `PREVIEW_MAX_PIXELS`.
- Very large TIFF, HEIF, or RAW files may require raising `MAX_UPLOAD_MB` or `PREVIEW_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 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.
- 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](https://exiftool.org/#supported) 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.
@@ -218,12 +223,9 @@ The default map tiles and address search are external OpenStreetMap services. Bo
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.
### Proposed order
### Remaining considerations
1. **Nikon RAW**`NEF` and `NRW`, using per-format tag rules and embedded/server-generated previews.
2. **Sony RAW** — modern `ARW` and `ARQ`, using TIFF-based metadata profiles and embedded/server-generated previews.
Older Sony `SR2` and `SRF` files are recorded as a compatibility decision for the Sony phase. `SR2` is writable through ExifTool, while `SRF` is read-only, so they should not be enabled automatically with modern Sony RAW support.
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.
+36 -6
View File
@@ -32,6 +32,8 @@ FORMAT_GROUPS: dict[str, tuple[str, ...]] = {
"heif": (".heic", ".heif", ".hif"),
"dng": (".dng",),
"canon": (".cr2", ".cr3"),
"nikon": (".nef", ".nrw"),
"sony": (".arw", ".arq"),
}
FORMAT_LABELS = {
"jpg": "JPG",
@@ -40,6 +42,8 @@ FORMAT_LABELS = {
"heif": "HEIF",
"dng": "DNG",
"canon": "Canon RAW",
"nikon": "Nikon RAW",
"sony": "Sony RAW",
}
@@ -85,7 +89,7 @@ APP_SUBTITLE = (
or f"Local folder · in-place {ENABLED_FORMAT_LABEL} metadata"
)
APP_URL = os.getenv("APP_URL", "http://localhost:8080")
APP_VERSION = "0.8.3"
APP_VERSION = "0.9.0"
MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "150"))
MAX_UPLOAD_BYTES = MAX_UPLOAD_MB * 1024 * 1024
PREVIEW_MAX_EDGE = int(os.getenv("PREVIEW_MAX_EDGE", "2400"))
@@ -97,9 +101,15 @@ TIFF_EXTENSIONS = {".tif", ".tiff"}
HEIF_EXTENSIONS = {".heic", ".heif", ".hif"}
DNG_EXTENSIONS = {".dng"}
CANON_EXTENSIONS = {".cr2", ".cr3"}
IPTC_EXTENSIONS = {".jpg", ".jpeg", ".tif", ".tiff", ".dng", ".cr2"}
NIKON_EXTENSIONS = {".nef", ".nrw"}
SONY_EXTENSIONS = {".arw", ".arq"}
RAW_EXTENSIONS = DNG_EXTENSIONS | CANON_EXTENSIONS | NIKON_EXTENSIONS | SONY_EXTENSIONS
IPTC_EXTENSIONS = {
".jpg", ".jpeg", ".tif", ".tiff", ".dng", ".cr2",
".nef", ".nrw", ".arw", ".arq",
}
SERVER_PREVIEW_EXTENSIONS = (
TIFF_EXTENSIONS | HEIF_EXTENSIONS | DNG_EXTENSIONS | CANON_EXTENSIONS
TIFF_EXTENSIONS | HEIF_EXTENSIONS | RAW_EXTENSIONS
) & SUPPORTED_EXTENSIONS
MEDIA_TYPES = {
".jpg": "image/jpeg",
@@ -113,6 +123,10 @@ MEDIA_TYPES = {
".dng": "image/x-adobe-dng",
".cr2": "image/x-canon-cr2",
".cr3": "image/x-canon-cr3",
".nef": "image/x-nikon-nef",
".nrw": "image/x-nikon-nrw",
".arw": "image/x-sony-arw",
".arq": "image/x-sony-arq",
}
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
@@ -487,6 +501,14 @@ def _format_name(suffix: str) -> str:
return "CR2"
if suffix == ".cr3":
return "CR3"
if suffix == ".nef":
return "NEF"
if suffix == ".nrw":
return "NRW"
if suffix == ".arw":
return "ARW"
if suffix == ".arq":
return "ARQ"
return suffix.lstrip(".").upper() or "image"
@@ -528,6 +550,10 @@ def _validate_image_signature(path: Path, suffix: str) -> None:
for offset in range(8, len(signature) - 3, 4)
)
)
elif suffix in NIKON_EXTENSIONS | SONY_EXTENSIONS:
# NEF, NRW, ARW, and ARQ are TIFF-based. ExifTool performs deeper
# camera-format validation when the file is inspected or written.
valid = signature.startswith((b"II*\x00", b"MM\x00*"))
if not valid:
raise HTTPException(
@@ -706,6 +732,10 @@ def _raw_preview_tags(suffix: str) -> tuple[str, ...]:
return ("PreviewImage", "JpgFromRaw", "ThumbnailImage")
if suffix == ".cr3":
return ("JpgFromRaw", "PreviewImage", "ThumbnailImage")
if suffix in NIKON_EXTENSIONS:
return ("JpgFromRaw", "PreviewImage", "OtherImage", "ThumbnailImage")
if suffix in SONY_EXTENSIONS:
return ("JpgFromRaw", "PreviewImage", "ThumbnailImage")
return ("JpgFromRaw", "PreviewImage", "ThumbnailImage")
@@ -760,7 +790,7 @@ def _render_raw(path: Path, format_name: str) -> Image.Image:
def _preview_bytes(temp_path: Path, suffix: str, filename: str) -> bytes:
try:
if suffix in DNG_EXTENSIONS | CANON_EXTENSIONS:
if suffix in RAW_EXTENSIONS:
format_name = _format_name(suffix)
preview = _embedded_raw_preview(temp_path, format_name, suffix)
if preview is None:
@@ -775,7 +805,7 @@ def _preview_bytes(temp_path: Path, suffix: str, filename: str) -> bytes:
preview.load()
try:
max_edge = RAW_PREVIEW_MAX_EDGE if suffix in DNG_EXTENSIONS | CANON_EXTENSIONS else PREVIEW_MAX_EDGE
max_edge = RAW_PREVIEW_MAX_EDGE if suffix in RAW_EXTENSIONS else PREVIEW_MAX_EDGE
preview.thumbnail((max_edge, max_edge), Image.Resampling.LANCZOS)
if preview.mode in {"RGBA", "LA"} or (preview.mode == "P" and "transparency" in preview.info):
rgba = preview.convert("RGBA")
@@ -1024,7 +1054,7 @@ async def process_photo(
command,
capture_output=True,
text=True,
timeout=180 if suffix in DNG_EXTENSIONS | CANON_EXTENSIONS else 90,
timeout=180 if suffix in RAW_EXTENSIONS else 90,
check=False,
)
exiftool_seconds = time.perf_counter() - exiftool_started
+5 -1
View File
@@ -126,11 +126,15 @@ function formatType(filename) {
if (extension === 'dng') return 'DNG';
if (extension === 'cr2') return 'CR2';
if (extension === 'cr3') return 'CR3';
if (extension === 'nef') return 'NEF';
if (extension === 'nrw') return 'NRW';
if (extension === 'arw') return 'ARW';
if (extension === 'arq') return 'ARQ';
return extension.toUpperCase();
}
function requiresServerPreview(photo) {
return ['TIFF', 'HEIF', 'DNG', 'CR2', 'CR3'].includes(photo.format);
return ['TIFF', 'HEIF', 'DNG', 'CR2', 'CR3', 'NEF', 'NRW', 'ARW', 'ARQ'].includes(photo.format);
}
function isEnabledFilename(filename) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'photo-date-editor-v8-3';
const CACHE_NAME = 'photo-date-editor-v9-0';
const SHELL = ['/', '/styles.css', '/app.js', '/manifest.webmanifest', '/vendor/leaflet/leaflet.css', '/vendor/leaflet/leaflet.js'];
self.addEventListener('install', (event) => {