mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
feat: optionally generate thumbnails for invalid images (#11126)
This commit is contained in:
parent
c77702279c
commit
d37e8ede3b
@ -38,17 +38,18 @@ Regardless of filesystem, it is not recommended to use a network share for your
|
|||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
| Variable | Description | Default | Containers | Workers |
|
| Variable | Description | Default | Containers | Workers |
|
||||||
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
| :---------------------------------- | :-------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||||
| `TZ` | Timezone | | server | microservices |
|
| `TZ` | Timezone | | server | microservices |
|
||||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
|
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
|
||||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||||
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
|
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
|
||||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||||
|
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||||
|
|
||||||
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
|
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
|
||||||
It only need to be set if the Immich deployment method is changing.
|
It only need to be set if the Immich deployment method is changing.
|
||||||
|
@ -16,6 +16,7 @@ export interface ThumbnailOptions {
|
|||||||
colorspace: string;
|
colorspace: string;
|
||||||
quality: number;
|
quality: number;
|
||||||
crop?: CropOptions;
|
crop?: CropOptions;
|
||||||
|
processInvalidImages: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoStreamInfo {
|
export interface VideoStreamInfo {
|
||||||
|
@ -45,7 +45,8 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
|
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
|
||||||
const pipeline = sharp(input, { failOn: 'error', limitInputPixels: false })
|
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
|
||||||
|
const pipeline = sharp(input, { failOn: options.processInvalidImages ? 'none' : 'error', limitInputPixels: false })
|
||||||
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
|
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
|
||||||
.rotate();
|
.rotate();
|
||||||
|
|
||||||
|
@ -296,6 +296,7 @@ describe(MediaService.name, () => {
|
|||||||
format,
|
format,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.SRGB,
|
colorspace: Colorspace.SRGB,
|
||||||
|
processInvalidImages: false,
|
||||||
});
|
});
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath });
|
||||||
});
|
});
|
||||||
@ -326,6 +327,7 @@ describe(MediaService.name, () => {
|
|||||||
format: ImageFormat.JPEG,
|
format: ImageFormat.JPEG,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
@ -468,6 +470,7 @@ describe(MediaService.name, () => {
|
|||||||
format,
|
format,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.SRGB,
|
colorspace: Colorspace.SRGB,
|
||||||
|
processInvalidImages: false,
|
||||||
});
|
});
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath });
|
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath });
|
||||||
},
|
},
|
||||||
@ -498,6 +501,7 @@ describe(MediaService.name, () => {
|
|||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
@ -524,6 +528,7 @@ describe(MediaService.name, () => {
|
|||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@ -548,6 +553,7 @@ describe(MediaService.name, () => {
|
|||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@ -570,6 +576,7 @@ describe(MediaService.name, () => {
|
|||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
||||||
@ -590,11 +597,34 @@ describe(MediaService.name, () => {
|
|||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should process invalid images if enabled', async () => {
|
||||||
|
vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true');
|
||||||
|
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
||||||
|
|
||||||
|
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
|
||||||
|
assetStub.imageDng.originalPath,
|
||||||
|
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
||||||
|
{
|
||||||
|
format: ImageFormat.WEBP,
|
||||||
|
size: 250,
|
||||||
|
quality: 80,
|
||||||
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
describe('handleGenerateThumbhash', () => {
|
describe('handleGenerateThumbhash', () => {
|
||||||
it('should skip thumbhash generation if asset not found', async () => {
|
it('should skip thumbhash generation if asset not found', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
|
@ -199,7 +199,13 @@ export class MediaService {
|
|||||||
try {
|
try {
|
||||||
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
|
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
|
||||||
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
||||||
const imageOptions = { format, size, colorspace, quality: image.quality };
|
const imageOptions = {
|
||||||
|
format,
|
||||||
|
size,
|
||||||
|
colorspace,
|
||||||
|
quality: image.quality,
|
||||||
|
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
|
||||||
|
};
|
||||||
|
|
||||||
const outputPath = useExtracted ? extractedPath : asset.originalPath;
|
const outputPath = useExtracted ? extractedPath : asset.originalPath;
|
||||||
await this.mediaRepository.generateThumbnail(outputPath, path, imageOptions);
|
await this.mediaRepository.generateThumbnail(outputPath, path, imageOptions);
|
||||||
|
@ -958,6 +958,7 @@ describe(PersonService.name, () => {
|
|||||||
width: 274,
|
width: 274,
|
||||||
height: 274,
|
height: 274,
|
||||||
},
|
},
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(personMock.update).toHaveBeenCalledWith({
|
expect(personMock.update).toHaveBeenCalledWith({
|
||||||
@ -987,6 +988,7 @@ describe(PersonService.name, () => {
|
|||||||
width: 510,
|
width: 510,
|
||||||
height: 510,
|
height: 510,
|
||||||
},
|
},
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1012,6 +1014,7 @@ describe(PersonService.name, () => {
|
|||||||
width: 408,
|
width: 408,
|
||||||
height: 408,
|
height: 408,
|
||||||
},
|
},
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1038,6 +1041,7 @@ describe(PersonService.name, () => {
|
|||||||
width: 588,
|
width: 588,
|
||||||
height: 588,
|
height: 588,
|
||||||
},
|
},
|
||||||
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -559,6 +559,7 @@ export class PersonService {
|
|||||||
colorspace: image.colorspace,
|
colorspace: image.colorspace,
|
||||||
quality: image.quality,
|
quality: image.quality,
|
||||||
crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
|
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;
|
} as const;
|
||||||
|
|
||||||
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
|
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
|
||||||
|
Loading…
Reference in New Issue
Block a user