diff --git a/.vscode/settings.json b/.vscode/settings.json index 1deae116..e416c8eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,7 @@ "pushstate", "qaub", "Qrcode", + "qrserver", "QRSTUVWXYZ", "ratelimit", "ratelimiter", diff --git a/next.config.ts b/next.config.ts index 584e902f..389a8b7b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -81,6 +81,7 @@ const nextConfig: NextConfig = { remotePatterns, minimumCacheTTL: 31536000, }, + serverExternalPackages: ['exifr'], turbopack: { resolveAlias: { [LOCALE_ALIAS]: `@/${LOCALE_DYNAMIC}`, diff --git a/src/db/migration.ts b/src/db/migration.ts index 2ee044a2..c040d5b9 100644 --- a/src/db/migration.ts +++ b/src/db/migration.ts @@ -101,6 +101,15 @@ export const MIGRATIONS: Migration[] = [{ DROP COLUMN IF EXISTS latitude, DROP COLUMN IF EXISTS longitude; `), +}, { + label: '09: Image Dimensions', + table: 'photos', + fields: ['width', 'height'], + run: () => sql` + ALTER TABLE photos + ADD COLUMN IF NOT EXISTS width INTEGER, + ADD COLUMN IF NOT EXISTS height INTEGER + `, }]; export const migrationForError = (e: any) => diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index b10706bf..ca986e3d 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -295,7 +295,10 @@ export default function PhotoForm({ ?
{photoStorageUrls.map(({ url, size }) => { - const { fileName } = getFileNamePartsFromStorageUrl(url); + const { + fileName, + fileModifier, + } = getFileNamePartsFromStorageUrl(url); return
{fileName} - {size} + + {size} + {/* Show dimensions for original file when available */} + {!fileModifier && formData.width && formData.height && + ` @ ${formData.width}×${formData.height}`} +
; })}
diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 3ca3e558..1a170cf5 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -253,6 +253,18 @@ const FORM_METADATA = ( label: 'blur data', readOnly: true, }, + width: { + section: 'storage', + label: 'width', + readOnly: true, + hideIfEmpty: true, + }, + height: { + section: 'storage', + label: 'height', + readOnly: true, + hideIfEmpty: true, + }, aspectRatio: { section: 'storage', label: 'aspect ratio', @@ -423,6 +435,12 @@ export const convertFormDataToPhotoDbInsert = ( ...photoForm.recipeTitle && { recipeTitle: parameterize(photoForm.recipeTitle), }, + width: photoForm.width + ? parseInt(photoForm.width) + : undefined, + height: photoForm.height + ? parseInt(photoForm.height) + : undefined, // Convert form strings to numbers aspectRatio: photoForm.aspectRatio ? roundToNumber(parseFloat(photoForm.aspectRatio), 6) diff --git a/src/photo/form/server.ts b/src/photo/form/server.ts index 744b1bd4..b946994c 100644 --- a/src/photo/form/server.ts +++ b/src/photo/form/server.ts @@ -1,7 +1,7 @@ import { getCompatibleExifValue, convertApertureValueToFNumber, - getAspectRatioFromExif, + getDimensionsFromExif, getOffsetFromExif, } from '@/utility/exif'; import { @@ -45,8 +45,14 @@ export const convertExifToFormData = ( const dateTimeOriginal = getExifValue('DateTimeOriginal'); const offset = getOffsetFromExif(exif, exifr); + const { width, height, aspectRatio } = getDimensionsFromExif(exif, exifr); + return { - aspectRatio: getAspectRatioFromExif(exif).toString(), + ...width && height && { + width: width.toString(), + height: height.toString(), + }, + aspectRatio: aspectRatio.toString(), make: getExifValue('Make'), model: getExifValue('Model'), focalLength: getExifValue('FocalLength')?.toString(), diff --git a/src/photo/index.ts b/src/photo/index.ts index a4a7b99b..a9b9bca7 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -50,6 +50,8 @@ export const MAX_PHOTO_UPLOAD_SIZE_IN_BYTES = 50_000_000; // Core EXIF data export interface PhotoExif { + width?: number + height?: number aspectRatio: number make?: string model?: string diff --git a/src/photo/query.ts b/src/photo/query.ts index 2082153f..2ef904be 100644 --- a/src/photo/query.ts +++ b/src/photo/query.ts @@ -44,6 +44,8 @@ export const createPhotosTable = () => id VARCHAR(8) PRIMARY KEY, url VARCHAR(255) NOT NULL, extension VARCHAR(255) NOT NULL, + width INTEGER, + height INTEGER, aspect_ratio REAL DEFAULT 1.5, blur_data TEXT, title VARCHAR(255), @@ -85,6 +87,8 @@ export const insertPhoto = (photo: PhotoDbInsert) => id, url, extension, + width, + height, aspect_ratio, blur_data, title, @@ -118,6 +122,8 @@ export const insertPhoto = (photo: PhotoDbInsert) => ${photo.id}, ${photo.url}, ${photo.extension}, + ${photo.width}, + ${photo.height}, ${photo.aspectRatio}, ${photo.blurData}, ${photo.title}, @@ -155,6 +161,8 @@ export const updatePhoto = (photo: PhotoDbInsert) => UPDATE photos SET url=${photo.url}, extension=${photo.extension}, + width=${photo.width}, + height=${photo.height}, aspect_ratio=${photo.aspectRatio}, blur_data=${photo.blurData}, title=${photo.title}, diff --git a/src/platforms/storage/index.ts b/src/platforms/storage/index.ts index 9ad89218..daad17f7 100644 --- a/src/platforms/storage/index.ts +++ b/src/platforms/storage/index.ts @@ -67,13 +67,17 @@ export const getFileNamePartsFromStorageUrl = (url: string) => { fileName = '', fileNameBase = '', fileId = '', + fileModifier = '', fileExtension = '', - ] = url.match(/^(.+)\/((-*[a-z0-9]+-*([a-z0-9-]+))\.([a-z]{1,4}))$/i) ?? []; + ] = url.match( + /^(.+)\/((-*[a-z0-9]+-*([a-z0-9]+)-*([a-z0-9]+)*)\.([a-z]{1,4}))$/i, + ) ?? []; return { urlBase, fileName, fileNameBase, fileId, + fileModifier, fileExtension, }; }; diff --git a/src/utility/exif.ts b/src/utility/exif.ts index 99348393..c1e9b335 100644 --- a/src/utility/exif.ts +++ b/src/utility/exif.ts @@ -1,3 +1,4 @@ +import { DEFAULT_ASPECT_RATIO } from '@/photo'; import { OrientationTypes, type ExifData, ExifTags } from 'ts-exif-parser'; export const getCompatibleExifValue = ( @@ -19,12 +20,19 @@ export const getOffsetFromExif = ( Object.values(exifr).find(isValueOffset) ) as string | undefined; -export const getAspectRatioFromExif = (data: ExifData): number => { +export const getDimensionsFromExif = ( + exif: ExifData, + exifr: any, +): { + width: number | undefined + height: number | undefined + aspectRatio: number +} => { // Using '||' operator to handle `Orientation` unexpectedly being '0' - const orientation = data.tags?.Orientation || OrientationTypes.TOP_LEFT; + const orientation = exif.tags?.Orientation || OrientationTypes.TOP_LEFT; - const width = data.imageSize?.width ?? 3.0; - const height = data.imageSize?.height ?? 2.0; + let width: number | undefined; + let height: number | undefined; switch (orientation) { case OrientationTypes.TOP_LEFT: @@ -33,11 +41,25 @@ export const getAspectRatioFromExif = (data: ExifData): number => { case OrientationTypes.BOTTOM_LEFT: case OrientationTypes.LEFT_TOP: case OrientationTypes.RIGHT_BOTTOM: - return width / height; + width = exif.imageSize?.width || exifr.ImageWidth; + height = exif.imageSize?.height || exifr.ImageHeight; + break; case OrientationTypes.RIGHT_TOP: case OrientationTypes.LEFT_BOTTOM: - return height / width; + width = exif.imageSize?.height || exifr.ImageHeight; + height = exif.imageSize?.width || exifr.ImageWidth; + break; } + + const aspectRatio = width && height + ? width / height + : DEFAULT_ASPECT_RATIO; + + return { + width, + height, + aspectRatio, + }; }; export const convertApertureValueToFNumber = (