Skip to content

Commit

Permalink
fix: load original image for gifs (#10252)
Browse files Browse the repository at this point in the history
  • Loading branch information
michelheusschen committed Jun 13, 2024
1 parent fb641c7 commit a54e01e
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 11 deletions.
10 changes: 9 additions & 1 deletion mobile/openapi/lib/model/asset_response_dto.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions open-api/immich-openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7708,6 +7708,9 @@
"originalFileName": {
"type": "string"
},
"originalMimeType": {
"type": "string"
},
"originalPath": {
"type": "string"
},
Expand Down Expand Up @@ -7782,6 +7785,7 @@
"isTrashed",
"localDateTime",
"originalFileName",
"originalMimeType",
"originalPath",
"ownerId",
"resized",
Expand Down
1 change: 1 addition & 0 deletions open-api/typescript-sdk/src/fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type AssetResponseDto = {
livePhotoVideoId?: string | null;
localDateTime: string;
originalFileName: string;
originalMimeType: string;
originalPath: string;
owner?: UserResponseDto;
ownerId: string;
Expand Down
4 changes: 4 additions & 0 deletions server/src/dtos/asset-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { mimeTypes } from 'src/utils/mime-types';

export class SanitizedAssetResponseDto {
id!: string;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType;
thumbhash!: string | null;
originalMimeType!: string;
resized!: boolean;
localDateTime!: Date;
duration!: string;
Expand Down Expand Up @@ -87,6 +89,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id,
type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime,
resized: !!entity.previewPath,
Expand All @@ -107,6 +110,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
type: entity.type,
originalPath: entity.originalPath,
originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
resized: !!entity.previewPath,
thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt,
Expand Down
2 changes: 2 additions & 0 deletions server/test/fixtures/shared-link.stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const assetResponse: AssetResponseDto = {
ownerId: 'user_id_1',
deviceId: 'device_id_1',
type: AssetType.VIDEO,
originalMimeType: 'image/jpeg',
originalPath: 'fake_path/jpeg',
originalFileName: 'asset_1.jpeg',
resized: false,
Expand Down Expand Up @@ -82,6 +83,7 @@ const assetResponse: AssetResponseDto = {
const assetResponseWithoutMetadata = {
id: 'id_1',
type: AssetType.VIDEO,
originalMimeType: 'image/jpeg',
resized: false,
thumbhash: null,
localDateTime: today,
Expand Down
49 changes: 49 additions & 0 deletions web/src/lib/components/asset-viewer/photo-viewer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import PhotoViewer from '$lib/components/asset-viewer/photo-viewer.svelte';
import * as utils from '$lib/utils';
import { AssetMediaSize } from '@immich/sdk';
import { assetFactory } from '@test-data/factories/asset-factory';
import { render } from '@testing-library/svelte';
import type { MockInstance } from 'vitest';

vi.mock('$lib/utils', async (originalImport) => {
const meta = await originalImport<typeof import('$lib/utils')>();
return {
...meta,
getAssetOriginalUrl: vi.fn(),
getAssetThumbnailUrl: vi.fn(),
};
});

describe('PhotoViewer component', () => {
let getAssetOriginalUrlSpy: MockInstance;
let getAssetThumbnailUrlSpy: MockInstance;

beforeAll(() => {
getAssetOriginalUrlSpy = vi.spyOn(utils, 'getAssetOriginalUrl');
getAssetThumbnailUrlSpy = vi.spyOn(utils, 'getAssetThumbnailUrl');
});

afterEach(() => {
vi.resetAllMocks();
});

it('loads the thumbnail', () => {
const asset = assetFactory.build({ originalPath: 'image.jpg', originalMimeType: 'image/jpeg' });
render(PhotoViewer, { asset });

expect(getAssetThumbnailUrlSpy).toBeCalledWith({
id: asset.id,
size: AssetMediaSize.Preview,
checksum: asset.checksum,
});
expect(getAssetOriginalUrlSpy).not.toBeCalled();
});

it('loads the original image for gifs', () => {
const asset = assetFactory.build({ originalPath: 'image.gif', originalMimeType: 'image/gif' });
render(PhotoViewer, { asset });

expect(getAssetThumbnailUrlSpy).not.toBeCalled();
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, checksum: asset.checksum });
});
});
3 changes: 2 additions & 1 deletion web/src/lib/components/asset-viewer/photo-viewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
$: useOriginalByDefault = isWebCompatible && $alwaysLoadOriginalFile;
$: useOriginalImage = useOriginalByDefault || forceUseOriginal;
// when true, will force loading of the original image
$: forceUseOriginal = forceUseOriginal || ($photoZoomState.currentZoom > 1 && isWebCompatible);
$: forceUseOriginal =
forceUseOriginal || asset.originalMimeType === 'image/gif' || ($photoZoomState.currentZoom > 1 && isWebCompatible);
$: preload(useOriginalImage, preloadAssets);
$: imageLoaderUrl = getAssetUrl(asset.id, useOriginalImage, asset.checksum);
Expand Down
18 changes: 9 additions & 9 deletions web/src/lib/utils/asset-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,20 +257,20 @@ export function getAssetRatio(asset: AssetResponseDto) {
}

// list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
const supportedImageExtensions = new Set(['apng', 'avif', 'gif', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png', 'webp']);
const supportedImageMimeTypes = new Set([
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/webp',
]);

/**
* Returns true if the asset is an image supported by web browsers, false otherwise
*/
export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
// originalPath is undefined when public shared link has metadata option turned off
if (!asset.originalPath) {
return false;
}

const imgExtension = getFilenameExtension(asset.originalPath);

return supportedImageExtensions.has(imgExtension);
return supportedImageMimeTypes.has(asset.originalMimeType);
}

export const getAssetType = (type: AssetTypeEnum) => {
Expand Down
1 change: 1 addition & 0 deletions web/src/test-data/factories/asset-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
type: Sync.each(() => faker.helpers.enumValue(AssetTypeEnum)),
originalPath: Sync.each(() => faker.system.filePath()),
originalFileName: Sync.each(() => faker.system.fileName()),
originalMimeType: Sync.each(() => faker.system.mimeType()),
resized: true,
thumbhash: Sync.each(() => faker.string.alphanumeric(28)),
fileCreatedAt: Sync.each(() => faker.date.past().toISOString()),
Expand Down

0 comments on commit a54e01e

Please sign in to comment.