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