Add Nikon and Sony RAW support v0.9

This commit is contained in:
dmo committed 2026-07-25 08:41:41 +02:00
1 parent f1704c9822
commit 6e8c30c1d4
6 files changed
+87 -37

No files matched your search

+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) => {