fix(web): use irot/imir tags for HEIF Orientation (#27820)

* fix(web): use irot/imir tags for HEIF Orientation

* ignore Exif Orientation for HEIF images per MIAF standard compliance

* add Rotation and Mirroring to exiftool numericTags

* add isHeifBasedImage function to detect HEIF-based image extensions

* add getHeifBasedOrientation method to map irot/imir tags to ExifOrientation

* removed mirroring, simplified code

* Removed "Based" in "heifBased"

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
joojoooo
2026-06-08 15:33:28 +02:00
committed by GitHub
parent 12d344efe0
commit 164cda87a3
3 changed files with 38 additions and 1 deletions
@@ -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,
+31
View File
@@ -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;
}
}
+6
View File
@@ -74,6 +74,11 @@ const possiblyAnimatedImage: Record<string, string[]> = Object.fromEntries(
Object.entries(image).filter(([key]) => possiblyAnimatedImageExtensions.has(key)),
);
const heifImageExtensions = new Set(['.avif', '.heic', '.heif', '.hif']);
const heifImage: Record<string, string[]> = Object.fromEntries(
Object.entries(image).filter(([key]) => heifImageExtensions.has(key)),
);
const extensionOverrides: Record<string, string> = {
'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),