mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			156 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			156 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
| import { copy, remove } from 'fs-extra/esm'
 | |
| import { readFile, rename } from 'fs/promises'
 | |
| import { ColorActionName } from '@jimp/plugin-color'
 | |
| import { buildUUID, getLowercaseExtension } from '@peertube/peertube-node-utils'
 | |
| import { convertWebPToJPG, processGIF } from './ffmpeg/index.js'
 | |
| import { logger } from './logger.js'
 | |
| 
 | |
| import type Jimp from 'jimp'
 | |
| 
 | |
| export function generateImageFilename (extension = '.jpg') {
 | |
|   return buildUUID() + extension
 | |
| }
 | |
| 
 | |
| export async function processImage (options: {
 | |
|   path: string
 | |
|   destination: string
 | |
|   newSize: { width: number, height: number }
 | |
|   keepOriginal?: boolean // default false
 | |
| }) {
 | |
|   const { path, destination, newSize, keepOriginal = false } = options
 | |
| 
 | |
|   const extension = getLowercaseExtension(path)
 | |
| 
 | |
|   if (path === destination) {
 | |
|     throw new Error('Jimp/FFmpeg needs an input path different that the output path.')
 | |
|   }
 | |
| 
 | |
|   logger.debug('Processing image %s to %s.', path, destination)
 | |
| 
 | |
|   // Use FFmpeg to process GIF
 | |
|   if (extension === '.gif') {
 | |
|     await processGIF({ path, destination, newSize })
 | |
|   } else {
 | |
|     await jimpProcessor({ path, destination, newSize, inputExt: extension })
 | |
|   }
 | |
| 
 | |
|   if (keepOriginal !== true) await remove(path)
 | |
| 
 | |
|   logger.debug('Finished processing image %s to %s.', path, destination)
 | |
| }
 | |
| 
 | |
| export async function getImageSize (path: string) {
 | |
|   const inputBuffer = await readFile(path)
 | |
| 
 | |
|   const Jimp = await import('jimp')
 | |
| 
 | |
|   const image = await Jimp.default.read(inputBuffer)
 | |
| 
 | |
|   return {
 | |
|     width: image.getWidth(),
 | |
|     height: image.getHeight()
 | |
|   }
 | |
| }
 | |
| 
 | |
| // ---------------------------------------------------------------------------
 | |
| // Private
 | |
| // ---------------------------------------------------------------------------
 | |
| 
 | |
| async function jimpProcessor (options: {
 | |
|   path: string
 | |
|   destination: string
 | |
|   newSize: {
 | |
|     width: number
 | |
|     height: number
 | |
|   }
 | |
|   inputExt: string
 | |
| }) {
 | |
|   const { path, destination, newSize, inputExt } = options
 | |
| 
 | |
|   let sourceImage: Jimp
 | |
|   const inputBuffer = await readFile(path)
 | |
| 
 | |
|   const Jimp = await import('jimp')
 | |
| 
 | |
|   try {
 | |
|     sourceImage = await Jimp.default.read(inputBuffer)
 | |
|   } catch (err) {
 | |
|     logger.debug('Cannot read %s with jimp. Try to convert the image using ffmpeg first.', path, { err })
 | |
| 
 | |
|     const newName = path + '.jpg'
 | |
|     await convertWebPToJPG({ path, destination: newName })
 | |
|     await rename(newName, path)
 | |
| 
 | |
|     sourceImage = await Jimp.default.read(path)
 | |
|   }
 | |
| 
 | |
|   await remove(destination)
 | |
| 
 | |
|   // Optimization if the source file has the appropriate size
 | |
|   const outputExt = getLowercaseExtension(destination)
 | |
|   if (skipProcessing({ sourceImage, newSize, imageBytes: inputBuffer.byteLength, inputExt, outputExt })) {
 | |
|     return copy(path, destination)
 | |
|   }
 | |
| 
 | |
|   await autoResize({ sourceImage, newSize, destination })
 | |
| }
 | |
| 
 | |
| async function autoResize (options: {
 | |
|   sourceImage: Jimp
 | |
|   newSize: { width: number, height: number }
 | |
|   destination: string
 | |
| }) {
 | |
|   const { sourceImage, newSize, destination } = options
 | |
| 
 | |
|   // Portrait mode targeting a landscape, apply some effect on the image
 | |
|   const sourceIsPortrait = sourceImage.getWidth() <= sourceImage.getHeight()
 | |
|   const destIsPortraitOrSquare = newSize.width <= newSize.height
 | |
| 
 | |
|   removeExif(sourceImage)
 | |
| 
 | |
|   if (sourceIsPortrait && !destIsPortraitOrSquare) {
 | |
|     const baseImage = sourceImage.cloneQuiet().cover(newSize.width, newSize.height)
 | |
|                                               .color([ { apply: ColorActionName.SHADE, params: [ 50 ] } ])
 | |
| 
 | |
|     const topImage = sourceImage.cloneQuiet().contain(newSize.width, newSize.height)
 | |
| 
 | |
|     return write(baseImage.blit(topImage, 0, 0), destination)
 | |
|   }
 | |
| 
 | |
|   return write(sourceImage.cover(newSize.width, newSize.height), destination)
 | |
| }
 | |
| 
 | |
| function write (image: Jimp, destination: string) {
 | |
|   return image.quality(80).writeAsync(destination)
 | |
| }
 | |
| 
 | |
| function skipProcessing (options: {
 | |
|   sourceImage: Jimp
 | |
|   newSize: { width: number, height: number }
 | |
|   imageBytes: number
 | |
|   inputExt: string
 | |
|   outputExt: string
 | |
| }) {
 | |
|   const { sourceImage, newSize, imageBytes, inputExt, outputExt } = options
 | |
|   const { width, height } = newSize
 | |
| 
 | |
|   if (hasExif(sourceImage)) return false
 | |
|   if (sourceImage.getWidth() !== width || sourceImage.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
 | |
| }
 | |
| 
 | |
| function hasExif (image: Jimp) {
 | |
|   return !!(image.bitmap as any).exifBuffer
 | |
| }
 | |
| 
 | |
| function removeExif (image: Jimp) {
 | |
|   (image.bitmap as any).exifBuffer = null
 | |
| }
 |