mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(server): wide gamut thumbnails (#3658)
This commit is contained in:
@@ -76,6 +76,8 @@ export enum SystemConfigKey {
|
||||
|
||||
THUMBNAIL_WEBP_SIZE = 'thumbnail.webpSize',
|
||||
THUMBNAIL_JPEG_SIZE = 'thumbnail.jpegSize',
|
||||
THUMBNAIL_QUALITY = 'thumbnail.quality',
|
||||
THUMBNAIL_COLORSPACE = 'thumbnail.colorspace',
|
||||
}
|
||||
|
||||
export enum TranscodePolicy {
|
||||
@@ -117,6 +119,11 @@ export enum CQMode {
|
||||
ICQ = 'icq',
|
||||
}
|
||||
|
||||
export enum Colorspace {
|
||||
SRGB = 'srgb',
|
||||
P3 = 'p3',
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
@@ -179,5 +186,7 @@ export interface SystemConfig {
|
||||
thumbnail: {
|
||||
webpSize: number;
|
||||
jpegSize: number;
|
||||
quality: number;
|
||||
colorspace: Colorspace;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoInfo } from '@app/domain';
|
||||
import { Colorspace } from '@app/infra/entities';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||
import fs from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { Writable } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
|
||||
@@ -11,7 +13,7 @@ sharp.concurrency(0);
|
||||
export class MediaRepository implements IMediaRepository {
|
||||
private logger = new Logger(MediaRepository.name);
|
||||
|
||||
crop(input: string, options: CropOptions): Promise<Buffer> {
|
||||
crop(input: string | Buffer, options: CropOptions): Promise<Buffer> {
|
||||
return sharp(input, { failOn: 'none' })
|
||||
.extract({
|
||||
left: options.left,
|
||||
@@ -23,10 +25,25 @@ export class MediaRepository implements IMediaRepository {
|
||||
}
|
||||
|
||||
async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void> {
|
||||
await sharp(input, { failOn: 'none' })
|
||||
let colorProfile = options.colorspace;
|
||||
if (options.colorspace !== Colorspace.SRGB) {
|
||||
try {
|
||||
const { space } = await sharp(input).metadata();
|
||||
// if the image is already in srgb, keep it that way
|
||||
if (space === 'srgb') {
|
||||
colorProfile = Colorspace.SRGB;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`Could not determine colorspace of image, defaulting to ${colorProfile} profile`);
|
||||
}
|
||||
}
|
||||
const chromaSubsampling = options.quality >= 80 ? '4:4:4' : '4:2:0'; // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
|
||||
sharp(input, { failOn: 'none' })
|
||||
.pipelineColorspace('rgb16')
|
||||
.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true })
|
||||
.rotate()
|
||||
.toFormat(options.format)
|
||||
.withMetadata({ icc: colorProfile })
|
||||
.toFormat(options.format, { quality: options.quality, chromaSubsampling })
|
||||
.toFile(output);
|
||||
}
|
||||
|
||||
@@ -61,7 +78,7 @@ export class MediaRepository implements IMediaRepository {
|
||||
};
|
||||
}
|
||||
|
||||
transcode(input: string, output: string, options: TranscodeOptions): Promise<void> {
|
||||
transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> {
|
||||
if (!options.twoPass) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg(input, { niceness: 10 })
|
||||
@@ -77,6 +94,10 @@ export class MediaRepository implements IMediaRepository {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof output !== 'string') {
|
||||
throw new Error('Two-pass transcoding does not support writing to a stream');
|
||||
}
|
||||
|
||||
// two-pass allows for precise control of bitrate at the cost of running twice
|
||||
// recommended for vp9 for better quality and compression
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Reference in New Issue
Block a user