mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(web/server): webp thumbnail size configurable (#3598)
* feat(server/web): webp thumbnail size configurable * update api * add ui and fix test * lint * setting for jpeg size * feat: coerce to number * api * jpeg resolution --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
1812e8811b
commit
ddd4ec2d9e
26 changed files with 429 additions and 18 deletions
|
|
@ -6590,6 +6590,9 @@
|
|||
},
|
||||
"storageTemplate": {
|
||||
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
|
||||
},
|
||||
"thumbnail": {
|
||||
"$ref": "#/components/schemas/SystemConfigThumbnailDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -6597,7 +6600,8 @@
|
|||
"oauth",
|
||||
"passwordLogin",
|
||||
"storageTemplate",
|
||||
"job"
|
||||
"job",
|
||||
"thumbnail"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
|
@ -6828,6 +6832,21 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigThumbnailDto": {
|
||||
"properties": {
|
||||
"jpegSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"webpSize": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"webpSize",
|
||||
"jpegSize"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TagResponseDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export const JPEG_THUMBNAIL_SIZE = 1440;
|
||||
export const WEBP_THUMBNAIL_SIZE = 250;
|
||||
export const FACE_THUMBNAIL_SIZE = 250;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SI
|
|||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { JPEG_THUMBNAIL_SIZE, WEBP_THUMBNAIL_SIZE } from './media.constant';
|
||||
import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository';
|
||||
import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util';
|
||||
|
||||
|
|
@ -63,11 +62,12 @@ export class MediaService {
|
|||
const resizePath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
|
||||
this.storageRepository.mkdirSync(resizePath);
|
||||
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
|
||||
const { thumbnail } = await this.configCore.getConfig();
|
||||
|
||||
switch (asset.type) {
|
||||
case AssetType.IMAGE:
|
||||
await this.mediaRepository.resize(asset.originalPath, jpegThumbnailPath, {
|
||||
size: JPEG_THUMBNAIL_SIZE,
|
||||
size: thumbnail.jpegSize,
|
||||
format: 'jpeg',
|
||||
});
|
||||
this.logger.log(`Successfully generated image thumbnail ${asset.id}`);
|
||||
|
|
@ -80,7 +80,7 @@ export class MediaService {
|
|||
return false;
|
||||
}
|
||||
const { ffmpeg } = await this.configCore.getConfig();
|
||||
const config = { ...ffmpeg, targetResolution: JPEG_THUMBNAIL_SIZE.toString(), twoPass: false };
|
||||
const config = { ...ffmpeg, targetResolution: thumbnail.jpegSize.toString(), twoPass: false };
|
||||
const options = new ThumbnailConfig(config).getOptions(mainVideoStream);
|
||||
await this.mediaRepository.transcode(asset.originalPath, jpegThumbnailPath, options);
|
||||
this.logger.log(`Successfully generated video thumbnail ${asset.id}`);
|
||||
|
|
@ -100,7 +100,8 @@ export class MediaService {
|
|||
|
||||
const webpPath = asset.resizePath.replace('jpeg', 'webp').replace('jpg', 'webp');
|
||||
|
||||
await this.mediaRepository.resize(asset.resizePath, webpPath, { size: WEBP_THUMBNAIL_SIZE, format: 'webp' });
|
||||
const { thumbnail } = await this.configCore.getConfig();
|
||||
await this.mediaRepository.resize(asset.resizePath, webpPath, { size: thumbnail.webpSize, format: 'webp' });
|
||||
await this.assetRepository.save({ id: asset.id, webpPath });
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ export * from './system-config-ffmpeg.dto';
|
|||
export * from './system-config-oauth.dto';
|
||||
export * from './system-config-password-login.dto';
|
||||
export * from './system-config-storage-template.dto';
|
||||
export * from './system-config-thumbnail.dto';
|
||||
export * from './system-config.dto';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt } from 'class-validator';
|
||||
|
||||
export class SystemConfigThumbnailDto {
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
webpSize!: number;
|
||||
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
jpegSize!: number;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { SystemConfigThumbnailDto } from '@app/domain/system-config';
|
||||
import { SystemConfig } from '@app/infra/entities';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsObject, ValidateNested } from 'class-validator';
|
||||
|
|
@ -32,6 +33,11 @@ export class SystemConfigDto {
|
|||
@ValidateNested()
|
||||
@IsObject()
|
||||
job!: SystemConfigJobDto;
|
||||
|
||||
@Type(() => SystemConfigThumbnailDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
thumbnail!: SystemConfigThumbnailDto;
|
||||
}
|
||||
|
||||
export function mapConfig(config: SystemConfig): SystemConfigDto {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
storageTemplate: {
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
|
||||
thumbnail: {
|
||||
webpSize: 250,
|
||||
jpegSize: 1440,
|
||||
},
|
||||
});
|
||||
|
||||
const singleton = new Subject<SystemConfig>();
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
storageTemplate: {
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
thumbnail: {
|
||||
webpSize: 250,
|
||||
jpegSize: 1440,
|
||||
},
|
||||
});
|
||||
|
||||
describe(SystemConfigService.name, () => {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ export enum SystemConfigKey {
|
|||
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
|
||||
|
||||
STORAGE_TEMPLATE = 'storageTemplate.template',
|
||||
|
||||
THUMBNAIL_WEBP_SIZE = 'thumbnail.webpSize',
|
||||
THUMBNAIL_JPEG_SIZE = 'thumbnail.jpegSize',
|
||||
}
|
||||
|
||||
export enum TranscodePolicy {
|
||||
|
|
@ -121,4 +124,8 @@ export interface SystemConfig {
|
|||
storageTemplate: {
|
||||
template: string;
|
||||
};
|
||||
thumbnail: {
|
||||
webpSize: number;
|
||||
jpegSize: number;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export class MediaRepository implements IMediaRepository {
|
|||
private logger = new Logger(MediaRepository.name);
|
||||
|
||||
crop(input: string, options: CropOptions): Promise<Buffer> {
|
||||
return sharp(input, { failOnError: false })
|
||||
return sharp(input, { failOn: 'none' })
|
||||
.extract({
|
||||
left: options.left,
|
||||
top: options.top,
|
||||
|
|
@ -23,7 +23,7 @@ export class MediaRepository implements IMediaRepository {
|
|||
}
|
||||
|
||||
async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void> {
|
||||
await sharp(input, { failOnError: false })
|
||||
await sharp(input, { failOn: 'none' })
|
||||
.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true })
|
||||
.rotate()
|
||||
.toFormat(options.format)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue