From 10dfb3a5798ad115a9d054cc3b5ff194acdfa30a Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Wed, 28 Dec 2022 16:49:45 +0100 Subject: [PATCH] Store animated images as lossless webp --- .../managers/image/image-converter.service.ts | 32 +++++++++++-------- .../managers/image/image-processor.service.ts | 26 ++++++++------- backend/src/workers/sharp/sharp.message.ts | 4 +++ backend/src/workers/sharp/universal-sharp.ts | 13 +++++--- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/backend/src/managers/image/image-converter.service.ts b/backend/src/managers/image/image-converter.service.ts index 742c1be..9e12795 100644 --- a/backend/src/managers/image/image-converter.service.ts +++ b/backend/src/managers/image/image-converter.service.ts @@ -2,21 +2,28 @@ import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; import { - FileType, - SupportedFileTypeCategory, + FileType, + SupportedFileTypeCategory, } from 'picsur-shared/dist/dto/mimes.dto'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { - AsyncFailable, - Fail, - FT, - HasFailed, + AsyncFailable, + Fail, + FT, + HasFailed, } from 'picsur-shared/dist/types/failable'; import { SharpOptions } from 'sharp'; import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service.js'; import { SharpWrapper } from '../../workers/sharp.wrapper.js'; import { ImageResult } from './imageresult.js'; +interface InternalConvertOptions { + lossless?: boolean; + effort?: number; +} + +export type ConvertOptions = ImageRequestParams & InternalConvertOptions; + @Injectable() export class ImageConverterService { constructor(private readonly sysPref: SysPreferenceDbService) {} @@ -25,7 +32,7 @@ export class ImageConverterService { image: Buffer, sourceFiletype: FileType, targetFiletype: FileType, - options: ImageRequestParams, + options: ConvertOptions, ): AsyncFailable { if ( sourceFiletype.identifier === targetFiletype.identifier && @@ -37,23 +44,22 @@ export class ImageConverterService { }; } - if (targetFiletype.category === SupportedFileTypeCategory.Image) { - return this.convertStill(image, sourceFiletype, targetFiletype, options); - } else if ( + if ( + targetFiletype.category === SupportedFileTypeCategory.Image || targetFiletype.category === SupportedFileTypeCategory.Animation ) { - return this.convertStill(image, sourceFiletype, targetFiletype, options); + return this.convertImage(image, sourceFiletype, targetFiletype, options); //return this.convertAnimation(image, targetmime, options); } else { return Fail(FT.SysValidation, 'Unsupported mime type'); } } - private async convertStill( + private async convertImage( image: Buffer, sourceFiletype: FileType, targetFiletype: FileType, - options: ImageRequestParams, + options: ConvertOptions, ): AsyncFailable { const [memLimit, timeLimit] = await Promise.all([ this.sysPref.getNumberPreference(SysPreference.ConversionMemoryLimit), diff --git a/backend/src/managers/image/image-processor.service.ts b/backend/src/managers/image/image-processor.service.ts index d8ec80c..92a9149 100644 --- a/backend/src/managers/image/image-processor.service.ts +++ b/backend/src/managers/image/image-processor.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; import { - FileType, - ImageFileType, - SupportedFileTypeCategory, + FileType, + ImageFileType, + SupportedFileTypeCategory, } from 'picsur-shared/dist/dto/mimes.dto'; import { - AsyncFailable, - Fail, - FT, - HasFailed, + AsyncFailable, + Fail, + FT, + HasFailed, } from 'picsur-shared/dist/types/failable'; import { ParseFileType } from 'picsur-shared/dist/util/parse-mime'; import { ImageConverterService } from './image-converter.service.js'; @@ -46,10 +46,12 @@ export class ImageProcessorService { image: Buffer, filetype: FileType, ): AsyncFailable { - // Webps and gifs are stored as is for now - return { - image: image, - filetype: filetype.identifier, - }; + const outputFileType = ParseFileType(AnimFileType.WEBP); + if (HasFailed(outputFileType)) return outputFileType; + + return this.imageConverter.convert(image, filetype, outputFileType, { + lossless: true, + effort: 0, + }); } } diff --git a/backend/src/workers/sharp/sharp.message.ts b/backend/src/workers/sharp/sharp.message.ts index 7dd6d2d..16eb85f 100644 --- a/backend/src/workers/sharp/sharp.message.ts +++ b/backend/src/workers/sharp/sharp.message.ts @@ -26,6 +26,10 @@ export type SharpWorkerOperation = export interface SharpWorkerFinishOptions { quality?: number; + + // Only for internal use + lossless?: boolean; + effort?: number; } // Messages diff --git a/backend/src/workers/sharp/universal-sharp.ts b/backend/src/workers/sharp/universal-sharp.ts index 0a39e57..7644a3f 100644 --- a/backend/src/workers/sharp/universal-sharp.ts +++ b/backend/src/workers/sharp/universal-sharp.ts @@ -2,10 +2,11 @@ import { BMPdecode, BMPencode } from 'bmp-img'; import { AnimFileType, FileType, - ImageFileType, + ImageFileType } from 'picsur-shared/dist/dto/mimes.dto'; import { QOIdecode, QOIencode } from 'qoi-img'; import sharp, { Sharp, SharpOptions } from 'sharp'; +import { SharpWorkerFinishOptions } from './sharp.message'; export interface SharpResult { data: Buffer; @@ -72,9 +73,7 @@ function qoiSharpIn(image: Buffer, options?: SharpOptions) { export async function UniversalSharpOut( image: Sharp, filetype: FileType, - options?: { - quality?: number; - }, + options?: SharpWorkerFinishOptions, ): Promise { let result: SharpResult | undefined; @@ -103,7 +102,11 @@ export async function UniversalSharpOut( case ImageFileType.WEBP: case AnimFileType.WEBP: result = await image - .webp({ quality: options?.quality }) + .webp({ + quality: options?.quality, + lossless: options?.lossless, + effort: options?.effort, + }) .toBuffer({ resolveWithObject: true }); break; case AnimFileType.GIF: