Begin storing image width + height

This commit is contained in:
Sam Becker 2026-02-01 19:05:58 -06:00
parent 42bf07445c
commit 1cb3c4a22c
10 changed files with 90 additions and 11 deletions

View File

@ -57,6 +57,7 @@
"pushstate",
"qaub",
"Qrcode",
"qrserver",
"QRSTUVWXYZ",
"ratelimit",
"ratelimiter",

View File

@ -81,6 +81,7 @@ const nextConfig: NextConfig = {
remotePatterns,
minimumCacheTTL: 31536000,
},
serverExternalPackages: ['exifr'],
turbopack: {
resolveAlias: {
[LOCALE_ALIAS]: `@/${LOCALE_DYNAMIC}`,

View File

@ -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) =>

View File

@ -295,7 +295,10 @@ export default function PhotoForm({
? <SmallDisclosure label="Optimized file set">
<div className="space-y-1">
{photoStorageUrls.map(({ url, size }) => {
const { fileName } = getFileNamePartsFromStorageUrl(url);
const {
fileName,
fileModifier,
} = getFileNamePartsFromStorageUrl(url);
return <div
key={url}
className="flex items-center gap-2"
@ -307,7 +310,12 @@ export default function PhotoForm({
>
{fileName}
</Link>
<span className="text-dim">{size}</span>
<span className="text-dim">
{size}
{/* Show dimensions for original file when available */}
{!fileModifier && formData.width && formData.height &&
` @ ${formData.width}×${formData.height}`}
</span>
</div>;
})}
</div>

View File

@ -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)

View File

@ -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(),

View File

@ -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

View File

@ -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},

View File

@ -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,
};
};

View File

@ -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 = (