Skip to content

Commit 9353105

Browse files
committed
fix(server): adjust panorama medata for new image dimensions
1 parent db15e5e commit 9353105

File tree

6 files changed

+89
-21
lines changed

6 files changed

+89
-21
lines changed

server/src/repositories/media.repository.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,15 @@ export class MediaRepository {
121121
}
122122
}
123123

124-
async copyTagGroup(tagGroup: string, source: string, target: string): Promise<boolean> {
124+
async writeTags(tags: WriteTags, output: string): Promise<boolean> {
125125
try {
126-
await exiftool.write(
127-
target,
128-
{},
129-
{
130-
ignoreMinorErrors: true,
131-
writeArgs: ['-TagsFromFile', source, `-${tagGroup}:all>${tagGroup}:all`, '-overwrite_original'],
132-
},
133-
);
126+
await exiftool.write(output, tags, {
127+
ignoreMinorErrors: true,
128+
writeArgs: ['-overwrite_original'],
129+
});
134130
return true;
135131
} catch (error: any) {
136-
this.logger.warn(`Could not copy tag data to image: ${error.message}`);
132+
this.logger.warn(`Could not write tags to image: ${error.message}`);
137133
return false;
138134
}
139135
}

server/src/repositories/metadata.repository.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
7272

7373
AndroidMake?: string;
7474
AndroidModel?: string;
75+
76+
UsePanoramaViewer?: boolean;
77+
ProjectionType?: string;
78+
PoseHeadingDegrees?: number;
79+
PosePitchDegrees?: number;
80+
PoseRollDegrees?: number;
81+
InitialViewHeadingDegrees?: number;
82+
InitialViewPitchDegrees?: number;
83+
InitialViewRollDegrees?: number;
84+
CroppedAreaImageWidthPixels?: number;
85+
CroppedAreaImageHeightPixels?: number;
86+
FullPanoWidthPixels?: number;
87+
FullPanoHeightPixels?: number;
88+
CroppedAreaLeftPixels?: number;
89+
CroppedAreaTopPixels?: number;
7590
}
7691

7792
@Injectable()

server/src/services/media.service.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,12 @@ describe(MediaService.name, () => {
865865
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
866866
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
867867
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
868-
mocks.media.copyTagGroup.mockResolvedValue(true);
868+
mocks.metadata.readTags.mockResolvedValue({
869+
ProjectionType: 'equirectangular',
870+
PoseHeadingDegrees: 127,
871+
FullPanoWidthPixels: 3840,
872+
FullPanoHeightPixels: 2160,
873+
});
869874

870875
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.panoramaTif);
871876

@@ -892,10 +897,14 @@ describe(MediaService.name, () => {
892897
expect.any(String),
893898
);
894899

895-
expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2);
896-
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith(
897-
'XMP-GPano',
898-
assetStub.panoramaTif.originalPath,
900+
expect(mocks.media.writeTags).toHaveBeenCalledTimes(2);
901+
expect(mocks.media.writeTags).toHaveBeenCalledWith(
902+
{
903+
ProjectionType: 'equirectangular',
904+
PoseHeadingDegrees: 127,
905+
FullPanoWidthPixels: 2560,
906+
FullPanoHeightPixels: 1440,
907+
},
899908
expect.any(String),
900909
);
901910
});

server/src/services/media.service.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ interface UpsertFileOptions {
4747
path: string;
4848
}
4949

50+
const PANORAMA_CONSTANTS = [
51+
'UsePanoramaViewer',
52+
'ProjectionType',
53+
'PoseHeadingDegrees',
54+
'PosePitchDegrees',
55+
'PoseRollDegrees',
56+
'InitialViewHeadingDegrees',
57+
'InitialViewPitchDegrees',
58+
'InitialViewRollDegrees',
59+
] as const;
60+
61+
const PANORAMA_SCALABLES = [
62+
'CroppedAreaImageWidthPixels',
63+
'CroppedAreaImageHeightPixels',
64+
'FullPanoWidthPixels',
65+
'FullPanoHeightPixels',
66+
'CroppedAreaLeftPixels',
67+
'CroppedAreaTopPixels',
68+
] as const;
69+
70+
const PANORAMA_ALL = [...PANORAMA_CONSTANTS, ...PANORAMA_SCALABLES] as const;
71+
5072
@Injectable()
5173
export class MediaService extends BaseService {
5274
videoInterfaces: VideoInterfaces = { dri: [], mali: false };
@@ -316,12 +338,37 @@ export class MediaService extends BaseService {
316338

317339
const outputs = await Promise.all(promises);
318340

319-
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') {
341+
const originalSize = asset.exifInfo.exifImageHeight;
342+
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR' && originalSize) {
343+
// preview size is min(preview size, original size) so do the same for pano pixel adjustment
344+
const scaleRatio = Math.min(1, image.preview.size / originalSize);
345+
const originalTags = await this.metadataRepository.readTags(asset.originalPath);
346+
347+
const previewTags = {} as Record<string, string | number | boolean>;
348+
const fullsizeTags = {} as Record<string, string | number | boolean>;
349+
350+
for (const key of PANORAMA_CONSTANTS) {
351+
if (key in originalTags && originalTags[key]) {
352+
previewTags[key] = originalTags[key];
353+
}
354+
}
355+
for (const key of PANORAMA_SCALABLES) {
356+
if (key in originalTags && originalTags[key]) {
357+
previewTags[key] = Math.round(originalTags[key] * scaleRatio);
358+
}
359+
}
360+
361+
if (fullsizePath) {
362+
for (const key of PANORAMA_ALL) {
363+
if (key in originalTags && originalTags[key]) {
364+
fullsizeTags[key] = originalTags[key];
365+
}
366+
}
367+
}
368+
320369
const promises = [
321-
this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewPath),
322-
fullsizePath
323-
? this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, fullsizePath)
324-
: Promise.resolve(),
370+
this.mediaRepository.writeTags(previewTags, previewPath),
371+
fullsizePath ? this.mediaRepository.writeTags(fullsizeTags, fullsizePath) : Promise.resolve(),
325372
];
326373
await Promise.all(promises);
327374
}

server/test/fixtures/asset.stub.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ export const assetStub = {
897897
exifInfo: {
898898
fileSizeInByte: 5000,
899899
projectionType: 'EQUIRECTANGULAR',
900+
exifImageHeight: 2160,
900901
} as Exif,
901902
duplicateId: null,
902903
isOffline: false,

server/test/repositories/media.repository.mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaReposi
66
return {
77
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
88
writeExif: vitest.fn().mockImplementation(() => Promise.resolve()),
9-
copyTagGroup: vitest.fn().mockImplementation(() => Promise.resolve()),
9+
writeTags: vitest.fn().mockResolvedValue(true),
1010
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
1111
decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }),
1212
extract: vitest.fn().mockResolvedValue(null),

0 commit comments

Comments
 (0)