1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-21 03:02:44 +01: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(); 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]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
await sut.handleGeneratePreview({ id: assetStub.image.id }); await sut.handleGeneratePreview({ id: assetStub.image.id });
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
expect(mediaMock.resize).toHaveBeenCalledWith( expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', previewPath, {
'/original/path.jpg', size: 1440,
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', format,
{ quality: 80,
size: 1440, colorspace: Colorspace.SRGB,
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(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath });
}); });
it('should generate a P3 thumbnail for a wide gamut image', async () => { it('should generate a P3 thumbnail for a wide gamut image', async () => {
@ -342,25 +338,25 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith(); expect(assetMock.update).not.toHaveBeenCalledWith();
}); });
it('should generate a thumbnail', async () => { it.each(Object.values(ImageFormat))(
assetMock.getByIds.mockResolvedValue([assetStub.image]); 'should generate a %s thumbnail for an image when specified',
await sut.handleGenerateThumbnail({ id: assetStub.image.id }); 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( await sut.handleGenerateThumbnail({ id: assetStub.image.id });
'/original/path.jpg',
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
{ expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', thumbnailPath, {
format: ImageFormat.WEBP,
size: 250, size: 250,
format,
quality: 80, quality: 80,
colorspace: Colorspace.SRGB, colorspace: Colorspace.SRGB,
}, });
); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath });
expect(assetMock.update).toHaveBeenCalledWith({ },
id: 'asset-id', );
thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
});
});
}); });
it('should generate a P3 thumbnail for a wide gamut image', async () => { 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> { 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) { if (!asset) {
return JobStatus.FAILED; 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 }); await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
@ -210,18 +213,21 @@ export class MediaService {
} }
} }
this.logger.log( 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; return path;
} }
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> { 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) { if (!asset) {
return JobStatus.FAILED; 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 }); await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Colorspace, type SystemConfigDto } from '@immich/sdk'; import { Colorspace, ImageFormat, type SystemConfigDto } from '@immich/sdk';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@ -24,6 +24,19 @@
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault> <form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4"> <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 <SettingSelect
label="THUMBNAIL RESOLUTION" 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." 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} {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 <SettingSelect
label="PREVIEW RESOLUTION" 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." 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."