1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

feat(server,web): configure image format (#8581)

This commit is contained in:
Mert 2024-04-07 12:44:34 -04:00 committed by GitHub
parent 55b9acca78
commit 105a74caca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 35 deletions

View File

@ -210,25 +210,21 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith();
});
it('should generate a thumbnail for an image', async () => {
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
assetMock.getByIds.mockResolvedValue([assetStub.image]);
const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
await sut.handleGeneratePreview({ id: assetStub.image.id });
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
expect(mediaMock.resize).toHaveBeenCalledWith(
'/original/path.jpg',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
{
size: 1440,
format: ImageFormat.JPEG,
quality: 80,
colorspace: Colorspace.SRGB,
},
);
expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id',
previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', previewPath, {
size: 1440,
format,
quality: 80,
colorspace: Colorspace.SRGB,
});
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath });
});
it('should generate a P3 thumbnail for a wide gamut image', async () => {
@ -342,25 +338,25 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith();
});
it('should generate a thumbnail', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
it.each(Object.values(ImageFormat))(
'should generate a %s thumbnail for an image when specified',
async (format) => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_THUMBNAIL_FORMAT, value: format }]);
assetMock.getByIds.mockResolvedValue([assetStub.image]);
const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
expect(mediaMock.resize).toHaveBeenCalledWith(
'/original/path.jpg',
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
{
format: ImageFormat.WEBP,
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', thumbnailPath, {
size: 250,
format,
quality: 80,
colorspace: Colorspace.SRGB,
},
);
expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id',
thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
});
});
});
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath });
},
);
});
it('should generate a P3 thumbnail for a wide gamut image', async () => {

View File

@ -167,12 +167,15 @@ export class MediaService {
}
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
const [{ image }, [asset]] = await Promise.all([
this.configCore.getConfig(),
this.assetRepository.getByIds([id], { exifInfo: true }),
]);
if (!asset) {
return JobStatus.FAILED;
}
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG);
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS;
}
@ -210,18 +213,21 @@ export class MediaService {
}
}
this.logger.log(
`Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} thumbnail for asset ${asset.id}`,
`Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} ${type} for asset ${asset.id}`,
);
return path;
}
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
const [{ image }, [asset]] = await Promise.all([
this.configCore.getConfig(),
this.assetRepository.getByIds([id], { exifInfo: true }),
]);
if (!asset) {
return JobStatus.FAILED;
}
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP);
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS;
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Colorspace, type SystemConfigDto } from '@immich/sdk';
import { Colorspace, ImageFormat, type SystemConfigDto } from '@immich/sdk';
import { isEqual } from 'lodash-es';
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
@ -24,6 +24,19 @@
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSelect
label="THUMBNAIL FORMAT"
desc="WebP produces smaller files than JPEG, but is slower to encode."
bind:value={config.image.thumbnailFormat}
options={[
{ value: ImageFormat.Jpeg, text: 'JPEG' },
{ value: ImageFormat.Webp, text: 'WebP' },
]}
name="format"
isEdited={config.image.thumbnailFormat !== savedConfig.image.thumbnailFormat}
{disabled}
/>
<SettingSelect
label="THUMBNAIL RESOLUTION"
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
@ -41,6 +54,19 @@
{disabled}
/>
<SettingSelect
label="PREVIEW FORMAT"
desc="WebP produces smaller files than JPEG, but is slower to encode."
bind:value={config.image.previewFormat}
options={[
{ value: ImageFormat.Jpeg, text: 'JPEG' },
{ value: ImageFormat.Webp, text: 'WebP' },
]}
name="format"
isEdited={config.image.previewFormat !== savedConfig.image.previewFormat}
{disabled}
/>
<SettingSelect
label="PREVIEW RESOLUTION"
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."