nuxt / image

Plug-and-play image optimization for Nuxt applications.
https://image.nuxt.com
MIT License
1.28k stars 253 forks source link

feat: add imgproxy #1337

Open casualmatt opened 2 months ago

casualmatt commented 2 months ago

WIP.

To start somewhere, I just imported my custom provider for imgproxy.

I used hash.js but that could probably be switch for ohash.

And I'm open to suggestions on how to secure the imgProxySaltand imgProxyKey.

--> Add support provider "imgproxy"

everyx commented 2 months ago

Some related resource about "secure the imgProxySalt and imgProxyKey"

https://github.com/nuxt/image/issues/276 https://github.com/nuxt/image/issues/963

everyx commented 3 weeks ago

@casualmatt I have implemented a version myself, you can use it as a reference

import type { ImageModifiers } from '@nuxt/image'

import { joinURL } from 'ufo'
import { defu } from 'defu'

import { urlSafeBase64 } from '../utils'
import { createOperationsGenerator } from '#image'

export interface ImgproxyModifiers extends ImageModifiers {
  quality: string
  background: string
  rotate: 'auto_right' | 'auto_left' | 'ignore' | 'vflip' | 'hflip' | number
  roundCorner: string
  gravity: 'sm' | string
  effect: string
  color: string
  flags: string
  dpr: string
  opacity: number
  overlay: string
  underlay: string
  transformation: string
  zoom: number
  colorSpace: string
  customFunc: string
  density: number
  aspectRatio: string
}

export interface ImgproxyOptions {
  baseURL?: string
  modifiers?: Partial<ImgproxyOptions>
  key?: string
  salt?: string
  signatureSize?: number
  srcPrefix?: string
  [key: string]: any
}

const operationsGenerator = createOperationsGenerator({
  keyMap: {
    // standard
    width: 'w',
    height: 'h',
    // format will act as a extension
    // format: 'f',
    quality: 'q',
    fit: 'rs',

    // imgporxy
    formatQuality: 'fq', // fq:%format1:%quality1:%format2:%quality2:...:%formatN:%qualityN
    resize: 'rs', // rs:%resizing_type:%width:%height:%enlarge:%extend
    size: 's', // s:%width:%height:%enlarge:%extend
    resizingType: 'rt', // rt:%resizing_type
    enlarge: 'el', // el:%enlarge
    extend: 'ex', // ex:%extend:%gravity
    minWidth: 'mw', // mw:%width
    minHeight: 'mh', // min-height
    zoom: 'z', // z:%zoom_x_y | z:%zoom_x:%zoom_y
    dpr: 'dpr', // dpr:%dpr
    extendAspectRatio: 'exar', // exar:%extend:%gravity
    gravity: 'g', // g:%type:%x_offset:%y_offset
    crop: 'c', // c:%width:%height:%gravity
    trim: 't', // t:%threshold:%color:%equal_hor:%equal_ver
    padding: 'pd', // pd:%top:%right:%bottom:%left
    autoRotate: 'ar', // ar:%auto_rotate
    rotate: 'rot', // rot:%angle
    background: 'bg', // bg:%R:%G:%B | bg:%hex_color
    blur: 'bl', // bl:%sigma
    sharpen: 'sh', // sh:%sigma
    pixelate: 'pix', // pix:%size
    watermark: 'wm', // wm:%opacity:%position:%x_offset:%y_offset:%scale
    stripMetadata: 'sm', // sm:%strip_metadata
    keepCopyright: 'kcr', // kcr:%keep_copyright
    stripColorProfile: 'scp', // scp:%strip_color_profile
    enforceThumbnail: 'eth', // eth:%enforce_thumbnail
    max_bytes: 'mb', // mb:%bytes
    skipProcessing: 'skp', // skp:%extension1:%extension2:...:%extensionN
    raw: 'raw', // raw:%raw
    cachebuster: 'cb', // cb:%string
    expires: 'exp', // exp:%timestamp
    filename: 'fn', // fn:%string

    // pro features
    resizingAlgorithm: 'ra', // * ra:%algorithm
    unsharpening: 'ush', // * ush:%mode:%weight:%dividor
    blurDetections: 'bd', // * bd:%sigma:%class_name1:%class_name2:...:%class_nameN
    drawDetections: 'dd', // * dd:%draw:%class_name1:%class_name2:...:%class_nameN
    gradient: 'gr', // * gr:%opacity:%color:%direction:%start%stop
    watermarkURL: 'wmu', // * wmu:%url
    watermarkText: 'wmt', // * wmt:%text
    watermarkSize: 'wms', // * wms:%width:%height
    watermarkShadow: 'wmsh', // * wmsh:%sigma
    style: 'st', // * st:%style
    backgroundAlpha: 'bga', // * bga:%alpha
    adjust: 'a', // * a:%brightness:%contrast:%saturation
    brightness: 'br', // * br:%brightness
    contrast: 'co', // * co:%contrast
    saturation: 'sa', // * sa:%saturation
    autoquality: 'aq', // * aq:%method:%target:%min_quality:%max_quality:%allowed_error
    jpegOptions: 'jpgo', // * jpgo:%progressive:%no_subsample:%trellis_quant:%overshoot_deringing:%optimize_scans:%quant_table
    pngOptions: 'pngo', // * pngo:%interlaced:%quantize:%quantization_colors
    webpOptions: 'pngo', // * webpo:%compression
    page: 'pg', // * pg:%page
    disableAnimation: 'da', // * da:%disable
    videoThumbnailSecond: 'vts', // * vts:%second
    fallbackImageUrl: 'fiu', // * fiu:%url
  },
  valueMap: {
    fit: {
      cover: 'fill:::1:0',
      contain: 'fit:::0:1',
      fill: 'force:::1:0',
      inside: 'fit:::0:0', // inside use min dimensions
      outside: 'fit:::0:0', // outside use max dimensions
    },
  },
  joinWith: '/',
  formatter: (key: string, val: string) => `${key}:${val}`,
})

/**
 * 让修饰符兼容 nuxt image 默认选项值
 */
function makeModifiersCompatible(modifiers: Partial<ImgproxyModifiers> = {}): Partial<ImgproxyModifiers> {
  const _modifiers: Partial<ImgproxyModifiers> = { ...modifiers }

  if (_modifiers.fit === 'outside' && _modifiers.width && _modifiers.height) {
    if (_modifiers.width > _modifiers.height)
      delete _modifiers.height
    else
      delete _modifiers.width
  }

  // 这里采用 URL 后缀方式来设置 format,不使用 format 参数
  if (_modifiers.format)
    delete _modifiers.format

  return _modifiers
}

const defaultModifiers = {
  fit: 'cover',
}

export function getImage(
  src: string,
  { modifiers = {}, baseURL = '/', srcPrefix = '' }: ImgproxyOptions = {}, // signatureSize = 32, key = '', salt = '',
) {
  const mergedModifiers = defu(modifiers, defaultModifiers)
  const compModifiers = makeModifiersCompatible(mergedModifiers)
  const processingOptions = operationsGenerator(compModifiers)

  const finalSrc = srcPrefix.length > 0 ? src.replace(new RegExp(srcPrefix), '') : src
  const encodedURL = urlSafeBase64(finalSrc)

  // const signature = await sign(salt, `/${processingOptions}/${encodedURL}`, key, signatureSize);
  const signature = '_'
  const extension = (typeof modifiers.format === 'string' && modifiers.format.length > 0)
    ? modifiers.format
    : undefined

  // https://docs.imgproxy.net/generating_the_url?id=example
  return {
    url: joinURL(
      baseURL,
      signature,
      processingOptions,
      extension ? `${encodedURL}.${extension}` : encodedURL,
    ),
  }
}

export default getImage