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:
Alex 2023-08-08 09:39:51 -05:00 committed by GitHub
parent 1812e8811b
commit ddd4ec2d9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 429 additions and 18 deletions

View file

@ -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": {

View file

@ -1,3 +1 @@
export const JPEG_THUMBNAIL_SIZE = 1440;
export const WEBP_THUMBNAIL_SIZE = 250;
export const FACE_THUMBNAIL_SIZE = 250;

View file

@ -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;

View file

@ -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';

View file

@ -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;
}

View file

@ -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 {

View file

@ -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>();

View file

@ -65,6 +65,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
storageTemplate: {
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
thumbnail: {
webpSize: 250,
jpegSize: 1440,
},
});
describe(SystemConfigService.name, () => {

View file

@ -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;
};
}

View file

@ -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)