From 5bde9dc4a6f3f13c8826a3f486018982177bf893 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Wed, 31 Dec 2025 21:21:48 +0100 Subject: [PATCH] feat(gallery): load and display exif data --- app/components/GalleryModal.vue | 39 ++-- app/pages/gallery.vue | 306 +++++++++----------------------- package.json | 1 + pnpm-lock.yaml | 11 ++ server/api/gallery.get.ts | 82 +++++++++ 5 files changed, 189 insertions(+), 250 deletions(-) create mode 100644 server/api/gallery.get.ts diff --git a/app/components/GalleryModal.vue b/app/components/GalleryModal.vue index c65173a..00cde46 100644 --- a/app/components/GalleryModal.vue +++ b/app/components/GalleryModal.vue @@ -1,19 +1,8 @@ + diff --git a/package.json b/package.json index 959d410..5c88b3e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@tresjs/cientos": "^5.1.2", "@tresjs/core": "^5.2.0", "@tresjs/nuxt": "^5.1.2", + "exifr": "^7.1.3", "gsap": "3.13.0", "pinia": "3.0.4", "tailwindcss": "^4.1.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64cf97f..b3d352c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: "@tresjs/nuxt": specifier: ^5.1.2 version: 5.1.2(f1341efb47d1338baf7454f3b929107c) + exifr: + specifier: ^7.1.3 + version: 7.1.3 gsap: specifier: 3.13.0 version: 3.13.0 @@ -5572,6 +5575,12 @@ packages: } engines: { node: ">=16.17" } + exifr@7.1.3: + resolution: + { + integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==, + } + expand-template@2.0.3: resolution: { @@ -14097,6 +14106,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + exifr@7.1.3: {} + expand-template@2.0.3: {} exsolve@1.0.8: {} diff --git a/server/api/gallery.get.ts b/server/api/gallery.get.ts new file mode 100644 index 0000000..f694373 --- /dev/null +++ b/server/api/gallery.get.ts @@ -0,0 +1,82 @@ +import { readdir } from "fs/promises"; +import { join } from "path"; +import exifr from "exifr"; +import { z } from "zod"; + +const exifSchema = z.object({ + DateTimeOriginal: z.date(), + Make: z.string(), + Model: z.string(), + LensModel: z.string(), + FNumber: z.number(), + ExposureTime: z.number(), + ISO: z.number(), + FocalLength: z.number(), +}); + +export default defineCachedEventHandler( + async () => { + const publicDir = join(process.cwd(), "public/gallery"); + + try { + const files = await readdir(publicDir); + const imageFiles = files.filter((file) => + /\.(jpg|jpeg|png|webp)$/i.test(file), + ); + + const imagesWithExif = await Promise.all( + imageFiles.map(async (filename) => { + const filePath = join(publicDir, filename); + const rawExif = await exifr.parse(filePath, { + tiff: true, + exif: true, + gps: false, + interop: false, + ifd1: false, + }); + + const exif = exifSchema.parse(rawExif); + + return { + filename, + url: `/gallery/${filename}`, + exif: { + date: exif.DateTimeOriginal, + camera: `${exif.Make} ${exif.Model}`, + lens: exif.LensModel, + settings: { + aperture: `f/${exif.FNumber}`, + shutter: `1/${Math.round(1 / exif.ExposureTime)}s`, + iso: String(exif.ISO), + focalLength: `${exif.FocalLength}mm`, + }, + }, + }; + }), + ); + + return imagesWithExif; + } catch (err) { + console.log(err); + throw createError({ + statusCode: 500, + message: "Failed to read gallery directory", + }); + } + }, + { + maxAge: 60 * 60 * 24, + getKey: async () => { + const publicDir = join(process.cwd(), "public/gallery"); + try { + const files = await readdir(publicDir); + const imageFiles = files.filter((file) => + /\.(jpg|jpeg|png|webp)$/i.test(file), + ); + return `gallery:${imageFiles.length}`; + } catch { + return "gallery:0"; + } + }, + }, +);