-
Notifications
You must be signed in to change notification settings - Fork 279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add imgproxy #1337
base: main
Are you sure you want to change the base?
feat: add imgproxy #1337
Conversation
@@ -33,6 +33,7 @@ | |||
"consola": "^3.2.3", | |||
"defu": "^6.1.4", | |||
"h3": "^1.11.1", | |||
"hash.js": "^1.1.7", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as you've suggested, let's switch to ohash
🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so experienced in hash or ohash, but I think that ohash misses the hmac
function to generate the signature.
const hmac = hash.hmac(hash.sha256, hexDecode(secret));
More in general, I'm open to suggestions on this topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@casualmatt I have a implement at #963, using uncrypto
, but need async getImage
support
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ty for the hint, I will work on it tomorrow or later today👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@everyx
I see, .. so to properly support imgproxy, and to do it safely or imgproxy will allow us to optimize any URL provided; we are waiting for:
#276 --> To securely sign the URLs with uncrypto.
#963 --> To support getImage and not just the NuxtImg component.
I hope to get it right,
For now, as an alternative, @danielroe, we could remove the signing of the URL and add big, pretty big, I would say, disclaimer to use the EnvVar IMGPROXY_ALLOWED_SOURCES
to secure the install of imgproxy.
|
||
export const getImage: ProviderGetImage = (src, options) => { | ||
const { modifiers, url, salt, key } = options; | ||
const mergeModifiers = { ...defaultModifiers, ...modifiers }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the Cloudinary provider, defu
should be used here to ensure the defaults are correctly merged.
<NuxtImg src="..." width="400" height="400" />
results in the following mergeModifiers
values with and without defu
, if modifiers
isn't set in nuxt.config.ts.
Without (this results in srcset
not having the defaults set on the <img />
):
{ fit: undefined, width: 400, height: 400, gravity: 'no', enlarge: 1, format: undefined, quality: undefined, background: undefined }
{ fit: undefined, width: 800, height: 800, gravity: 'no', enlarge: 1, format: undefined, quality: undefined, background: undefined }
{ fit: 'fill', width: 400, height: 400, gravity: 'no', enlarge: 1, format: 'webp' }
With:
{ fit: 'fill', width: 400, height: 400, gravity: 'no', enlarge: 1, format: 'webp' }
{ fit: 'fill', width: 800, height: 800, gravity: 'no', enlarge: 1, format: 'webp' }
{ fit: 'fill', width: 400, height: 400, gravity: 'no', enlarge: 1, format: 'webp' }
@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 |
@everyx Without support for server-side signatures, I wouldn't recommend publishing this. The risk is that users might expose their Imgproxy instance to the public, which could lead to security vulnerabilities or abuses. |
you don't have to use salt and salt, by the way. can remove these 2 situations and assign them to normal requests. Salt protection is an additional feature. |
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
andimgProxyKey
.--> Add support provider "imgproxy"