44b4dbcd41
Generally, AVIF quality can be lower compared to WebP so we can adjust this for the user so that it looks roughly the same depending on if the browser supports AVIF or WebP. - Fixes #31254 - Related to https://github.com/lovell/sharp/issues/2850
143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
import semver from 'next/dist/compiled/semver'
|
|
import { codecs as supportedFormats, preprocessors } from './codecs'
|
|
import ImageData from './image_data'
|
|
|
|
type EncoderKey = keyof typeof supportedFormats
|
|
|
|
// Fixed in Node.js 16.5.0 and newer.
|
|
// See https://github.com/nodejs/node/pull/39337
|
|
// Eventually, remove this delay when engines is updated.
|
|
// See https://git.io/JCTr0
|
|
const FIXED_VERSION = '16.5.0'
|
|
const DELAY_MS = 1000
|
|
let _promise: Promise<void> | undefined
|
|
|
|
function delayOnce(ms: number): Promise<void> {
|
|
if (!_promise) {
|
|
_promise = new Promise((resolve) => {
|
|
setTimeout(resolve, ms)
|
|
})
|
|
}
|
|
return _promise
|
|
}
|
|
|
|
function maybeDelay(): Promise<void> {
|
|
const isAppleM1 = process.arch === 'arm64' && process.platform === 'darwin'
|
|
if (isAppleM1 && semver.lt(process.version, FIXED_VERSION)) {
|
|
return delayOnce(DELAY_MS)
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
export async function decodeBuffer(
|
|
_buffer: Buffer | Uint8Array
|
|
): Promise<ImageData> {
|
|
const buffer = Buffer.from(_buffer)
|
|
const firstChunk = buffer.slice(0, 16)
|
|
const firstChunkString = Array.from(firstChunk)
|
|
.map((v) => String.fromCodePoint(v))
|
|
.join('')
|
|
const key = Object.entries(supportedFormats).find(([, { detectors }]) =>
|
|
detectors.some((detector) => detector.exec(firstChunkString))
|
|
)?.[0] as EncoderKey | undefined
|
|
if (!key) {
|
|
throw Error(`Buffer has an unsupported format`)
|
|
}
|
|
const encoder = supportedFormats[key]
|
|
const mod = await encoder.dec()
|
|
const rgba = mod.decode(new Uint8Array(buffer))
|
|
return rgba
|
|
}
|
|
|
|
export async function rotate(
|
|
image: ImageData,
|
|
numRotations: number
|
|
): Promise<ImageData> {
|
|
image = ImageData.from(image)
|
|
|
|
const m = await preprocessors['rotate'].instantiate()
|
|
return await m(image.data, image.width, image.height, { numRotations })
|
|
}
|
|
|
|
type ResizeOpts = { image: ImageData } & (
|
|
| { width: number; height?: never }
|
|
| { height: number; width?: never }
|
|
)
|
|
|
|
export async function resize({ image, width, height }: ResizeOpts) {
|
|
image = ImageData.from(image)
|
|
|
|
const p = preprocessors['resize']
|
|
const m = await p.instantiate()
|
|
await maybeDelay()
|
|
return await m(image.data, image.width, image.height, {
|
|
...p.defaultOptions,
|
|
width,
|
|
height,
|
|
})
|
|
}
|
|
|
|
export async function encodeJpeg(
|
|
image: ImageData,
|
|
{ quality }: { quality: number }
|
|
): Promise<Buffer | Uint8Array> {
|
|
image = ImageData.from(image)
|
|
|
|
const e = supportedFormats['mozjpeg']
|
|
const m = await e.enc()
|
|
await maybeDelay()
|
|
const r = await m.encode(image.data, image.width, image.height, {
|
|
...e.defaultEncoderOptions,
|
|
quality,
|
|
})
|
|
return Buffer.from(r)
|
|
}
|
|
|
|
export async function encodeWebp(
|
|
image: ImageData,
|
|
{ quality }: { quality: number }
|
|
): Promise<Buffer | Uint8Array> {
|
|
image = ImageData.from(image)
|
|
|
|
const e = supportedFormats['webp']
|
|
const m = await e.enc()
|
|
await maybeDelay()
|
|
const r = await m.encode(image.data, image.width, image.height, {
|
|
...e.defaultEncoderOptions,
|
|
quality,
|
|
})
|
|
return Buffer.from(r)
|
|
}
|
|
|
|
export async function encodeAvif(
|
|
image: ImageData,
|
|
{ quality }: { quality: number }
|
|
): Promise<Buffer | Uint8Array> {
|
|
image = ImageData.from(image)
|
|
|
|
const e = supportedFormats['avif']
|
|
const m = await e.enc()
|
|
await maybeDelay()
|
|
const val = e.autoOptimize.min || 62
|
|
const r = await m.encode(image.data, image.width, image.height, {
|
|
...e.defaultEncoderOptions,
|
|
// Think of cqLevel as the "amount" of quantization (0 to 62),
|
|
// so a lower value yields higher quality (0 to 100).
|
|
cqLevel: Math.round(val - (quality / 100) * val),
|
|
})
|
|
return Buffer.from(r)
|
|
}
|
|
|
|
export async function encodePng(
|
|
image: ImageData
|
|
): Promise<Buffer | Uint8Array> {
|
|
image = ImageData.from(image)
|
|
|
|
const e = supportedFormats['oxipng']
|
|
const m = await e.enc()
|
|
await maybeDelay()
|
|
const r = await m.encode(image.data, image.width, image.height, {
|
|
...e.defaultEncoderOptions,
|
|
})
|
|
return Buffer.from(r)
|
|
}
|