From 6de35c84cef124081e0f4b439cfb2faf4512d548 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:03:12 -0400 Subject: [PATCH 1/4] allow different thumbnail and preview quality, better config structure --- server/src/config.ts | 23 +++++++----- server/src/dtos/system-config.dto.ts | 30 ++++++++------- server/src/interfaces/media.interface.ts | 9 +++-- ...7-SeparateQualityForThumbnailAndPreview.ts | 37 +++++++++++++++++++ server/src/services/media.service.spec.ts | 8 ++-- server/src/services/media.service.ts | 27 +++++--------- server/src/services/person.service.ts | 2 +- .../services/system-config.service.spec.ts | 15 +++++--- 8 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts diff --git a/server/src/config.ts b/server/src/config.ts index 1522371487e3b..3317351f9ff3a 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -20,6 +20,7 @@ import { VideoContainer, } from 'src/enum'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; +import { ImageOutputConfig } from 'src/interfaces/media.interface'; export interface SystemConfig { ffmpeg: { @@ -109,11 +110,8 @@ export interface SystemConfig { template: string; }; image: { - thumbnailFormat: ImageFormat; - thumbnailSize: number; - previewFormat: ImageFormat; - previewSize: number; - quality: number; + thumbnail: ImageOutputConfig; + preview: ImageOutputConfig; colorspace: Colorspace; extractEmbedded: boolean; }; @@ -259,11 +257,16 @@ export const defaults = Object.freeze({ template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, image: { - thumbnailFormat: ImageFormat.WEBP, - thumbnailSize: 250, - previewFormat: ImageFormat.JPEG, - previewSize: 1440, - quality: 80, + thumbnail: { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + }, + preview: { + format: ImageFormat.JPEG, + size: 1440, + quality: 80, + }, colorspace: Colorspace.P3, extractEmbedded: false, }, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 4a3ca37691604..c12a54cd613e6 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -473,33 +473,35 @@ export class SystemConfigThemeDto { customCss!: string; } -class SystemConfigImageDto { +class SystemConfigGeneratedImageDto { @IsEnum(ImageFormat) @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) - thumbnailFormat!: ImageFormat; + format!: ImageFormat; @IsInt() @Min(1) + @Max(100) @Type(() => Number) @ApiProperty({ type: 'integer' }) - thumbnailSize!: number; - - @IsEnum(ImageFormat) - @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) - previewFormat!: ImageFormat; + quality!: number; @IsInt() @Min(1) @Type(() => Number) @ApiProperty({ type: 'integer' }) - previewSize!: number; + size!: number; +} - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - quality!: number; +class SystemConfigImageDto { + @Type(() => SystemConfigGeneratedImageDto) + @ValidateNested() + @IsObject() + thumbnail!: SystemConfigGeneratedImageDto; + + @Type(() => SystemConfigGeneratedImageDto) + @ValidateNested() + @IsObject() + preview!: SystemConfigGeneratedImageDto; @IsEnum(Colorspace) @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 7193684e7acc1..64ba6236e80f0 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -10,11 +10,14 @@ export interface CropOptions { height: number; } -export interface ThumbnailOptions { - size: number; +export interface ImageOutputConfig { format: ImageFormat; - colorspace: string; quality: number; + size: number; +} + +export interface ThumbnailOptions extends ImageOutputConfig { + colorspace: string; crop?: CropOptions; processInvalidImages: boolean; } diff --git a/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts new file mode 100644 index 0000000000000..e02203997f723 --- /dev/null +++ b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeparateQualityForThumbnailAndPreview1727471863507 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update system_metadata + set value = jsonb_set(value, '{image}', jsonb_strip_nulls( + jsonb_build_object( + 'preview', jsonb_build_object( + 'format', value->'image'->'previewFormat', + 'quality', value->'image'->'quality', + 'size', value->'image'->'previewSize'), + 'thumbnail', jsonb_build_object( + 'format', value->'image'->'thumbnailFormat', + 'quality', value->'image'->'quality', + 'size', value->'image'->'thumbnailSize'), + 'extractEmbedded', value->'extractEmbedded', + 'colorspace', value->'colorspace' + ))) + where key = 'system-config'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update system_metadata + set value = jsonb_set(value, '{image}', jsonb_strip_nulls(jsonb_build_object( + 'previewFormat', value->'image'->'preview'->'format', + 'previewSize', value->'image'->'preview'->'size', + 'thumbnailFormat', value->'image'->'thumbnail'->'format', + 'thumbnailSize', value->'image'->'thumbnail'->'size', + 'extractEmbedded', value->'extractEmbedded', + 'colorspace', value->'colorspace', + 'quality', value->'image'->'preview'->'quality' + ))) + where key = 'system-config'`); + } +} diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index ddda8f64fc74f..c0903fa101412 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -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 }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 720bef6c7661b..1b69c5acd5504 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -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 { - 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 { - 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; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 7cb76d1a71535..651c8eebee54e 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -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; diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 8b4fb0bc2fd3c..514d8aa0f8d58 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -135,11 +135,16 @@ const updatedConfig = Object.freeze({ 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, }, From a877ee89a85f9d2e8ee61fa55527304cc57106c8 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:03:47 -0400 Subject: [PATCH 2/4] update web and api --- mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + .../system_config_generated_image_dto.dart | 118 ++++++++++++++ .../lib/model/system_config_image_dto.dart | 58 ++----- open-api/immich-openapi-specs.json | 48 +++--- open-api/typescript-sdk/src/fetch-client.ts | 12 +- .../settings/image/image-settings.svelte | 150 ++++++++++-------- web/src/lib/i18n/en.json | 17 +- 9 files changed, 268 insertions(+), 139 deletions(-) create mode 100644 mobile/openapi/lib/model/system_config_generated_image_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index fecbbf482be54..81827a9079e5a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -416,6 +416,7 @@ Class | Method | HTTP request | Description - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) + - [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 22b48df2fbcb1..8be44029805d5 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -229,6 +229,7 @@ part 'model/stack_update_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_faces_dto.dart'; +part 'model/system_config_generated_image_dto.dart'; part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 3db3297acb091..9e38eaf30a8a9 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -512,6 +512,8 @@ class ApiClient { return SystemConfigFFmpegDto.fromJson(value); case 'SystemConfigFacesDto': return SystemConfigFacesDto.fromJson(value); + case 'SystemConfigGeneratedImageDto': + return SystemConfigGeneratedImageDto.fromJson(value); case 'SystemConfigImageDto': return SystemConfigImageDto.fromJson(value); case 'SystemConfigJobDto': diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart new file mode 100644 index 0000000000000..2192a7cb0cbd5 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -0,0 +1,118 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigGeneratedImageDto { + /// Returns a new [SystemConfigGeneratedImageDto] instance. + SystemConfigGeneratedImageDto({ + required this.format, + required this.quality, + required this.size, + }); + + ImageFormat format; + + /// Minimum value: 1 + /// Maximum value: 100 + int quality; + + /// Minimum value: 1 + int size; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto && + other.format == format && + other.quality == quality && + other.size == size; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (format.hashCode) + + (quality.hashCode) + + (size.hashCode); + + @override + String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]'; + + Map toJson() { + final json = {}; + json[r'format'] = this.format; + json[r'quality'] = this.quality; + json[r'size'] = this.size; + return json; + } + + /// Returns a new [SystemConfigGeneratedImageDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigGeneratedImageDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigGeneratedImageDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigGeneratedImageDto( + format: ImageFormat.fromJson(json[r'format'])!, + quality: mapValueOfType(json, r'quality')!, + size: mapValueOfType(json, r'size')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigGeneratedImageDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigGeneratedImageDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigGeneratedImageDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigGeneratedImageDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'format', + 'quality', + 'size', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 681a8c00c3bc0..5309f7745c44d 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -15,64 +15,42 @@ class SystemConfigImageDto { SystemConfigImageDto({ required this.colorspace, required this.extractEmbedded, - required this.previewFormat, - required this.previewSize, - required this.quality, - required this.thumbnailFormat, - required this.thumbnailSize, + required this.preview, + required this.thumbnail, }); Colorspace colorspace; bool extractEmbedded; - ImageFormat previewFormat; + SystemConfigGeneratedImageDto preview; - /// Minimum value: 1 - int previewSize; - - /// Minimum value: 1 - /// Maximum value: 100 - int quality; - - ImageFormat thumbnailFormat; - - /// Minimum value: 1 - int thumbnailSize; + SystemConfigGeneratedImageDto thumbnail; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && other.colorspace == colorspace && other.extractEmbedded == extractEmbedded && - other.previewFormat == previewFormat && - other.previewSize == previewSize && - other.quality == quality && - other.thumbnailFormat == thumbnailFormat && - other.thumbnailSize == thumbnailSize; + other.preview == preview && + other.thumbnail == thumbnail; @override int get hashCode => // ignore: unnecessary_parenthesis (colorspace.hashCode) + (extractEmbedded.hashCode) + - (previewFormat.hashCode) + - (previewSize.hashCode) + - (quality.hashCode) + - (thumbnailFormat.hashCode) + - (thumbnailSize.hashCode); + (preview.hashCode) + + (thumbnail.hashCode); @override - String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]'; Map toJson() { final json = {}; json[r'colorspace'] = this.colorspace; json[r'extractEmbedded'] = this.extractEmbedded; - json[r'previewFormat'] = this.previewFormat; - json[r'previewSize'] = this.previewSize; - json[r'quality'] = this.quality; - json[r'thumbnailFormat'] = this.thumbnailFormat; - json[r'thumbnailSize'] = this.thumbnailSize; + json[r'preview'] = this.preview; + json[r'thumbnail'] = this.thumbnail; return json; } @@ -87,11 +65,8 @@ class SystemConfigImageDto { return SystemConfigImageDto( colorspace: Colorspace.fromJson(json[r'colorspace'])!, extractEmbedded: mapValueOfType(json, r'extractEmbedded')!, - previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, - previewSize: mapValueOfType(json, r'previewSize')!, - quality: mapValueOfType(json, r'quality')!, - thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!, - thumbnailSize: mapValueOfType(json, r'thumbnailSize')!, + preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!, + thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!, ); } return null; @@ -141,11 +116,8 @@ class SystemConfigImageDto { static const requiredKeys = { 'colorspace', 'extractEmbedded', - 'previewFormat', - 'previewSize', - 'quality', - 'thumbnailFormat', - 'thumbnailSize', + 'preview', + 'thumbnail', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6afd0d792ff34..1077762ac3a56 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11654,42 +11654,48 @@ ], "type": "object" }, - "SystemConfigImageDto": { + "SystemConfigGeneratedImageDto": { "properties": { - "colorspace": { - "$ref": "#/components/schemas/Colorspace" - }, - "extractEmbedded": { - "type": "boolean" - }, - "previewFormat": { + "format": { "$ref": "#/components/schemas/ImageFormat" }, - "previewSize": { - "minimum": 1, - "type": "integer" - }, "quality": { "maximum": 100, "minimum": 1, "type": "integer" }, - "thumbnailFormat": { - "$ref": "#/components/schemas/ImageFormat" - }, - "thumbnailSize": { + "size": { "minimum": 1, "type": "integer" } }, + "required": [ + "format", + "quality", + "size" + ], + "type": "object" + }, + "SystemConfigImageDto": { + "properties": { + "colorspace": { + "$ref": "#/components/schemas/Colorspace" + }, + "extractEmbedded": { + "type": "boolean" + }, + "preview": { + "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" + }, + "thumbnail": { + "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" + } + }, "required": [ "colorspace", "extractEmbedded", - "previewFormat", - "previewSize", - "quality", - "thumbnailFormat", - "thumbnailSize" + "preview", + "thumbnail" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index b1ae5d28764f2..e88f431e8c787 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1100,14 +1100,16 @@ export type SystemConfigFFmpegDto = { transcode: TranscodePolicy; twoPass: boolean; }; +export type SystemConfigGeneratedImageDto = { + format: ImageFormat; + quality: number; + size: number; +}; export type SystemConfigImageDto = { colorspace: Colorspace; extractEmbedded: boolean; - previewFormat: ImageFormat; - previewSize: number; - quality: number; - thumbnailFormat: ImageFormat; - thumbnailSize: number; + preview: SystemConfigGeneratedImageDto; + thumbnail: SystemConfigGeneratedImageDto; }; export type JobSettingsDto = { concurrency: number; diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index d6fc814b98e4c..b5e381d5f87a0 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -11,6 +11,7 @@ SettingInputFieldType, } from '$lib/components/shared-components/settings/setting-input-field.svelte'; import { t } from 'svelte-i18n'; + import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; export let savedConfig: SystemConfigDto; export let defaultConfig: SystemConfigDto; @@ -24,73 +25,96 @@
- + + - + - + + - + + - + + + + Date: Fri, 27 Sep 2024 19:25:29 -0400 Subject: [PATCH 3/4] wording --- web/src/lib/i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 9e9f78a61490d..a6c6723475db5 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -60,7 +60,7 @@ "image_prefer_embedded_preview_setting_description": "Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.", "image_prefer_wide_gamut": "Prefer wide gamut", "image_prefer_wide_gamut_setting_description": "Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.", - "image_preview_description": "Medium-size image with stripped metadata, used for viewing a single asset and for machine learning", + "image_preview_description": "Medium-size image with stripped metadata, used when viewing a single asset and for machine learning", "image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.", "image_preview_title": "Preview Settings", "image_quality": "Quality", From 19ec7b0b3e4c7525aa7b21476c3b6a760e81d308 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Sat, 28 Sep 2024 01:34:26 -0400 Subject: [PATCH 4/4] remove empty line? --- web/src/lib/i18n/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index a6c6723475db5..5ded90aad361e 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -955,7 +955,6 @@ "port": "Port", "preset": "Preset", "preview": "Preview", - "previous": "Previous", "previous_memory": "Previous memory", "previous_or_next_photo": "Previous or next photo",