mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: generate progressive JPEGs for thumbnails (#25463)
This commit is contained in:
parent
4fedae4150
commit
357ec1394a
13 changed files with 168 additions and 2 deletions
|
|
@ -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.",
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue