diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 3ebf07305..9285c12fc 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts @@ -1,10 +1,9 @@ -import { remove, rename } from 'fs-extra' +import { copy, readFile, remove, rename } from 'fs-extra' +import * as Jimp from 'jimp' import { extname } from 'path' import { convertWebPToJPG, processGIF } from './ffmpeg-utils' import { logger } from './logger' -const Jimp = require('jimp') - async function processImage ( path: string, destination: string, @@ -23,7 +22,7 @@ async function processImage ( if (extension === '.gif') { await processGIF(path, destination, newSize) } else { - await jimpProcessor(path, destination, newSize) + await jimpProcessor(path, destination, newSize, extension) } if (keepOriginal !== true) await remove(path) @@ -37,11 +36,12 @@ export { // --------------------------------------------------------------------------- -async function jimpProcessor (path: string, destination: string, newSize: { width: number, height: number }) { - let jimpInstance: any +async function jimpProcessor (path: string, destination: string, newSize: { width: number, height: number }, inputExt: string) { + let jimpInstance: Jimp + const inputBuffer = await readFile(path) try { - jimpInstance = await Jimp.read(path) + jimpInstance = await Jimp.read(inputBuffer) } catch (err) { logger.debug('Cannot read %s with jimp. Try to convert the image using ffmpeg first.', path, { err }) @@ -54,8 +54,34 @@ async function jimpProcessor (path: string, destination: string, newSize: { widt await remove(destination) + // Optimization if the source file has the appropriate size + if (await skipProcessing({ jimpInstance, newSize, imageBytes: inputBuffer.byteLength, inputExt, outputExt: extname(destination) })) { + return copy(path, destination) + } + await jimpInstance .resize(newSize.width, newSize.height) .quality(80) .writeAsync(destination) } + +function skipProcessing (options: { + jimpInstance: Jimp + newSize: { width: number, height: number } + imageBytes: number + inputExt: string + outputExt: string +}) { + const { jimpInstance, newSize, imageBytes, inputExt, outputExt } = options + const { width, height } = newSize + + if (jimpInstance.getWidth() > width || jimpInstance.getHeight() > height) return false + if (inputExt !== outputExt) return false + + const kB = 1000 + + if (height >= 1000) return imageBytes <= 200 * kB + if (height >= 500) return imageBytes <= 100 * kB + + return imageBytes <= 15 * kB +} diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 5d0c9f742..4bad8d6ca 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts @@ -1,5 +1,4 @@ -import chaiJsonSchema = require('chai-json-schema') -import { copy, move } from 'fs-extra' +import { copy } from 'fs-extra' import { join } from 'path' import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' diff --git a/server/tests/fixtures/thumbnail-big.jpg b/server/tests/fixtures/thumbnail-big.jpg new file mode 100644 index 000000000..537720d24 Binary files /dev/null and b/server/tests/fixtures/thumbnail-big.jpg differ diff --git a/server/tests/fixtures/thumbnail.png b/server/tests/fixtures/thumbnail.png new file mode 100644 index 000000000..b331aba3b Binary files /dev/null and b/server/tests/fixtures/thumbnail.png differ diff --git a/server/tests/helpers/image.ts b/server/tests/helpers/image.ts new file mode 100644 index 000000000..54911697f --- /dev/null +++ b/server/tests/helpers/image.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import 'mocha' +import { readFile, remove } from 'fs-extra' +import { join } from 'path' +import { processImage } from '../../../server/helpers/image-utils' +import { buildAbsoluteFixturePath, root } from '../../../shared/extra-utils' +import { expect } from 'chai' + +async function checkBuffers (path1: string, path2: string, equals: boolean) { + const [ buf1, buf2 ] = await Promise.all([ + readFile(path1), + readFile(path2) + ]) + + if (equals) { + expect(buf1.equals(buf2)).to.be.true + } else { + expect(buf1.equals(buf2)).to.be.false + } +} + +describe('Image helpers', function () { + const imageDestDir = join(root(), 'test-images') + const imageDest = join(imageDestDir, 'test.jpg') + const thumbnailSize = { width: 223, height: 122 } + + it('Should skip processing if the source image is okay', async function () { + const input = buildAbsoluteFixturePath('thumbnail.jpg') + await processImage(input, imageDest, thumbnailSize, true) + + await checkBuffers(input, imageDest, true) + }) + + it('Should not skip processing if the source image does not have the appropriate extension', async function () { + const input = buildAbsoluteFixturePath('thumbnail.png') + await processImage(input, imageDest, thumbnailSize, true) + + await checkBuffers(input, imageDest, false) + }) + + it('Should not skip processing if the source image does not have the appropriate size', async function () { + const input = buildAbsoluteFixturePath('preview.jpg') + await processImage(input, imageDest, thumbnailSize, true) + + await checkBuffers(input, imageDest, false) + }) + + it('Should not skip processing if the source image does not have the appropriate size', async function () { + const input = buildAbsoluteFixturePath('thumbnail-big.jpg') + await processImage(input, imageDest, thumbnailSize, true) + + await checkBuffers(input, imageDest, false) + }) + + after(async function () { + await remove(imageDest) + }) +}) diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts index 03b971770..66db93c99 100644 --- a/server/tests/helpers/index.ts +++ b/server/tests/helpers/index.ts @@ -1,3 +1,4 @@ +import './image' import './core-utils' import './comment-model' import './request'