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