diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 57c029961e..8f7fe71be9 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1836,6 +1836,36 @@ describe(MetadataService.name, () => { ); }); + it('should still swap width/height using EXIF Orientation for a .heic-named file that is actually JPEG', async () => { + const asset = AssetFactory.create({ originalFileName: 'IMG_1.heic' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset)); + // no QuickTime `Rotation` tag and FileTypeExtension `jpg` - this is what ExifTool reports + // for a file that was converted to JPEG in place but kept its .heic extension/filename. + mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6, FileTypeExtension: 'jpg' }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 2000, + height: 1000, + }), + ); + }); + + it('should ignore EXIF Orientation and use QuickTime Rotation for a genuine HEIC file', async () => { + const asset = AssetFactory.create({ originalFileName: 'IMG_1.heic' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset)); + mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6, FileTypeExtension: 'heic', Rotation: 0 }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 1000, + height: 2000, + }), + ); + }); + it('should overwrite existing width/height for unedited assets', async () => { const asset = AssetFactory.create({ width: 1920, height: 1080, isEdited: false }); mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset)); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index a95d1f1497..57257c9af2 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -603,7 +603,16 @@ export class MetadataService extends BaseService { // 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)) { + // + // Some files carry a .heic/.heif/.avif extension without actually being encoded + // as such (e.g. converted to JPEG in place by external tools before being + // imported). Trusting the filename alone would discard a perfectly valid EXIF + // Orientation tag below and lead to a squished/stretched thumbnail, since the + // dimensions never get swapped for the sideways rotation. ExifTool's own + // content-detected FileTypeExtension is the source of truth here, not the name. + const isActuallyHeif = + mimeTypes.isHeifImage(asset.originalPath) && mimeTypes.isHeifImage(`file.${mediaTags.FileTypeExtension ?? ''}`); + if (isActuallyHeif) { const orientation = this.getHeifOrientation(mediaTags); if (orientation === null) { delete mediaTags.Orientation;