]>
Commit | Line | Data |
---|---|---|
1664bc60 | 1 | import { copy, readFile, remove, rename } from 'fs-extra' |
41fb13c3 | 2 | import Jimp, { read } from 'jimp' |
c729caf6 | 3 | import { join } from 'path' |
0628157f C |
4 | import { getLowercaseExtension } from '@shared/core-utils' |
5 | import { buildUUID } from '@shared/extra-utils' | |
c729caf6 C |
6 | import { convertWebPToJPG, generateThumbnailFromVideo, processGIF } from './ffmpeg/ffmpeg-images' |
7 | import { logger, loggerTagsFactory } from './logger' | |
8 | ||
9 | const lTags = loggerTagsFactory('image-utils') | |
18782242 | 10 | |
84531547 | 11 | function generateImageFilename (extension = '.jpg') { |
d4a8e7a6 | 12 | return buildUUID() + extension |
84531547 C |
13 | } |
14 | ||
ac81d1a0 | 15 | async function processImage ( |
2fb5b3a5 | 16 | path: string, |
ac81d1a0 | 17 | destination: string, |
e8bafea3 C |
18 | newSize: { width: number, height: number }, |
19 | keepOriginal = false | |
ac81d1a0 | 20 | ) { |
ea54cd04 | 21 | const extension = getLowercaseExtension(path) |
123f6193 | 22 | |
f619de0e C |
23 | if (path === destination) { |
24 | throw new Error('Jimp/FFmpeg needs an input path different that the output path.') | |
25 | } | |
26 | ||
27 | logger.debug('Processing image %s to %s.', path, destination) | |
28 | ||
123f6193 K |
29 | // Use FFmpeg to process GIF |
30 | if (extension === '.gif') { | |
f619de0e C |
31 | await processGIF(path, destination, newSize) |
32 | } else { | |
1664bc60 | 33 | await jimpProcessor(path, destination, newSize, extension) |
123f6193 K |
34 | } |
35 | ||
f619de0e C |
36 | if (keepOriginal !== true) await remove(path) |
37 | } | |
a8a63227 | 38 | |
c729caf6 C |
39 | async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { |
40 | const pendingImageName = 'pending-' + imageName | |
41 | const pendingImagePath = join(folder, pendingImageName) | |
42 | ||
43 | try { | |
44 | await generateThumbnailFromVideo(fromPath, folder, imageName) | |
45 | ||
46 | const destination = join(folder, imageName) | |
47 | await processImage(pendingImagePath, destination, size) | |
48 | } catch (err) { | |
49 | logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() }) | |
50 | ||
51 | try { | |
52 | await remove(pendingImagePath) | |
53 | } catch (err) { | |
54 | logger.debug('Cannot remove pending image path after generation error.', { err, ...lTags() }) | |
55 | } | |
56 | } | |
57 | } | |
58 | ||
f619de0e | 59 | // --------------------------------------------------------------------------- |
a8a63227 | 60 | |
f619de0e | 61 | export { |
84531547 | 62 | generateImageFilename, |
c729caf6 | 63 | generateImageFromVideoFile, |
f619de0e C |
64 | processImage |
65 | } | |
66 | ||
67 | // --------------------------------------------------------------------------- | |
68 | ||
1664bc60 | 69 | async function jimpProcessor (path: string, destination: string, newSize: { width: number, height: number }, inputExt: string) { |
7196a70b | 70 | let sourceImage: Jimp |
1664bc60 | 71 | const inputBuffer = await readFile(path) |
18782242 C |
72 | |
73 | try { | |
7196a70b | 74 | sourceImage = await read(inputBuffer) |
18782242 | 75 | } catch (err) { |
b5b68755 | 76 | logger.debug('Cannot read %s with jimp. Try to convert the image using ffmpeg first.', path, { err }) |
18782242 C |
77 | |
78 | const newName = path + '.jpg' | |
79 | await convertWebPToJPG(path, newName) | |
80 | await rename(newName, path) | |
81 | ||
7196a70b | 82 | sourceImage = await read(path) |
18782242 | 83 | } |
a8a63227 C |
84 | |
85 | await remove(destination) | |
86 | ||
1664bc60 | 87 | // Optimization if the source file has the appropriate size |
ea54cd04 | 88 | const outputExt = getLowercaseExtension(destination) |
7196a70b | 89 | if (skipProcessing({ sourceImage, newSize, imageBytes: inputBuffer.byteLength, inputExt, outputExt })) { |
1664bc60 C |
90 | return copy(path, destination) |
91 | } | |
92 | ||
7196a70b C |
93 | await autoResize({ sourceImage, newSize, destination }) |
94 | } | |
95 | ||
96 | async function autoResize (options: { | |
97 | sourceImage: Jimp | |
98 | newSize: { width: number, height: number } | |
99 | destination: string | |
100 | }) { | |
101 | const { sourceImage, newSize, destination } = options | |
102 | ||
9c7cf007 C |
103 | // Portrait mode targetting a landscape, apply some effect on the image |
104 | const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight() | |
105 | const destIsPortraitOrSquare = newSize.width <= newSize.height | |
106 | ||
107 | if (sourceIsPortrait && !destIsPortraitOrSquare) { | |
7196a70b C |
108 | const baseImage = sourceImage.cloneQuiet().cover(newSize.width, newSize.height) |
109 | .color([ { apply: 'shade', params: [ 50 ] } ]) | |
110 | ||
111 | const topImage = sourceImage.cloneQuiet().contain(newSize.width, newSize.height) | |
112 | ||
113 | return write(baseImage.blit(topImage, 0, 0), destination) | |
114 | } | |
115 | ||
9c7cf007 | 116 | return write(sourceImage.cover(newSize.width, newSize.height), destination) |
7196a70b C |
117 | } |
118 | ||
119 | function write (image: Jimp, destination: string) { | |
120 | return image.quality(80).writeAsync(destination) | |
ac81d1a0 | 121 | } |
1664bc60 C |
122 | |
123 | function skipProcessing (options: { | |
7196a70b | 124 | sourceImage: Jimp |
1664bc60 C |
125 | newSize: { width: number, height: number } |
126 | imageBytes: number | |
127 | inputExt: string | |
128 | outputExt: string | |
129 | }) { | |
7196a70b | 130 | const { sourceImage, newSize, imageBytes, inputExt, outputExt } = options |
1664bc60 C |
131 | const { width, height } = newSize |
132 | ||
7196a70b | 133 | if (sourceImage.getWidth() > width || sourceImage.getHeight() > height) return false |
1664bc60 C |
134 | if (inputExt !== outputExt) return false |
135 | ||
136 | const kB = 1000 | |
137 | ||
138 | if (height >= 1000) return imageBytes <= 200 * kB | |
139 | if (height >= 500) return imageBytes <= 100 * kB | |
140 | ||
141 | return imageBytes <= 15 * kB | |
142 | } |