Skip to content

Commit 45f1293

Browse files
grgergo1gergo=
authored andcommitted
fix: use full-size image for non-web-compatible panoramas (immich-app#20359)
* fix(web): use full-size image for non-web-compatible panoramas * always generate full-size image for panoramas * add unit test * fix formatting --------- Co-authored-by: gergo= <[email protected]>
1 parent afb2980 commit 45f1293

File tree

4 files changed

+77
-3
lines changed

4 files changed

+77
-3
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,37 @@ describe(MediaService.name, () => {
861861
);
862862
});
863863

864+
it('should always generate full-size preview from non-web-friendly panoramas', async () => {
865+
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
866+
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
867+
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
868+
869+
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.panoramaTif);
870+
871+
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
872+
873+
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
874+
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.panoramaTif.originalPath, {
875+
colorspace: Colorspace.Srgb,
876+
orientation: undefined,
877+
processInvalidImages: false,
878+
size: undefined,
879+
});
880+
881+
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
882+
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
883+
rawBuffer,
884+
{
885+
colorspace: Colorspace.Srgb,
886+
format: ImageFormat.Jpeg,
887+
quality: 80,
888+
processInvalidImages: false,
889+
raw: rawInfo,
890+
},
891+
expect.any(String),
892+
);
893+
});
894+
864895
it('should respect encoding options when generating full-size preview', async () => {
865896
mocks.systemMetadata.get.mockResolvedValue({
866897
image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } },

server/src/services/media.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,9 @@ export class MediaService extends BaseService {
271271
// Handle embedded preview extraction for RAW files
272272
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
273273
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
274-
const generateFullsize = image.fullsize.enabled && !mimeTypes.isWebSupportedImage(asset.originalPath);
274+
const generateFullsize =
275+
(image.fullsize.enabled || asset.exifInfo.projectionType == 'EQUIRECTANGULAR') &&
276+
!mimeTypes.isWebSupportedImage(asset.originalPath);
275277
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
276278

277279
const { info, data, colorspace } = await this.decodeImage(

server/test/fixtures/asset.stub.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,4 +866,43 @@ export const assetStub = {
866866
stackId: null,
867867
visibility: AssetVisibility.Timeline,
868868
}),
869+
panoramaTif: Object.freeze({
870+
id: 'asset-id',
871+
status: AssetStatus.Active,
872+
deviceAssetId: 'device-asset-id',
873+
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
874+
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
875+
owner: userStub.user1,
876+
ownerId: 'user-id',
877+
deviceId: 'device-id',
878+
originalPath: '/original/path.tif',
879+
checksum: Buffer.from('file hash', 'utf8'),
880+
type: AssetType.Image,
881+
files,
882+
thumbhash: Buffer.from('blablabla', 'base64'),
883+
encodedVideoPath: null,
884+
createdAt: new Date('2023-02-23T05:06:29.716Z'),
885+
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
886+
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
887+
isFavorite: true,
888+
duration: null,
889+
isExternal: false,
890+
livePhotoVideo: null,
891+
livePhotoVideoId: null,
892+
sharedLinks: [],
893+
originalFileName: 'asset-id.tif',
894+
faces: [],
895+
deletedAt: null,
896+
sidecarPath: null,
897+
exifInfo: {
898+
fileSizeInByte: 5000,
899+
projectionType: 'EQUIRECTANGULAR',
900+
} as Exif,
901+
duplicateId: null,
902+
isOffline: false,
903+
updateId: '42',
904+
libraryId: null,
905+
stackId: null,
906+
visibility: AssetVisibility.Timeline,
907+
}),
869908
};

web/src/lib/components/asset-viewer/image-panorama-viewer.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { authManager } from '$lib/managers/auth-manager.svelte';
3-
import { getAssetOriginalUrl } from '$lib/utils';
3+
import { getAssetOriginalUrl, getAssetThumbnailUrl } from '$lib/utils';
44
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
55
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
66
import { LoadingSpinner } from '@immich/ui';
@@ -25,7 +25,9 @@
2525
{:then [data, { default: PhotoSphereViewer }]}
2626
<PhotoSphereViewer
2727
panorama={data}
28-
originalPanorama={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
28+
originalPanorama={isWebCompatibleImage(asset)
29+
? getAssetOriginalUrl(asset.id)
30+
: getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Fullsize, cacheKey: asset.thumbhash })}
2931
/>
3032
{:catch}
3133
{$t('errors.failed_to_load_asset')}

0 commit comments

Comments
 (0)