feat(server): separate quality for thumbnail and preview images (#13006)

* allow different thumbnail and preview quality, better config structure

* update web and api

* wording

* remove empty line?
This commit is contained in:
Mert
2024-09-28 02:01:04 -04:00
committed by GitHub
parent 4248594ac5
commit 995f0fda47
17 changed files with 369 additions and 198 deletions

View File

@@ -285,7 +285,7 @@ describe(MediaService.name, () => {
});
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
systemMock.get.mockResolvedValue({ image: { previewFormat: format } });
systemMock.get.mockResolvedValue({ image: { preview: { format } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
@@ -307,7 +307,7 @@ describe(MediaService.name, () => {
});
it('should delete previous preview if different path', async () => {
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGeneratePreview({ id: assetStub.image.id });
@@ -464,7 +464,7 @@ describe(MediaService.name, () => {
it.each(Object.values(ImageFormat))(
'should generate a %s thumbnail for an image when specified',
async (format) => {
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: format } });
systemMock.get.mockResolvedValue({ image: { thumbnail: { format } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
@@ -487,7 +487,7 @@ describe(MediaService.name, () => {
);
it('should delete previous thumbnail if different path', async () => {
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } });
assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleGenerateThumbnail({ id: assetStub.image.id });

View File

@@ -10,7 +10,6 @@ import {
AssetType,
AudioCodec,
Colorspace,
ImageFormat,
LogLevel,
StorageFolder,
TranscodeHWAccel,
@@ -175,18 +174,15 @@ export class MediaService {
return JobStatus.FAILED;
}
await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat);
await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format);
await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
await this.storageCore.moveAssetVideo(asset);
return JobStatus.SUCCESS;
}
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
const [{ image }, [asset]] = await Promise.all([
this.configCore.getConfig({ withCache: true }),
this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
]);
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, files: true });
if (!asset) {
return JobStatus.FAILED;
}
@@ -195,7 +191,7 @@ export class MediaService {
return JobStatus.SKIPPED;
}
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW);
if (!previewPath) {
return JobStatus.SKIPPED;
}
@@ -213,9 +209,9 @@ export class MediaService {
return JobStatus.SUCCESS;
}
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType) {
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
const { size, format, quality } = image[type];
const path = StorageCore.getImagePath(asset, type, format);
this.storageCore.ensureFolders(path);
@@ -226,13 +222,13 @@ export class MediaService {
const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
try {
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size));
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
const imageOptions = {
format,
size,
colorspace,
quality: image.quality,
quality,
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
};
@@ -274,10 +270,7 @@ export class MediaService {
}
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
const [{ image }, [asset]] = await Promise.all([
this.configCore.getConfig({ withCache: true }),
this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
]);
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true, files: true });
if (!asset) {
return JobStatus.FAILED;
}
@@ -286,7 +279,7 @@ export class MediaService {
return JobStatus.SKIPPED;
}
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL);
if (!thumbnailPath) {
return JobStatus.SKIPPED;
}

View File

@@ -574,7 +574,7 @@ export class PersonService {
format: ImageFormat.JPEG,
size: FACE_THUMBNAIL_SIZE,
colorspace: image.colorspace,
quality: image.quality,
quality: image.thumbnail.quality,
crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
} as const;

View File

@@ -135,11 +135,16 @@ const updatedConfig = Object.freeze<SystemConfig>({
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
image: {
thumbnailFormat: ImageFormat.WEBP,
thumbnailSize: 250,
previewFormat: ImageFormat.JPEG,
previewSize: 1440,
quality: 80,
thumbnail: {
size: 250,
format: ImageFormat.WEBP,
quality: 80,
},
preview: {
size: 1440,
format: ImageFormat.JPEG,
quality: 80,
},
colorspace: Colorspace.P3,
extractEmbedded: false,
},