Allow reselecting the upload photo and prefill date from EXIF
Once a file was picked the preview <img> 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.
This commit is contained in:
+72
-1
@@ -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 @@
|
||||
<small>jpg or png</small>
|
||||
</div>
|
||||
<img id="preview" alt="Preview">
|
||||
<div class="change-hint">Click to change</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user