1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-04 02:46:47 +01:00

test: full-size previews

This commit is contained in:
Eli Gao 2024-12-24 00:43:35 +08:00
parent dcf2ce38e3
commit 2fe37a2607
3 changed files with 182 additions and 17 deletions

View file

@ -1,4 +1,5 @@
import { SystemConfig } from 'src/config';
import { AssetMediaSize } from 'src/dtos/asset-media.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import {
@ -275,12 +276,20 @@ describe(MediaService.name, () => {
describe('handleGenerateThumbnails', () => {
let rawBuffer: Buffer;
let fullsizeBuffer: Buffer;
let rawInfo: RawImageInfo;
beforeEach(() => {
fullsizeBuffer = Buffer.from('embedded image data');
rawBuffer = Buffer.from('image data');
rawInfo = { width: 100, height: 100, channels: 3 };
mediaMock.decodeImage.mockResolvedValue({ data: rawBuffer, info: rawInfo });
mediaMock.decodeImage.mockImplementation((path) =>
Promise.resolve(
path.includes(AssetMediaSize.FULLSIZE)
? { data: fullsizeBuffer, info: rawInfo }
: { data: rawBuffer, info: rawInfo },
),
);
});
it('should skip thumbnail generation if asset not found', async () => {
@ -627,7 +636,7 @@ describe(MediaService.name, () => {
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
const convertedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
const convertedPath = mediaMock.extract.mock.lastCall?.[1].toString();
expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
expect(mediaMock.decodeImage).toHaveBeenCalledWith(convertedPath, {
colorspace: Colorspace.P3,
@ -644,17 +653,14 @@ describe(MediaService.name, () => {
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
const convertedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({ size: 1440 }),
convertedPath,
);
expect(convertedPath).toMatch(/-converted\.jpeg$/);
const extractedPath = mediaMock.extract.mock.lastCall?.[1].toString();
expect(extractedPath).toMatch(/-fullsize\.jpeg$/);
expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
colorspace: Colorspace.P3,
processInvalidImages: false,
size: 1440,
});
});
@ -668,6 +674,7 @@ describe(MediaService.name, () => {
expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
colorspace: Colorspace.P3,
processInvalidImages: false,
size: 1440,
});
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
});
@ -683,6 +690,7 @@ describe(MediaService.name, () => {
expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
colorspace: Colorspace.P3,
processInvalidImages: false,
size: 1440,
});
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
});
@ -700,12 +708,7 @@ describe(MediaService.name, () => {
expect.objectContaining({ processInvalidImages: true }),
);
expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(3);
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({ processInvalidImages: true }),
'upload/thumbs/user-id/as/se/asset-id-converted.jpeg',
);
expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({ processInvalidImages: true }),
@ -726,6 +729,128 @@ describe(MediaService.name, () => {
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
vi.unstubAllEnvs();
});
it('should generate full-size preview using embedded JPEG from RAW images when extractEmbedded is true', async () => {
systemMock.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: true } });
mediaMock.extract.mockResolvedValue(true);
mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
assetMock.getById.mockResolvedValue(assetStub.imageDng);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
const extractedPath = mediaMock.extract.mock.lastCall?.[1].toString();
expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
expect(mediaMock.decodeImage).toHaveBeenCalledWith(extractedPath, {
colorspace: Colorspace.P3,
processInvalidImages: false,
});
expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
fullsizeBuffer,
{
colorspace: Colorspace.P3,
format: ImageFormat.JPEG,
size: 1440,
quality: 80,
processInvalidImages: false,
raw: rawInfo,
},
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
);
});
it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => {
systemMock.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
mediaMock.extract.mockResolvedValue(true);
mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
assetMock.getById.mockResolvedValue(assetStub.imageDng);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
colorspace: Colorspace.P3,
processInvalidImages: false,
});
expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(3);
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
{
colorspace: Colorspace.P3,
format: ImageFormat.JPEG,
quality: 80,
processInvalidImages: false,
raw: rawInfo,
},
'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg',
);
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
{
colorspace: Colorspace.P3,
format: ImageFormat.JPEG,
quality: 80,
size: 1440,
processInvalidImages: false,
raw: rawInfo,
},
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
);
});
it('should generate full-size preview from non-web-friendly images', async () => {
systemMock.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
mediaMock.extract.mockResolvedValue(true);
mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
assetMock.getById.mockResolvedValue(assetStub.imageHif);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, {
colorspace: Colorspace.P3,
processInvalidImages: false,
});
expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(3);
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
{
colorspace: Colorspace.P3,
format: ImageFormat.JPEG,
quality: 80,
processInvalidImages: false,
raw: rawInfo,
},
'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg',
);
});
it('should skip generating full-size preview for web-friendly images', async () => {
systemMock.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
mediaMock.extract.mockResolvedValue(true);
mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
expect(mediaMock.decodeImage).toHaveBeenCalledOnce();
expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
colorspace: Colorspace.SRGB,
processInvalidImages: false,
size: 1440,
});
expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2);
expect(mediaMock.generateThumbnail).not.toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
'upload/thumbs/user-id/as/se/asset-id-fullsize.jpeg',
);
});
});
describe('handleQueueVideoConversion', () => {

View file

@ -151,9 +151,9 @@ const updatedConfig = Object.freeze<SystemConfig>({
format: ImageFormat.JPEG,
quality: 80,
},
fullsize: { enabled: false, format: ImageFormat.JPEG, quality: 80 },
colorspace: Colorspace.P3,
extractEmbedded: false,
fullsizePreview: false,
},
newVersionCheck: {
enabled: true,

View file

@ -774,7 +774,47 @@ export const assetStub = {
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.jpg',
originalFileName: 'asset-id.dng',
faces: [],
deletedAt: null,
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
profileDescription: 'Adobe RGB',
bitsPerSample: 14,
} as ExifEntity,
duplicateId: null,
isOffline: false,
}),
imageHif: Object.freeze<AssetEntity>({
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.hif',
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,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.hif',
faces: [],
deletedAt: null,
sidecarPath: null,