chiiya/shion

View on GitHub
src/processor.ts

Summary

Maintainability
A
1 hr
Test Coverage
import {
  Output,
  Input,
  OptimizeOptions,
  OptimizeResult,
  ResizeResult,
  ResolvedResizeOptions,
} from '../types/types'
const { basename, join, extname } = require('path')
import { formatSize, getFileInformation, getNumberInputAsArray, isAbsolutePath } from './helpers'
const { readFile, createReadStream, createWriteStream, writeFile, copyFile } = require('fs-extra')
const imageminPngquant = require('imagemin-pngquant')
const imageminGifsicle = require('imagemin-gifsicle')
const sharp = require('sharp')

export default class Processor {
  /**
   * Get the output path and filename information.
   * @param input
   * @param basedir
   */
  public getOutputData(input: Input, basedir: string): Output {
    const filename = basename(input.fullPath)
    const dir = join(basedir, input.fullPath.replace(input.basedir, '').replace(filename, ''))
    return {
      fullPath: isAbsolutePath(dir) ? join(dir, filename) : join(process.cwd(), dir, filename),
      basedir,
      dir: isAbsolutePath(dir) ? dir : join(process.cwd(), dir),
      filename,
    }
  }

  /**
   * Optimize an image file using image-min. Also creates webp versions
   * if so configured.
   * @param input
   * @param output
   * @param options
   */
  public async optimizeImage(
    input: Input,
    output: Output,
    options: OptimizeOptions
  ): Promise<OptimizeResult[]> {
    let buffer: Buffer = await readFile(input.fullPath)
    // Create optimized buffer
    const extension = extname(output.filename).substring(1).toUpperCase()
    const originalSize = buffer.length
    const optimizedBuffer = await this.optimize(buffer, options)

    // If new size is larger than origin, simply copy the original
    const newSize = optimizedBuffer.length
    if (originalSize < newSize) {
      return this.copyImage(input, output)
    }
    await writeFile(output.fullPath, optimizedBuffer)
    const results: OptimizeResult[] = [
      {
        path: join(output.dir, output.filename),
        originalSize: formatSize(originalSize),
        newSize: formatSize(newSize),
        type: extension,
      },
    ]

    // If webp images should be created, do it
    if (options.webp === true && ['JPG', 'JPEG', 'PNG'].includes(extension)) {
      const { default: imagemin } = await import('imagemin')
      const { default: imageminWebp } = await import('imagemin-webp')
      const webpBuffer = await imagemin.buffer(buffer, {
        plugins: [imageminWebp()],
      })
      await writeFile(`${output.fullPath}.webp`, webpBuffer)
      results.push({
        path: join(output.dir, `${output.filename}.webp`),
        originalSize: formatSize(originalSize),
        newSize: formatSize(webpBuffer.length),
        type: 'WEBP',
      })
    }

    return results
  }

  public async optimizeAndResize(
    input: Input,
    output: Output,
    options: ResolvedResizeOptions
  ): Promise<ResizeResult[]> {
    const results: ResizeResult[] = []

    const extension = extname(output.filename).substring(1).toUpperCase()
    const sizes = getNumberInputAsArray(options.sizes)

    for (const size of sizes) {
      const resizer = sharp().resize(size)
      if (options.optimize) {
        resizer.jpeg(options.jpg).png(options.png).webp(options.webp)
      }

      const filename = output.filename.replace(/\.[^/.]+$/, '')
      const outputName = this.getResizedFilename(
        filename,
        extension.toLowerCase(),
        size,
        options.pattern
      )
      const path = join(output.dir, outputName)
      await this.resizeImage(input.fullPath, path, resizer)
      results.push({
        path: join(output.dir, outputName),
        type: extension,
        size,
      })
    }

    return results
  }

  protected async resizeImage(input: string, output: string, resizer: any): Promise<void> {
    return new Promise<void>((resolve) => {
      const stream = createReadStream(input)
      stream.pipe(resizer).pipe(createWriteStream(output).on('finish', resolve))
    })
  }

  protected getResizedFilename(
    filename: string,
    extension: string,
    size: number,
    pattern: string
  ): string {
    return pattern
      .replace('[name]', filename)
      .replace('[extension]', extension)
      .replace('[size]', size.toString())
  }

  /**
   * Copy an image from one location to another.
   * @param input
   * @param output
   */
  public async copyImage(input: Input, output: Output): Promise<OptimizeResult[]> {
    const information = getFileInformation(input.fullPath)
    await copyFile(input.fullPath, output.fullPath)
    return [
      {
        path: join(output.dir, output.filename),
        originalSize: information.size,
        newSize: information.size,
        type: information.type,
      },
    ]
  }

  /**
   * Optimize an image with imagemin. Returns optimized buffer.
   * @param buffer
   * @param options
   */
  protected async optimize(buffer: Buffer, options: OptimizeOptions): Promise<Buffer> {
    const { default: imagemin } = await import('imagemin')
    const { default: imageminSvgo } = await import('imagemin-svgo')
    const { default: imageminMozjpeg } = await import('imagemin-mozjpeg')
    return imagemin.buffer(buffer, {
      plugins: [
        imageminMozjpeg(options.mozJpeg),
        imageminPngquant(options.pngQuant),
        imageminSvgo(options.svgo),
        imageminGifsicle(options.gifSicle),
      ],
    })
  }
}