fix(server): handle orientation of imported face regions (#18021)

* Transform imported face RegionInfo according to Exif Orientation

* Add unit tests for re-orienting metadata face regions

* Make code DRY using ImmichTagsWithFaces instead of NonNullable

* Add e2e test for importing metadata face regions when orientation is RotateCW90

* Disable new e2e test until its asset is added to the test-assets project

* Simplify unit tests by using the same face tag definition

* Combine similar e2e tests

* Disable new e2e test until portrait-orientation-6.jpg is added to test-assets

* Fix lint error: Expected property shorthand

* Update test-assets ref to latest

* Enable new e2e test after updating test-assets
This commit is contained in:
Sergey Katsubo
2025-05-05 18:11:21 +03:00
committed by GitHub
parent 2b3efa02d8
commit 12610e4a9f
4 changed files with 230 additions and 43 deletions
+80 -3
View File
@@ -596,6 +596,80 @@ export class MetadataService extends BaseService {
);
}
private orientRegionInfo(
regionInfo: ImmichTagsWithFaces['RegionInfo'],
orientation: ExifOrientation | undefined,
): ImmichTagsWithFaces['RegionInfo'] {
// skip default Orientation
if (orientation === undefined || orientation === ExifOrientation.Horizontal) {
return regionInfo;
}
const isSidewards = [
ExifOrientation.MirrorHorizontalRotate270CW,
ExifOrientation.Rotate90CW,
ExifOrientation.MirrorHorizontalRotate90CW,
ExifOrientation.Rotate270CW,
].includes(orientation);
// swap image dimensions in AppliedToDimensions if orientation is sidewards
const adjustedAppliedToDimensions = isSidewards
? {
...regionInfo.AppliedToDimensions,
W: regionInfo.AppliedToDimensions.H,
H: regionInfo.AppliedToDimensions.W,
}
: regionInfo.AppliedToDimensions;
// update area coordinates and dimensions in RegionList assuming "normalized" unit as per MWG guidelines
const adjustedRegionList = regionInfo.RegionList.map((region) => {
let { X, Y, W, H } = region.Area;
switch (orientation) {
case ExifOrientation.MirrorHorizontal: {
X = 1 - X;
break;
}
case ExifOrientation.Rotate180: {
[X, Y] = [1 - X, 1 - Y];
break;
}
case ExifOrientation.MirrorVertical: {
Y = 1 - Y;
break;
}
case ExifOrientation.MirrorHorizontalRotate270CW: {
[X, Y] = [Y, X];
break;
}
case ExifOrientation.Rotate90CW: {
[X, Y] = [1 - Y, X];
break;
}
case ExifOrientation.MirrorHorizontalRotate90CW: {
[X, Y] = [1 - Y, 1 - X];
break;
}
case ExifOrientation.Rotate270CW: {
[X, Y] = [Y, 1 - X];
break;
}
}
if (isSidewards) {
[W, H] = [H, W];
}
return {
...region,
Area: { ...region.Area, X, Y, W, H },
};
});
return {
...regionInfo,
AppliedToDimensions: adjustedAppliedToDimensions,
RegionList: adjustedRegionList,
};
}
private async applyTaggedFaces(
asset: { id: string; ownerId: string; faces: AssetFace[]; originalPath: string },
tags: ImmichTags,
@@ -609,13 +683,16 @@ export class MetadataService extends BaseService {
const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id]));
const missing: (Insertable<Person> & { ownerId: string })[] = [];
const missingWithFaceAsset: { id: string; ownerId: string; faceAssetId: string }[] = [];
for (const region of tags.RegionInfo.RegionList) {
const adjustedRegionInfo = this.orientRegionInfo(tags.RegionInfo, tags.Orientation);
const imageWidth = adjustedRegionInfo.AppliedToDimensions.W;
const imageHeight = adjustedRegionInfo.AppliedToDimensions.H;
for (const region of adjustedRegionInfo.RegionList) {
if (!region.Name) {
continue;
}
const imageWidth = tags.RegionInfo.AppliedToDimensions.W;
const imageHeight = tags.RegionInfo.AppliedToDimensions.H;
const loweredName = region.Name.toLowerCase();
const personId = existingNameMap.get(loweredName) || this.cryptoRepository.randomUUID();