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:
parent
fb641c74be
commit
a54e01ef2f
9 changed files with 81 additions and 11 deletions
10
mobile/openapi/lib/model/asset_response_dto.dart
generated
10
mobile/openapi/lib/model/asset_response_dto.dart
generated
|
@ -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',
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
2
server/test/fixtures/shared-link.stub.ts
vendored
2
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -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,
|
||||||
|
|
49
web/src/lib/components/asset-viewer/photo-viewer.spec.ts
Normal file
49
web/src/lib/components/asset-viewer/photo-viewer.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
Loading…
Add table
Reference in a new issue