]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/image-utils.ts
Fix s3 mock cleanup
[github/Chocobozzz/PeerTube.git] / server / helpers / image-utils.ts
CommitLineData
1664bc60 1import { copy, readFile, remove, rename } from 'fs-extra'
7bde6250 2import Jimp, { read as jimpRead } from 'jimp'
c729caf6 3import { join } from 'path'
83002a82 4import { ColorActionName } from '@jimp/plugin-color'
0628157f
C
5import { getLowercaseExtension } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils'
0c9668f7 7import { convertWebPToJPG, generateThumbnailFromVideo, processGIF } from './ffmpeg'
c729caf6
C
8import { logger, loggerTagsFactory } from './logger'
9
10const lTags = loggerTagsFactory('image-utils')
18782242 11
84531547 12function generateImageFilename (extension = '.jpg') {
d4a8e7a6 13 return buildUUID() + extension
84531547
C
14}
15
3a54605d
C
16async 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
42async 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
69async 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 82export {
84531547 83 generateImageFilename,
c729caf6 84 generateImageFromVideoFile,
7bde6250
C
85
86 processImage,
87
88 getImageSize
f619de0e
C
89}
90
91// ---------------------------------------------------------------------------
92
1664bc60 93async 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
120async 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
145function write (image: Jimp, destination: string) {
146 return image.quality(80).writeAsync(destination)
ac81d1a0 147}
1664bc60
C
148
149function 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
171function hasExif (image: Jimp) {
172 return !!(image.bitmap as any).exifBuffer
173}
174
175function removeExif (image: Jimp) {
176 (image.bitmap as any).exifBuffer = null
177}