From b26bef02968e4efaaea225ef8cb43ff56388f728 Mon Sep 17 00:00:00 2001 From: puttaalu Date: Thu, 2 Jul 2026 02:09:54 +0200 Subject: [PATCH] Allow reselecting the upload photo and prefill date from EXIF Once a file was picked the preview overlaid the file input and blocked further clicks, so the drop zone became read-only. Making the preview and the new 'Click to change' hint pointer-events:none lets clicks pass through to the input again. Also parses the JPEG APP1 EXIF segment inline (no dependency) to read DateTimeOriginal and prefill the date field. Falls back to the file's lastModified for PNGs or JPEGs without EXIF. The date field stays editable in either case. --- templates/upload.html | 73 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/templates/upload.html b/templates/upload.html index 5906975..54dc5ab 100644 --- a/templates/upload.html +++ b/templates/upload.html @@ -62,7 +62,23 @@ width: 100%; height: 100%; object-fit: cover; + pointer-events: none; } + .change-hint { + display: none; + position: absolute; + inset: auto 0 0 0; + padding: 6px 10px; + background: rgba(29, 32, 33, 0.75); + color: var(--fg2); + font-family: monospace; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + text-align: center; + pointer-events: none; + } + .drop-zone.has-image .change-hint { display: block; } .field { margin-bottom: 18px; } .field label { display: block; @@ -129,6 +145,7 @@ jpg or png Preview +
Click to change
@@ -156,7 +173,54 @@ dateInput.value = new Date().toISOString().slice(0, 10); - fileInput.addEventListener('change', () => { + // Read DateTimeOriginal from a JPEG's EXIF APP1 segment. Returns "YYYY-MM-DD" + // or null if the file isn't a JPEG, has no EXIF, or lacks the tag. + async function readExifDate(file) { + if (!/jpe?g$/i.test(file.type) && !/\.jpe?g$/i.test(file.name)) return null; + const buf = await file.slice(0, 65536).arrayBuffer(); + const view = new DataView(buf); + if (view.getUint16(0) !== 0xFFD8) return null; + + let offset = 2; + while (offset + 4 < view.byteLength) { + const marker = view.getUint16(offset); + if (marker < 0xFF00) return null; + const size = view.getUint16(offset + 2); + if (marker === 0xFFE1 && view.getUint32(offset + 4) === 0x45786966) { + const tiff = offset + 10; + const little = view.getUint16(tiff) === 0x4949; + const get16 = (o) => view.getUint16(o, little); + const get32 = (o) => view.getUint32(o, little); + const ifd0 = tiff + get32(tiff + 4); + + let exifIfd = 0; + const n = get16(ifd0); + for (let i = 0; i < n; i++) { + const e = ifd0 + 2 + i * 12; + if (get16(e) === 0x8769) { exifIfd = tiff + get32(e + 8); break; } + } + if (!exifIfd) return null; + + const en = get16(exifIfd); + for (let i = 0; i < en; i++) { + const e = exifIfd + 2 + i * 12; + if (get16(e) === 0x9003) { + const count = get32(e + 4); + const dataOff = count > 4 ? tiff + get32(e + 8) : e + 8; + let s = ''; + for (let k = 0; k < count - 1; k++) s += String.fromCharCode(view.getUint8(dataOff + k)); + const m = s.match(/^(\d{4}):(\d{2}):(\d{2})/); + return m ? `${m[1]}-${m[2]}-${m[3]}` : null; + } + } + return null; + } + offset += 2 + size; + } + return null; + } + + fileInput.addEventListener('change', async () => { const file = fileInput.files[0]; if (!file) return; const url = URL.createObjectURL(file); @@ -168,6 +232,13 @@ submitBtn.disabled = false; feedback.className = ''; feedback.style.display = 'none'; + + let d = null; + try { d = await readExifDate(file); } catch (_) {} + if (!d && file.lastModified) { + d = new Date(file.lastModified).toISOString().slice(0, 10); + } + if (d) dateInput.value = d; }); submitBtn.addEventListener('click', async () => {