Skip to content

Commit

Permalink
feat(server,web): configure image format (immich-app#8581)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertalev authored Apr 7, 2024
1 parent 55b9acc commit 105a74c
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 35 deletions.
54 changes: 25 additions & 29 deletions server/src/services/media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
16 changes: 11 additions & 5 deletions server/src/services/media.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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."
Expand All @@ -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."
Expand Down

0 comments on commit 105a74c

Please sign in to comment.