diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 188cc016f1..93a91c26d7 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -84,7 +84,7 @@ export class MetadataRepository { inferTimezoneFromDatestamps: true, inferTimezoneFromTimeStamp: true, useMWG: true, - numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength', 'FileSize'], + numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength', 'FileSize', 'Rotation'], /* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */ geoTz: (lat, lon) => geotz.find(lat, lon)[0], geolocation: true, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 70e059d4bf..dfa254b78a 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -616,6 +616,17 @@ export class MetadataService extends BaseService { // never use duration from sidecar delete sidecarTags?.Duration; + // don't use Exif Orientation for HEIF based images, it's usually missing or invalid. + // prefer irot (ExifTool QuickTime:Rotation) mapped to ExifOrientation. + if (mimeTypes.isHeifImage(asset.originalPath)) { + const orientation = this.getHeifOrientation(mediaTags); + if (orientation === null) { + delete mediaTags.Orientation; + } else { + mediaTags.Orientation = orientation; + } + } + return { tags: { ...mediaTags, ...videoResult?.tags, ...sidecarTags }, audio: videoResult?.audio, @@ -1110,4 +1121,24 @@ export class MetadataService extends BaseService { return { tags, audio, video, packets, format }; } + + private getHeifOrientation(exifTags: ImmichTags): ExifOrientation | null { + // https://exiftool.org/TagNames/QuickTime.html#ItemPropCont + const rotation = typeof exifTags.Rotation === 'number' ? exifTags.Rotation : undefined; + switch (rotation) { + case 0: { + return ExifOrientation.Horizontal; + } + case 1: { + return ExifOrientation.Rotate270CW; + } + case 2: { + return ExifOrientation.Rotate180; + } + case 3: { + return ExifOrientation.Rotate90CW; + } + } + return null; + } } diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index be98edb9f0..346124df32 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -74,6 +74,11 @@ const possiblyAnimatedImage: Record = Object.fromEntries( Object.entries(image).filter(([key]) => possiblyAnimatedImageExtensions.has(key)), ); +const heifImageExtensions = new Set(['.avif', '.heic', '.heif', '.hif']); +const heifImage: Record = Object.fromEntries( + Object.entries(image).filter(([key]) => heifImageExtensions.has(key)), +); + const extensionOverrides: Record = { 'image/jpeg': '.jpg', }; @@ -147,6 +152,7 @@ export const mimeTypes = { isAsset: (filename: string) => isType(filename, image) || isType(filename, video), isImage: (filename: string) => isType(filename, image), isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage), + isHeifImage: (filename: string) => isType(filename, heifImage), isPossiblyAnimatedImage: (filename: string) => isType(filename, possiblyAnimatedImage), isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar),