mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	fix: use full-size image for non-web-compatible panoramas (#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= <gergo@pitty.hu>
This commit is contained in:
		
							parent
							
								
									42f46b11f4
								
							
						
					
					
						commit
						6973683ea7
					
				@ -861,6 +861,37 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should always generate full-size preview from non-web-friendly panoramas', async () => {
 | 
				
			||||||
 | 
					      mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
 | 
				
			||||||
 | 
					      mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
 | 
				
			||||||
 | 
					      mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.panoramaTif);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleGenerateThumbnails({ id: assetStub.image.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
 | 
				
			||||||
 | 
					      expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.panoramaTif.originalPath, {
 | 
				
			||||||
 | 
					        colorspace: Colorspace.Srgb,
 | 
				
			||||||
 | 
					        orientation: undefined,
 | 
				
			||||||
 | 
					        processInvalidImages: false,
 | 
				
			||||||
 | 
					        size: undefined,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
 | 
				
			||||||
 | 
					      expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        rawBuffer,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          colorspace: Colorspace.Srgb,
 | 
				
			||||||
 | 
					          format: ImageFormat.Jpeg,
 | 
				
			||||||
 | 
					          quality: 80,
 | 
				
			||||||
 | 
					          processInvalidImages: false,
 | 
				
			||||||
 | 
					          raw: rawInfo,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        expect.any(String),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should respect encoding options when generating full-size preview', async () => {
 | 
					    it('should respect encoding options when generating full-size preview', async () => {
 | 
				
			||||||
      mocks.systemMetadata.get.mockResolvedValue({
 | 
					      mocks.systemMetadata.get.mockResolvedValue({
 | 
				
			||||||
        image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } },
 | 
					        image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } },
 | 
				
			||||||
 | 
				
			|||||||
@ -271,7 +271,9 @@ export class MediaService extends BaseService {
 | 
				
			|||||||
    // Handle embedded preview extraction for RAW files
 | 
					    // Handle embedded preview extraction for RAW files
 | 
				
			||||||
    const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
 | 
					    const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
 | 
				
			||||||
    const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
 | 
					    const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
 | 
				
			||||||
    const generateFullsize = image.fullsize.enabled && !mimeTypes.isWebSupportedImage(asset.originalPath);
 | 
					    const generateFullsize =
 | 
				
			||||||
 | 
					      (image.fullsize.enabled || asset.exifInfo.projectionType == 'EQUIRECTANGULAR') &&
 | 
				
			||||||
 | 
					      !mimeTypes.isWebSupportedImage(asset.originalPath);
 | 
				
			||||||
    const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
 | 
					    const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { info, data, colorspace } = await this.decodeImage(
 | 
					    const { info, data, colorspace } = await this.decodeImage(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										39
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										39
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -866,4 +866,43 @@ export const assetStub = {
 | 
				
			|||||||
    stackId: null,
 | 
					    stackId: null,
 | 
				
			||||||
    visibility: AssetVisibility.Timeline,
 | 
					    visibility: AssetVisibility.Timeline,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					  panoramaTif: Object.freeze({
 | 
				
			||||||
 | 
					    id: 'asset-id',
 | 
				
			||||||
 | 
					    status: AssetStatus.Active,
 | 
				
			||||||
 | 
					    deviceAssetId: 'device-asset-id',
 | 
				
			||||||
 | 
					    fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
 | 
					    fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
 | 
					    owner: userStub.user1,
 | 
				
			||||||
 | 
					    ownerId: 'user-id',
 | 
				
			||||||
 | 
					    deviceId: 'device-id',
 | 
				
			||||||
 | 
					    originalPath: '/original/path.tif',
 | 
				
			||||||
 | 
					    checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
 | 
					    type: AssetType.Image,
 | 
				
			||||||
 | 
					    files,
 | 
				
			||||||
 | 
					    thumbhash: Buffer.from('blablabla', 'base64'),
 | 
				
			||||||
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
 | 
					    localDateTime: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
 | 
					    isFavorite: true,
 | 
				
			||||||
 | 
					    duration: null,
 | 
				
			||||||
 | 
					    isExternal: false,
 | 
				
			||||||
 | 
					    livePhotoVideo: null,
 | 
				
			||||||
 | 
					    livePhotoVideoId: null,
 | 
				
			||||||
 | 
					    sharedLinks: [],
 | 
				
			||||||
 | 
					    originalFileName: 'asset-id.tif',
 | 
				
			||||||
 | 
					    faces: [],
 | 
				
			||||||
 | 
					    deletedAt: null,
 | 
				
			||||||
 | 
					    sidecarPath: null,
 | 
				
			||||||
 | 
					    exifInfo: {
 | 
				
			||||||
 | 
					      fileSizeInByte: 5000,
 | 
				
			||||||
 | 
					      projectionType: 'EQUIRECTANGULAR',
 | 
				
			||||||
 | 
					    } as Exif,
 | 
				
			||||||
 | 
					    duplicateId: null,
 | 
				
			||||||
 | 
					    isOffline: false,
 | 
				
			||||||
 | 
					    updateId: '42',
 | 
				
			||||||
 | 
					    libraryId: null,
 | 
				
			||||||
 | 
					    stackId: null,
 | 
				
			||||||
 | 
					    visibility: AssetVisibility.Timeline,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { authManager } from '$lib/managers/auth-manager.svelte';
 | 
					  import { authManager } from '$lib/managers/auth-manager.svelte';
 | 
				
			||||||
  import { getAssetOriginalUrl } from '$lib/utils';
 | 
					  import { getAssetOriginalUrl, getAssetThumbnailUrl } from '$lib/utils';
 | 
				
			||||||
  import { isWebCompatibleImage } from '$lib/utils/asset-utils';
 | 
					  import { isWebCompatibleImage } from '$lib/utils/asset-utils';
 | 
				
			||||||
  import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
 | 
					  import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { LoadingSpinner } from '@immich/ui';
 | 
					  import { LoadingSpinner } from '@immich/ui';
 | 
				
			||||||
@ -25,7 +25,9 @@
 | 
				
			|||||||
  {:then [data, { default: PhotoSphereViewer }]}
 | 
					  {:then [data, { default: PhotoSphereViewer }]}
 | 
				
			||||||
    <PhotoSphereViewer
 | 
					    <PhotoSphereViewer
 | 
				
			||||||
      panorama={data}
 | 
					      panorama={data}
 | 
				
			||||||
      originalPanorama={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
 | 
					      originalPanorama={isWebCompatibleImage(asset)
 | 
				
			||||||
 | 
					        ? getAssetOriginalUrl(asset.id)
 | 
				
			||||||
 | 
					        : getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Fullsize, cacheKey: asset.thumbhash })}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  {:catch}
 | 
					  {:catch}
 | 
				
			||||||
    {$t('errors.failed_to_load_asset')}
 | 
					    {$t('errors.failed_to_load_asset')}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user