1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

fix: load original image for gifs (#10252)

This commit is contained in:
Michel Heusschen 2024-06-13 12:57:46 +02:00 committed by GitHub
parent fb641c74be
commit a54e01ef2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 81 additions and 11 deletions

View file

@ -31,6 +31,7 @@ class AssetResponseDto {
this.livePhotoVideoId, this.livePhotoVideoId,
required this.localDateTime, required this.localDateTime,
required this.originalFileName, required this.originalFileName,
required this.originalMimeType,
required this.originalPath, required this.originalPath,
this.owner, this.owner,
required this.ownerId, required this.ownerId,
@ -91,6 +92,8 @@ class AssetResponseDto {
String originalFileName; String originalFileName;
String originalMimeType;
String originalPath; String originalPath;
/// ///
@ -151,6 +154,7 @@ class AssetResponseDto {
other.livePhotoVideoId == livePhotoVideoId && other.livePhotoVideoId == livePhotoVideoId &&
other.localDateTime == localDateTime && other.localDateTime == localDateTime &&
other.originalFileName == originalFileName && other.originalFileName == originalFileName &&
other.originalMimeType == originalMimeType &&
other.originalPath == originalPath && other.originalPath == originalPath &&
other.owner == owner && other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
@ -187,6 +191,7 @@ class AssetResponseDto {
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(localDateTime.hashCode) + (localDateTime.hashCode) +
(originalFileName.hashCode) + (originalFileName.hashCode) +
(originalMimeType.hashCode) +
(originalPath.hashCode) + (originalPath.hashCode) +
(owner == null ? 0 : owner!.hashCode) + (owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
@ -203,7 +208,7 @@ class AssetResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -241,6 +246,7 @@ class AssetResponseDto {
} }
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String(); json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String();
json[r'originalFileName'] = this.originalFileName; json[r'originalFileName'] = this.originalFileName;
json[r'originalMimeType'] = this.originalMimeType;
json[r'originalPath'] = this.originalPath; json[r'originalPath'] = this.originalPath;
if (this.owner != null) { if (this.owner != null) {
json[r'owner'] = this.owner; json[r'owner'] = this.owner;
@ -304,6 +310,7 @@ class AssetResponseDto {
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', r'')!, localDateTime: mapDateTime(json, r'localDateTime', r'')!,
originalFileName: mapValueOfType<String>(json, r'originalFileName')!, originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalMimeType: mapValueOfType<String>(json, r'originalMimeType')!,
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']), owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
@ -379,6 +386,7 @@ class AssetResponseDto {
'isTrashed', 'isTrashed',
'localDateTime', 'localDateTime',
'originalFileName', 'originalFileName',
'originalMimeType',
'originalPath', 'originalPath',
'ownerId', 'ownerId',
'resized', 'resized',

View file

@ -7708,6 +7708,9 @@
"originalFileName": { "originalFileName": {
"type": "string" "type": "string"
}, },
"originalMimeType": {
"type": "string"
},
"originalPath": { "originalPath": {
"type": "string" "type": "string"
}, },
@ -7782,6 +7785,7 @@
"isTrashed", "isTrashed",
"localDateTime", "localDateTime",
"originalFileName", "originalFileName",
"originalMimeType",
"originalPath", "originalPath",
"ownerId", "ownerId",
"resized", "resized",

View file

@ -182,6 +182,7 @@ export type AssetResponseDto = {
livePhotoVideoId?: string | null; livePhotoVideoId?: string | null;
localDateTime: string; localDateTime: string;
originalFileName: string; originalFileName: string;
originalMimeType: string;
originalPath: string; originalPath: string;
owner?: UserResponseDto; owner?: UserResponseDto;
ownerId: string; ownerId: string;

View file

@ -13,12 +13,14 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { mimeTypes } from 'src/utils/mime-types';
export class SanitizedAssetResponseDto { export class SanitizedAssetResponseDto {
id!: string; id!: string;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType; type!: AssetType;
thumbhash!: string | null; thumbhash!: string | null;
originalMimeType!: string;
resized!: boolean; resized!: boolean;
localDateTime!: Date; localDateTime!: Date;
duration!: string; duration!: string;
@ -87,6 +89,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
const sanitizedAssetResponse: SanitizedAssetResponseDto = { const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id, id: entity.id,
type: entity.type, type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime, localDateTime: entity.localDateTime,
resized: !!entity.previewPath, resized: !!entity.previewPath,
@ -107,6 +110,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
type: entity.type, type: entity.type,
originalPath: entity.originalPath, originalPath: entity.originalPath,
originalFileName: entity.originalFileName, originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName),
resized: !!entity.previewPath, resized: !!entity.previewPath,
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt, fileCreatedAt: entity.fileCreatedAt,

View file

@ -52,6 +52,7 @@ const assetResponse: AssetResponseDto = {
ownerId: 'user_id_1', ownerId: 'user_id_1',
deviceId: 'device_id_1', deviceId: 'device_id_1',
type: AssetType.VIDEO, type: AssetType.VIDEO,
originalMimeType: 'image/jpeg',
originalPath: 'fake_path/jpeg', originalPath: 'fake_path/jpeg',
originalFileName: 'asset_1.jpeg', originalFileName: 'asset_1.jpeg',
resized: false, resized: false,
@ -82,6 +83,7 @@ const assetResponse: AssetResponseDto = {
const assetResponseWithoutMetadata = { const assetResponseWithoutMetadata = {
id: 'id_1', id: 'id_1',
type: AssetType.VIDEO, type: AssetType.VIDEO,
originalMimeType: 'image/jpeg',
resized: false, resized: false,
thumbhash: null, thumbhash: null,
localDateTime: today, localDateTime: today,

View file

@ -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 });
});
});

View file

@ -38,7 +38,8 @@
$: useOriginalByDefault = isWebCompatible && $alwaysLoadOriginalFile; $: useOriginalByDefault = isWebCompatible && $alwaysLoadOriginalFile;
$: useOriginalImage = useOriginalByDefault || forceUseOriginal; $: useOriginalImage = useOriginalByDefault || forceUseOriginal;
// when true, will force loading of the original image // 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); $: preload(useOriginalImage, preloadAssets);
$: imageLoaderUrl = getAssetUrl(asset.id, useOriginalImage, asset.checksum); $: imageLoaderUrl = getAssetUrl(asset.id, useOriginalImage, asset.checksum);

View file

@ -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 // 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 * Returns true if the asset is an image supported by web browsers, false otherwise
*/ */
export function isWebCompatibleImage(asset: AssetResponseDto): boolean { export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
// originalPath is undefined when public shared link has metadata option turned off return supportedImageMimeTypes.has(asset.originalMimeType);
if (!asset.originalPath) {
return false;
}
const imgExtension = getFilenameExtension(asset.originalPath);
return supportedImageExtensions.has(imgExtension);
} }
export const getAssetType = (type: AssetTypeEnum) => { export const getAssetType = (type: AssetTypeEnum) => {

View file

@ -11,6 +11,7 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
type: Sync.each(() => faker.helpers.enumValue(AssetTypeEnum)), type: Sync.each(() => faker.helpers.enumValue(AssetTypeEnum)),
originalPath: Sync.each(() => faker.system.filePath()), originalPath: Sync.each(() => faker.system.filePath()),
originalFileName: Sync.each(() => faker.system.fileName()), originalFileName: Sync.each(() => faker.system.fileName()),
originalMimeType: Sync.each(() => faker.system.mimeType()),
resized: true, resized: true,
thumbhash: Sync.each(() => faker.string.alphanumeric(28)), thumbhash: Sync.each(() => faker.string.alphanumeric(28)),
fileCreatedAt: Sync.each(() => faker.date.past().toISOString()), fileCreatedAt: Sync.each(() => faker.date.past().toISOString()),