feat: generate progressive JPEGs for thumbnails (#25463)

This commit is contained in:
Min Idzelis
2026-01-23 19:35:07 -05:00
committed by GitHub
parent 4fedae4150
commit 357ec1394a
13 changed files with 186 additions and 4 deletions

View File

@@ -104,6 +104,8 @@
"image_preview_description": "Medium-size image with stripped metadata, used when 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_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_preview_title": "Preview Settings",
"image_progressive": "Progressive",
"image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on WebP images.",
"image_quality": "Quality", "image_quality": "Quality",
"image_resolution": "Resolution", "image_resolution": "Resolution",
"image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.", "image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.",

View File

@@ -15,6 +15,7 @@ class SystemConfigGeneratedFullsizeImageDto {
SystemConfigGeneratedFullsizeImageDto({ SystemConfigGeneratedFullsizeImageDto({
required this.enabled, required this.enabled,
required this.format, required this.format,
required this.progressive,
required this.quality, required this.quality,
}); });
@@ -22,6 +23,8 @@ class SystemConfigGeneratedFullsizeImageDto {
ImageFormat format; ImageFormat format;
bool progressive;
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 100 /// Maximum value: 100
int quality; int quality;
@@ -30,6 +33,7 @@ class SystemConfigGeneratedFullsizeImageDto {
bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedFullsizeImageDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedFullsizeImageDto &&
other.enabled == enabled && other.enabled == enabled &&
other.format == format && other.format == format &&
other.progressive == progressive &&
other.quality == quality; other.quality == quality;
@override @override
@@ -37,15 +41,17 @@ class SystemConfigGeneratedFullsizeImageDto {
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(enabled.hashCode) + (enabled.hashCode) +
(format.hashCode) + (format.hashCode) +
(progressive.hashCode) +
(quality.hashCode); (quality.hashCode);
@override @override
String toString() => 'SystemConfigGeneratedFullsizeImageDto[enabled=$enabled, format=$format, quality=$quality]'; String toString() => 'SystemConfigGeneratedFullsizeImageDto[enabled=$enabled, format=$format, progressive=$progressive, quality=$quality]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'enabled'] = this.enabled; json[r'enabled'] = this.enabled;
json[r'format'] = this.format; json[r'format'] = this.format;
json[r'progressive'] = this.progressive;
json[r'quality'] = this.quality; json[r'quality'] = this.quality;
return json; return json;
} }
@@ -61,6 +67,7 @@ class SystemConfigGeneratedFullsizeImageDto {
return SystemConfigGeneratedFullsizeImageDto( return SystemConfigGeneratedFullsizeImageDto(
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
format: ImageFormat.fromJson(json[r'format'])!, format: ImageFormat.fromJson(json[r'format'])!,
progressive: mapValueOfType<bool>(json, r'progressive')!,
quality: mapValueOfType<int>(json, r'quality')!, quality: mapValueOfType<int>(json, r'quality')!,
); );
} }
@@ -111,6 +118,7 @@ class SystemConfigGeneratedFullsizeImageDto {
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'enabled', 'enabled',
'format', 'format',
'progressive',
'quality', 'quality',
}; };
} }

View File

@@ -14,12 +14,15 @@ class SystemConfigGeneratedImageDto {
/// Returns a new [SystemConfigGeneratedImageDto] instance. /// Returns a new [SystemConfigGeneratedImageDto] instance.
SystemConfigGeneratedImageDto({ SystemConfigGeneratedImageDto({
required this.format, required this.format,
required this.progressive,
required this.quality, required this.quality,
required this.size, required this.size,
}); });
ImageFormat format; ImageFormat format;
bool progressive;
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 100 /// Maximum value: 100
int quality; int quality;
@@ -30,6 +33,7 @@ class SystemConfigGeneratedImageDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto &&
other.format == format && other.format == format &&
other.progressive == progressive &&
other.quality == quality && other.quality == quality &&
other.size == size; other.size == size;
@@ -37,15 +41,17 @@ class SystemConfigGeneratedImageDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(format.hashCode) + (format.hashCode) +
(progressive.hashCode) +
(quality.hashCode) + (quality.hashCode) +
(size.hashCode); (size.hashCode);
@override @override
String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]'; String toString() => 'SystemConfigGeneratedImageDto[format=$format, progressive=$progressive, quality=$quality, size=$size]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'format'] = this.format; json[r'format'] = this.format;
json[r'progressive'] = this.progressive;
json[r'quality'] = this.quality; json[r'quality'] = this.quality;
json[r'size'] = this.size; json[r'size'] = this.size;
return json; return json;
@@ -61,6 +67,7 @@ class SystemConfigGeneratedImageDto {
return SystemConfigGeneratedImageDto( return SystemConfigGeneratedImageDto(
format: ImageFormat.fromJson(json[r'format'])!, format: ImageFormat.fromJson(json[r'format'])!,
progressive: mapValueOfType<bool>(json, r'progressive')!,
quality: mapValueOfType<int>(json, r'quality')!, quality: mapValueOfType<int>(json, r'quality')!,
size: mapValueOfType<int>(json, r'size')!, size: mapValueOfType<int>(json, r'size')!,
); );
@@ -111,6 +118,7 @@ class SystemConfigGeneratedImageDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'format', 'format',
'progressive',
'quality', 'quality',
'size', 'size',
}; };

View File

@@ -22624,6 +22624,9 @@
} }
] ]
}, },
"progressive": {
"type": "boolean"
},
"quality": { "quality": {
"maximum": 100, "maximum": 100,
"minimum": 1, "minimum": 1,
@@ -22633,6 +22636,7 @@
"required": [ "required": [
"enabled", "enabled",
"format", "format",
"progressive",
"quality" "quality"
], ],
"type": "object" "type": "object"
@@ -22646,6 +22650,9 @@
} }
] ]
}, },
"progressive": {
"type": "boolean"
},
"quality": { "quality": {
"maximum": 100, "maximum": 100,
"minimum": 1, "minimum": 1,
@@ -22658,6 +22665,7 @@
}, },
"required": [ "required": [
"format", "format",
"progressive",
"quality", "quality",
"size" "size"
], ],

View File

@@ -1538,10 +1538,12 @@ export type SystemConfigFFmpegDto = {
export type SystemConfigGeneratedFullsizeImageDto = { export type SystemConfigGeneratedFullsizeImageDto = {
enabled: boolean; enabled: boolean;
format: ImageFormat; format: ImageFormat;
progressive: boolean;
quality: number; quality: number;
}; };
export type SystemConfigGeneratedImageDto = { export type SystemConfigGeneratedImageDto = {
format: ImageFormat; format: ImageFormat;
progressive: boolean;
quality: number; quality: number;
size: number; size: number;
}; };

View File

@@ -319,11 +319,13 @@ export const defaults = Object.freeze<SystemConfig>({
format: ImageFormat.Webp, format: ImageFormat.Webp,
size: 250, size: 250,
quality: 80, quality: 80,
progressive: false,
}, },
preview: { preview: {
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
size: 1440, size: 1440,
quality: 80, quality: 80,
progressive: false,
}, },
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
extractEmbedded: false, extractEmbedded: false,
@@ -331,6 +333,7 @@ export const defaults = Object.freeze<SystemConfig>({
enabled: false, enabled: false,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
}, },
}, },
newVersionCheck: { newVersionCheck: {

View File

@@ -585,6 +585,9 @@ class SystemConfigGeneratedImageDto {
@Type(() => Number) @Type(() => Number)
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
size!: number; size!: number;
@ValidateBoolean()
progressive!: boolean;
} }
class SystemConfigGeneratedFullsizeImageDto { class SystemConfigGeneratedFullsizeImageDto {
@@ -600,6 +603,9 @@ class SystemConfigGeneratedFullsizeImageDto {
@Type(() => Number) @Type(() => Number)
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
quality!: number; quality!: number;
@ValidateBoolean()
progressive!: boolean;
} }
export class SystemConfigImageDto { export class SystemConfigImageDto {

View File

@@ -176,6 +176,7 @@ export class MediaRepository {
quality: options.quality, quality: options.quality,
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0', chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
progressive: options.progressive,
}); });
await decoded.toFile(output); await decoded.toFile(output);

View File

@@ -352,6 +352,7 @@ describe(MediaService.name, () => {
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
size: 1440, size: 1440,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -365,6 +366,7 @@ describe(MediaService.name, () => {
format: ImageFormat.Webp, format: ImageFormat.Webp,
size: 250, size: 250,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -575,6 +577,7 @@ describe(MediaService.name, () => {
format, format,
size: 1440, size: 1440,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -588,6 +591,7 @@ describe(MediaService.name, () => {
format: ImageFormat.Webp, format: ImageFormat.Webp,
size: 250, size: 250,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -622,6 +626,7 @@ describe(MediaService.name, () => {
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
size: 1440, size: 1440,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -635,6 +640,7 @@ describe(MediaService.name, () => {
format, format,
size: 250, size: 250,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -643,6 +649,58 @@ describe(MediaService.name, () => {
); );
}); });
it('should generate progressive JPEG for preview when enabled', async () => {
mocks.systemMetadata.get.mockResolvedValue({
image: { preview: { progressive: true }, thumbnail: { progressive: false } },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({
format: ImageFormat.Jpeg,
progressive: true,
}),
expect.stringContaining('preview.jpeg'),
);
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({
format: ImageFormat.Webp,
progressive: false,
}),
expect.stringContaining('thumbnail.webp'),
);
});
it('should generate progressive JPEG for thumbnail when enabled', async () => {
mocks.systemMetadata.get.mockResolvedValue({
image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({
format: ImageFormat.Jpeg,
progressive: false,
}),
expect.stringContaining('preview.jpeg'),
);
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({
format: ImageFormat.Jpeg,
progressive: true,
}),
expect.stringContaining('thumbnail.jpeg'),
);
});
it('should delete previous thumbnail if different path', async () => { it('should delete previous thumbnail if different path', async () => {
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
@@ -776,6 +834,7 @@ describe(MediaService.name, () => {
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
size: 1440, size: 1440,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -807,6 +866,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Webp, format: ImageFormat.Webp,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -820,6 +880,7 @@ describe(MediaService.name, () => {
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
size: 1440, size: 1440,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -849,6 +910,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -861,6 +923,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
size: 1440, size: 1440,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
@@ -892,6 +955,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -948,6 +1012,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.Srgb, colorspace: Colorspace.Srgb,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -987,6 +1052,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Webp, format: ImageFormat.Webp,
quality: 90, quality: 90,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
raw: rawInfo, raw: rawInfo,
edits: [], edits: [],
@@ -994,6 +1060,27 @@ describe(MediaService.name, () => {
expect.any(String), expect.any(String),
); );
}); });
it('should generate progressive JPEG for fullsize when enabled', async () => {
mocks.systemMetadata.get.mockResolvedValue({
image: { fullsize: { enabled: true, format: ImageFormat.Jpeg, progressive: true } },
});
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
rawBuffer,
expect.objectContaining({
format: ImageFormat.Jpeg,
progressive: true,
}),
expect.stringContaining('fullsize.jpeg'),
);
});
}); });
describe('handleAssetEditThumbnailGeneration', () => { describe('handleAssetEditThumbnailGeneration', () => {
@@ -1198,6 +1285,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',
@@ -1242,6 +1330,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',
@@ -1284,6 +1373,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',
@@ -1326,6 +1416,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',
@@ -1368,6 +1459,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',
@@ -1410,6 +1502,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',
@@ -1457,6 +1550,7 @@ describe(MediaService.name, () => {
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
edits: [ edits: [
{ {
action: 'crop', action: 'crop',

View File

@@ -351,6 +351,7 @@ export class MediaService extends BaseService {
const fullsizeOptions = { const fullsizeOptions = {
format: image.fullsize.format, format: image.fullsize.format,
quality: image.fullsize.quality, quality: image.fullsize.quality,
progressive: image.fullsize.progressive,
...thumbnailOptions, ...thumbnailOptions,
}; };
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath)); promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
@@ -434,6 +435,7 @@ export class MediaService extends BaseService {
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
raw: info, raw: info,
quality: image.thumbnail.quality, quality: image.thumbnail.quality,
progressive: false,
processInvalidImages: false, processInvalidImages: false,
size: FACE_THUMBNAIL_SIZE, size: FACE_THUMBNAIL_SIZE,
edits: [ edits: [

View File

@@ -167,13 +167,15 @@ const updatedConfig = Object.freeze<SystemConfig>({
size: 250, size: 250,
format: ImageFormat.Webp, format: ImageFormat.Webp,
quality: 80, quality: 80,
progressive: false,
}, },
preview: { preview: {
size: 1440, size: 1440,
format: ImageFormat.Jpeg, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
progressive: false,
}, },
fullsize: { enabled: false, format: ImageFormat.Jpeg, quality: 80 }, fullsize: { enabled: false, format: ImageFormat.Jpeg, quality: 80, progressive: false },
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
extractEmbedded: false, extractEmbedded: false,
}, },

View File

@@ -36,12 +36,14 @@ export type FullsizeImageOptions = {
format: ImageFormat; format: ImageFormat;
quality: number; quality: number;
enabled: boolean; enabled: boolean;
progressive: boolean;
}; };
export type ImageOptions = { export type ImageOptions = {
format: ImageFormat; format: ImageFormat;
quality: number; quality: number;
size: number; size: number;
progressive: boolean;
}; };
export type RawImageInfo = { export type RawImageInfo = {
@@ -62,7 +64,7 @@ export interface DecodeToBufferOptions extends DecodeImageOptions {
orientation?: ExifOrientation; orientation?: ExifOrientation;
} }
export type GenerateThumbnailOptions = Pick<ImageOptions, 'format' | 'quality'> & DecodeToBufferOptions; export type GenerateThumbnailOptions = Pick<ImageOptions, 'format' | 'quality' | 'progressive'> & DecodeToBufferOptions;
export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo }; export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo };

View File

@@ -37,6 +37,11 @@
name="format" name="format"
isEdited={configToEdit.image.thumbnail.format !== config.image.thumbnail.format} isEdited={configToEdit.image.thumbnail.format !== config.image.thumbnail.format}
{disabled} {disabled}
onSelect={(value) => {
if (value === ImageFormat.Webp) {
configToEdit.image.thumbnail.progressive = false;
}
}}
/> />
<SettingSelect <SettingSelect
@@ -64,6 +69,15 @@
isEdited={configToEdit.image.thumbnail.quality !== config.image.thumbnail.quality} isEdited={configToEdit.image.thumbnail.quality !== config.image.thumbnail.quality}
{disabled} {disabled}
/> />
<SettingSwitch
title={$t('admin.image_progressive')}
subtitle={$t('admin.image_progressive_description')}
checked={configToEdit.image.thumbnail.progressive}
onToggle={(isChecked) => (configToEdit.image.thumbnail.progressive = isChecked)}
isEdited={configToEdit.image.thumbnail.progressive !== config.image.thumbnail.progressive}
disabled={disabled || configToEdit.image.thumbnail.format === ImageFormat.Webp}
/>
</SettingAccordion> </SettingAccordion>
<SettingAccordion <SettingAccordion
@@ -82,6 +96,11 @@
name="format" name="format"
isEdited={configToEdit.image.preview.format !== config.image.preview.format} isEdited={configToEdit.image.preview.format !== config.image.preview.format}
{disabled} {disabled}
onSelect={(value) => {
if (value === ImageFormat.Webp) {
configToEdit.image.preview.progressive = false;
}
}}
/> />
<SettingSelect <SettingSelect
@@ -108,6 +127,15 @@
isEdited={configToEdit.image.preview.quality !== config.image.preview.quality} isEdited={configToEdit.image.preview.quality !== config.image.preview.quality}
{disabled} {disabled}
/> />
<SettingSwitch
title={$t('admin.image_progressive')}
subtitle={$t('admin.image_progressive_description')}
checked={configToEdit.image.preview.progressive}
onToggle={(isChecked) => (configToEdit.image.preview.progressive = isChecked)}
isEdited={configToEdit.image.preview.progressive !== config.image.preview.progressive}
disabled={disabled || configToEdit.image.preview.format === ImageFormat.Webp}
/>
</SettingAccordion> </SettingAccordion>
<SettingAccordion <SettingAccordion
@@ -137,6 +165,11 @@
name="format" name="format"
isEdited={configToEdit.image.fullsize.format !== config.image.fullsize.format} isEdited={configToEdit.image.fullsize.format !== config.image.fullsize.format}
disabled={disabled || !configToEdit.image.fullsize.enabled} disabled={disabled || !configToEdit.image.fullsize.enabled}
onSelect={(value) => {
if (value === ImageFormat.Webp) {
configToEdit.image.fullsize.progressive = false;
}
}}
/> />
<SettingInputField <SettingInputField
@@ -147,6 +180,17 @@
isEdited={configToEdit.image.fullsize.quality !== config.image.fullsize.quality} isEdited={configToEdit.image.fullsize.quality !== config.image.fullsize.quality}
disabled={disabled || !configToEdit.image.fullsize.enabled} disabled={disabled || !configToEdit.image.fullsize.enabled}
/> />
<SettingSwitch
title={$t('admin.image_progressive')}
subtitle={$t('admin.image_progressive_description')}
checked={configToEdit.image.fullsize.progressive}
onToggle={(isChecked) => (configToEdit.image.fullsize.progressive = isChecked)}
isEdited={configToEdit.image.fullsize.progressive !== config.image.fullsize.progressive}
disabled={disabled ||
!configToEdit.image.fullsize.enabled ||
configToEdit.image.fullsize.format === ImageFormat.Webp}
/>
</SettingAccordion> </SettingAccordion>
<div class="mt-4"> <div class="mt-4">