Skip to content

Commit 3350163

Browse files
Merge pull request #1 from EvanProgramming/copilot/fix-gallery-page-preview-exif
Use manifest-backed EXIF in Gallery preview with runtime fallback
2 parents 42236e3 + 66cfb03 commit 3350163

4 files changed

Lines changed: 79 additions & 8 deletions

File tree

scripts/protect-images.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import crypto from 'node:crypto'
22
import fs from 'node:fs/promises'
33
import os from 'node:os'
44
import path from 'node:path'
5+
import exifr from 'exifr'
56
import sharp from 'sharp'
67

78
const ROOT = process.cwd()
@@ -18,6 +19,7 @@ const BACKUP_BASE = process.env.PHOTO_ORIGINAL_BACKUP
1819
? path.resolve(process.env.PHOTO_ORIGINAL_BACKUP)
1920
: path.join(os.homedir(), 'Documents', 'evangong.tech-originals')
2021
const BACKUP_MARKER = '.photo-originals.json'
22+
const PUBLIC_EXIF_FIELDS = ['Make', 'Model', 'LensModel', 'DateTimeOriginal', 'FocalLength', 'FNumber', 'ExposureTime', 'ISO']
2123

2224
function usage() {
2325
console.log(`Usage: npm run images:protect -- --check|--apply
@@ -112,6 +114,27 @@ function outputFormat(repoPath) {
112114
return 'jpeg'
113115
}
114116

117+
function normalizeExifValue(value) {
118+
if (value == null) return null
119+
if (value instanceof Date) return value.toISOString()
120+
if (typeof value === 'string') {
121+
const trimmed = value.trim()
122+
return trimmed || null
123+
}
124+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
125+
return null
126+
}
127+
128+
function pickPublicExif(exif) {
129+
if (!exif || typeof exif !== 'object') return null
130+
const picked = {}
131+
for (const field of PUBLIC_EXIF_FIELDS) {
132+
const normalized = normalizeExifValue(exif[field])
133+
if (normalized != null) picked[field] = normalized
134+
}
135+
return Object.keys(picked).length ? picked : null
136+
}
137+
115138
async function renderDerivative(sourcePath, repoPath) {
116139
const sourceBuffer = await fs.readFile(sourcePath)
117140
const normalized = await sharp(sourceBuffer)
@@ -275,6 +298,8 @@ async function apply() {
275298
const relativePath = toRepoPath(repoFile)
276299
const sourcePath = path.join(backupDir, ...relativePath.split('/'))
277300
const sourceBuffer = await fs.readFile(sourcePath)
301+
const sourceExif = await exifr.parse(sourceBuffer, { gps: false, icc: false, xmp: false }).catch(() => null)
302+
const publicExif = pickPublicExif(sourceExif)
278303
const derivative = await renderDerivative(sourcePath, relativePath)
279304
const temporaryPath = `${repoFile}.protected.tmp`
280305

@@ -291,6 +316,7 @@ async function apply() {
291316
width: metadata.width,
292317
height: metadata.height,
293318
format: metadata.format,
319+
...(publicExif ? { exif: publicExif } : {}),
294320
})
295321
}
296322

src/components/DomeGallery/DomeGallery.jsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ function formatNumber(n, digits = 1) {
8080
return String(parseFloat(fixed));
8181
}
8282

83-
function renderExifRows(exif) {
84-
const rows = [
83+
function getExifRows(exif) {
84+
return [
8585
{ label: 'Camera', value: formatCamera(exif) },
8686
{ label: 'Lens', value: exif?.LensModel?.trim() },
8787
{ label: 'Date', value: formatDate(exif?.DateTimeOriginal) },
@@ -90,17 +90,23 @@ function renderExifRows(exif) {
9090
{ label: 'Shutter', value: formatExposureTime(exif?.ExposureTime) },
9191
{ label: 'ISO', value: exif?.ISO != null ? String(Number(exif.ISO)) : '' }
9292
].filter(r => r.value);
93+
}
9394

95+
function renderExifRows(exif) {
96+
const rows = getExifRows(exif);
9497
if (rows.length === 0) return '<div class="dg-exif-empty">No EXIF data available</div>';
9598

9699
return rows
97100
.map(r => `<div class="dg-exif-row"><span class="dg-exif-label">${r.label}</span><span class="dg-exif-value">${r.value}</span></div>`)
98101
.join('');
99102
}
100103

101-
async function loadExifPanel(rawSrc, overlay) {
104+
async function loadExifPanel(rawSrc, overlay, seededExif = null) {
102105
try {
103-
const exif = await exifr.parse(rawSrc, { gps: false, icc: false, xmp: false });
106+
let exif = seededExif;
107+
if (!getExifRows(exif).length) {
108+
exif = await exifr.parse(rawSrc, { gps: false, icc: false, xmp: false });
109+
}
104110
if (!exif) return;
105111
const panel = document.createElement('div');
106112
panel.className = 'dg-exif-panel';
@@ -143,7 +149,8 @@ function buildItems(pool, seg) {
143149
alt: image.alt || '',
144150
width: image.width,
145151
height: image.height,
146-
sizes: image.sizes
152+
sizes: image.sizes,
153+
exif: image.exif || null
147154
};
148155
});
149156

@@ -168,7 +175,8 @@ function buildItems(pool, seg) {
168175
alt: usedImages[i].alt,
169176
width: usedImages[i].width,
170177
height: usedImages[i].height,
171-
sizes: usedImages[i].sizes
178+
sizes: usedImages[i].sizes,
179+
exif: usedImages[i].exif
172180
}));
173181
}
174182

@@ -577,6 +585,14 @@ export default function DomeGallery({
577585
overlay.style.transformOrigin = 'top left';
578586
overlay.style.transition = `transform ${enlargeTransitionMs}ms ease, opacity ${enlargeTransitionMs}ms ease`;
579587
const rawSrc = parent.dataset.src || el.querySelector('img')?.src || '';
588+
let seededExif = null;
589+
if (parent.dataset.exif) {
590+
try {
591+
seededExif = JSON.parse(parent.dataset.exif);
592+
} catch {
593+
seededExif = null;
594+
}
595+
}
580596
const imgWrap = document.createElement('div');
581597
imgWrap.className = 'enlarge__image-wrap';
582598
const img = document.createElement('img');
@@ -645,7 +661,7 @@ export default function DomeGallery({
645661
overlay.removeEventListener('transitionend', cleanupSecond);
646662
overlay.style.transition = prevTransition;
647663
overlay.classList.add('enlarge--ready');
648-
loadExifPanel(rawSrc, overlay);
664+
loadExifPanel(rawSrc, overlay, seededExif);
649665
};
650666
overlay.addEventListener('transitionend', cleanupSecond, { once: true });
651667
};
@@ -710,6 +726,7 @@ export default function DomeGallery({
710726
key={`${it.x},${it.y},${i}`}
711727
className="item"
712728
data-src={it.src}
729+
data-exif={it.exif ? JSON.stringify(it.exif) : undefined}
713730
data-offset-x={it.x}
714731
data-offset-y={it.y}
715732
data-size-x={it.sizeX}

src/components/Gallery/galleryData.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ describe('galleryData', () => {
116116
expect(img.width).toBeGreaterThan(0)
117117
expect(img.height).toBeGreaterThan(0)
118118
expect(img.sizes).toContain('vw')
119+
expect(img).toHaveProperty('exif')
120+
if (img.exif !== null) {
121+
expect(typeof img.exif).toBe('object')
122+
}
119123
})
120124
})
121125

src/data/photoCatalog.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ const CATEGORY_LABELS = {
77
Miscellaneous: 'Miscellaneous'
88
}
99

10+
function normalizeExif(exif) {
11+
if (!exif || typeof exif !== 'object') return null
12+
13+
const normalized = {}
14+
const allowedFields = ['Make', 'Model', 'LensModel', 'DateTimeOriginal', 'FocalLength', 'FNumber', 'ExposureTime', 'ISO']
15+
16+
for (const field of allowedFields) {
17+
const value = exif[field]
18+
if (value == null) continue
19+
if (typeof value === 'string') {
20+
const trimmed = value.trim()
21+
if (trimmed) normalized[field] = trimmed
22+
continue
23+
}
24+
if (typeof value === 'number' && Number.isFinite(value)) {
25+
normalized[field] = value
26+
continue
27+
}
28+
}
29+
30+
return Object.keys(normalized).length > 0 ? normalized : null
31+
}
32+
1033
const publicPhotos = manifest.assets
1134
.filter(asset => asset.path.startsWith('public/Photography/'))
1235
.map(asset => {
@@ -19,7 +42,8 @@ const publicPhotos = manifest.assets
1942
category: category.toLowerCase(),
2043
width: asset.width,
2144
height: asset.height,
22-
sizes: '(max-width: 640px) 46vw, (max-width: 1200px) 30vw, 420px'
45+
sizes: '(max-width: 640px) 46vw, (max-width: 1200px) 30vw, 420px',
46+
exif: normalizeExif(asset.exif)
2347
}
2448
})
2549
.sort((a, b) => a.src.localeCompare(b.src))

0 commit comments

Comments
 (0)