image
image copied to clipboard
feat: add imgproxy
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 imgProxySalt and imgProxyKey.
Some related resource about "secure the imgProxySalt and imgProxyKey"
https://github.com/nuxt/image/issues/276 https://github.com/nuxt/image/issues/963
@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