astro/packages/integrations/image/src/vendor/squoosh/codecs.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

374 lines
9.7 KiB
TypeScript
Raw Normal View History

Adding an optional image service based on Squoosh (#4738) * WIP: adding a service built on @squoosh/lib * WIP: investigating memory leaks in Squoosh * WIP: vendoring Squoosh to work with our build * chore: a bit of cleanup and a small perf gain * removing a few unused deps * fix: removing temp .only() in sharp test * hooking up the last build steps to copy over .wasm files * removing the duplicated lib/*.wasm files * defaulting to Sharp for the initial @next release * make sure pnpm always runs the postbuild script * removing a few node dependencies * refactor: move the copy .wasm build step out of the SSR bundle * linter fixes * fixing lock file * chore: add TEMP changeset * fix built wasm location for SSG builds * Revert "defaulting to Sharp for the initial @next release" This reverts commit 1a8d4f7f603ac08b24ae6cd390f3a0fe39f4c87d. * removing sharp dependency * Revert "fix built wasm location for SSG builds" This reverts commit 446b80bb53dee364bbf6dca30f26af7061a93d12. * chore: update lockfile * fixing up image tests for the wasm loader * updating the README for squoosh * parallel wasm builds * refactor: a bit of house keeping * perf: allow a thread for each output encoding format * fix: dev broke with the shift to wasm workers * adds a new `astro:build:generated` hook for SSG builds * fix: typo + calling cleanup methods in wasm codecs * adding @astrojs/webapi for the TransformStream polyfill * Revert "adding @astrojs/webapi for the TransformStream polyfill" This reverts commit 39e5b845a52ed489ea6e2a618a179694de06b3b5. * perf: using sharp for most of the CI tests * chore: update lockfile * removing hard-coded squoosh imports * fix: adding sharp to rollup externals * test: using dev for the squoosh tests * fix: updating the build output dir for wasm filles in SSG builds * updating the changeset with migration details * Revert "adds a new `astro:build:generated` hook for SSG builds" This reverts commit 59b5fec7be203e53f5c4fcb43f2d3d81f4377282. * nit: adding comments for the wasm file copy * chore: fix eslint warning
2022-09-22 19:48:14 +00:00
import { promises as fsp } from 'node:fs'
import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData
}
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory
rotate(width: number, height: number, rotate: number): void
}
}
interface ResizeWithAspectParams {
input_width: number
input_height: number
target_width?: number
target_height?: number
}
export interface ResizeOptions {
width?: number
height?: number
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
premultiply: boolean
linearRGB: boolean
}
export interface RotateOptions {
numRotations: number
}
// MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from './mozjpeg/mozjpeg_enc'
// @ts-ignore
import mozEnc from './mozjpeg/mozjpeg_node_enc.js'
const mozEncWasm = new URL('./mozjpeg/mozjpeg_node_enc.wasm', import.meta.url)
// @ts-ignore
import mozDec from './mozjpeg/mozjpeg_node_dec.js'
const mozDecWasm = new URL('./mozjpeg/mozjpeg_node_dec.wasm', import.meta.url)
// WebP
import type { WebPModule as WebPEncodeModule } from './webp/webp_enc'
// @ts-ignore
import webpEnc from './webp/webp_node_enc.js'
const webpEncWasm = new URL('./webp/webp_node_enc.wasm', import.meta.url)
// @ts-ignore
import webpDec from './webp/webp_node_dec.js'
const webpDecWasm = new URL('./webp/webp_node_dec.wasm', import.meta.url)
// AVIF
import type { AVIFModule as AVIFEncodeModule } from './avif/avif_enc'
// @ts-ignore
import avifEnc from './avif/avif_node_enc.js'
const avifEncWasm = new URL('./avif/avif_node_enc.wasm', import.meta.url)
// @ts-ignore
import avifDec from './avif/avif_node_dec.js'
const avifDecWasm = new URL('./avif/avif_node_dec.wasm', import.meta.url)
// PNG
// @ts-ignore
import * as pngEncDec from './png/squoosh_png.js'
const pngEncDecWasm = new URL('./png/squoosh_png_bg.wasm', import.meta.url)
const pngEncDecInit = () =>
pngEncDec.default(fsp.readFile(pathify(pngEncDecWasm.toString())))
// OxiPNG
// @ts-ignore
import * as oxipng from './png/squoosh_oxipng.js'
const oxipngWasm = new URL('./png/squoosh_oxipng_bg.wasm', import.meta.url)
const oxipngInit = () => oxipng.default(fsp.readFile(pathify(oxipngWasm.toString())))
// Resize
// @ts-ignore
import * as resize from './resize/squoosh_resize.js'
const resizeWasm = new URL('./resize/squoosh_resize_bg.wasm', import.meta.url)
const resizeInit = () => resize.default(fsp.readFile(pathify(resizeWasm.toString())))
// rotate
const rotateWasm = new URL('./rotate/rotate.wasm', import.meta.url)
// Our decoders currently rely on a `ImageData` global.
2022-09-22 19:50:47 +00:00
import ImageData from './image_data.js'
Adding an optional image service based on Squoosh (#4738) * WIP: adding a service built on @squoosh/lib * WIP: investigating memory leaks in Squoosh * WIP: vendoring Squoosh to work with our build * chore: a bit of cleanup and a small perf gain * removing a few unused deps * fix: removing temp .only() in sharp test * hooking up the last build steps to copy over .wasm files * removing the duplicated lib/*.wasm files * defaulting to Sharp for the initial @next release * make sure pnpm always runs the postbuild script * removing a few node dependencies * refactor: move the copy .wasm build step out of the SSR bundle * linter fixes * fixing lock file * chore: add TEMP changeset * fix built wasm location for SSG builds * Revert "defaulting to Sharp for the initial @next release" This reverts commit 1a8d4f7f603ac08b24ae6cd390f3a0fe39f4c87d. * removing sharp dependency * Revert "fix built wasm location for SSG builds" This reverts commit 446b80bb53dee364bbf6dca30f26af7061a93d12. * chore: update lockfile * fixing up image tests for the wasm loader * updating the README for squoosh * parallel wasm builds * refactor: a bit of house keeping * perf: allow a thread for each output encoding format * fix: dev broke with the shift to wasm workers * adds a new `astro:build:generated` hook for SSG builds * fix: typo + calling cleanup methods in wasm codecs * adding @astrojs/webapi for the TransformStream polyfill * Revert "adding @astrojs/webapi for the TransformStream polyfill" This reverts commit 39e5b845a52ed489ea6e2a618a179694de06b3b5. * perf: using sharp for most of the CI tests * chore: update lockfile * removing hard-coded squoosh imports * fix: adding sharp to rollup externals * test: using dev for the squoosh tests * fix: updating the build output dir for wasm filles in SSG builds * updating the changeset with migration details * Revert "adds a new `astro:build:generated` hook for SSG builds" This reverts commit 59b5fec7be203e53f5c4fcb43f2d3d81f4377282. * nit: adding comments for the wasm file copy * chore: fix eslint warning
2022-09-22 19:48:14 +00:00
(global as any).ImageData = ImageData
function resizeNameToIndex(
name: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
) {
switch (name) {
case 'triangle':
return 0
case 'catrom':
return 1
case 'mitchell':
return 2
case 'lanczos3':
return 3
default:
throw Error(`Unknown resize algorithm "${name}"`)
}
}
function resizeWithAspect({
input_width,
input_height,
target_width,
target_height,
}: ResizeWithAspectParams): { width: number; height: number } {
if (!target_width && !target_height) {
throw Error('Need to specify at least width or height when resizing')
}
if (target_width && target_height) {
return { width: target_width, height: target_height }
}
if (!target_width) {
return {
width: Math.round((input_width / input_height) * target_height!),
height: target_height!,
}
}
return {
width: target_width,
height: Math.round((input_height / input_width) * target_width),
}
}
export const preprocessors = {
resize: {
name: 'Resize',
description: 'Resize the image before compressing',
instantiate: async () => {
await resizeInit()
return (
buffer: Uint8Array,
input_width: number,
input_height: number,
{ width, height, method, premultiply, linearRGB }: ResizeOptions
) => {
;({ width, height } = resizeWithAspect({
input_width,
input_height,
target_width: width,
target_height: height,
}))
const imageData = new ImageData(
resize.resize(
buffer,
input_width,
input_height,
width,
height,
resizeNameToIndex(method),
premultiply,
linearRGB
),
width,
height
)
resize.cleanup()
return imageData
}
},
defaultOptions: {
method: 'lanczos3',
fitMethod: 'stretch',
premultiply: true,
linearRGB: true,
},
},
rotate: {
name: 'Rotate',
description: 'Rotate image',
instantiate: async () => {
return async (
buffer: Uint8Array,
width: number,
height: number,
{ numRotations }: RotateOptions
) => {
const degrees = (numRotations * 90) % 360
const sameDimensions = degrees === 0 || degrees === 180
const size = width * height * 4
const instance = (
await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm.toString())))
).instance as RotateModuleInstance
const { memory } = instance.exports
const additionalPagesNeeded = Math.ceil(
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024)
)
if (additionalPagesNeeded > 0) {
memory.grow(additionalPagesNeeded)
}
const view = new Uint8ClampedArray(memory.buffer)
view.set(buffer, 8)
instance.exports.rotate(width, height, degrees)
return new ImageData(
view.slice(size + 8, size * 2 + 8),
sameDimensions ? width : height,
sameDimensions ? height : width
)
}
},
defaultOptions: {
numRotations: 0,
},
},
} as const
export const codecs = {
mozjpeg: {
name: 'MozJPEG',
extension: 'jpg',
detectors: [/^\xFF\xD8\xFF/],
dec: () =>
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm.toString()),
enc: () =>
instantiateEmscriptenWasm(
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
mozEncWasm.toString()
),
defaultEncoderOptions: {
quality: 75,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: 3 /*YCbCr*/,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
},
autoOptimize: {
option: 'quality',
min: 0,
max: 100,
},
},
webp: {
name: 'WebP',
extension: 'webp',
detectors: [/^RIFF....WEBPVP8[LX ]/s],
dec: () =>
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm.toString()),
enc: () =>
instantiateEmscriptenWasm(
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
webpEncWasm.toString()
),
defaultEncoderOptions: {
quality: 75,
target_size: 0,
target_PSNR: 0,
method: 4,
sns_strength: 50,
filter_strength: 60,
filter_sharpness: 0,
filter_type: 1,
partitions: 0,
segments: 4,
pass: 1,
show_compressed: 0,
preprocessing: 0,
autofilter: 0,
partition_limit: 0,
alpha_compression: 1,
alpha_filtering: 1,
alpha_quality: 100,
lossless: 0,
exact: 0,
image_hint: 0,
emulate_jpeg_size: 0,
thread_level: 0,
low_memory: 0,
near_lossless: 100,
use_delta_palette: 0,
use_sharp_yuv: 0,
},
autoOptimize: {
option: 'quality',
min: 0,
max: 100,
},
},
avif: {
name: 'AVIF',
extension: 'avif',
// eslint-disable-next-line no-control-regex
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
dec: () =>
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm.toString()),
enc: async () => {
return instantiateEmscriptenWasm(
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncWasm.toString()
)
},
defaultEncoderOptions: {
cqLevel: 33,
cqAlphaLevel: -1,
denoiseLevel: 0,
tileColsLog2: 0,
tileRowsLog2: 0,
speed: 6,
subsample: 1,
chromaDeltaQ: false,
sharpness: 0,
tune: 0 /* AVIFTune.auto */,
},
autoOptimize: {
option: 'cqLevel',
min: 62,
max: 0,
},
},
oxipng: {
name: 'OxiPNG',
extension: 'png',
// eslint-disable-next-line no-control-regex
detectors: [/^\x89PNG\x0D\x0A\x1A\x0A/],
dec: async () => {
await pngEncDecInit()
return {
decode: (buffer: Buffer | Uint8Array) => {
const imageData = pngEncDec.decode(buffer)
pngEncDec.cleanup()
return imageData
},
}
},
enc: async () => {
await pngEncDecInit()
await oxipngInit()
return {
encode: (
buffer: Uint8ClampedArray | ArrayBuffer,
width: number,
height: number,
opts: { level: number }
) => {
const simplePng = pngEncDec.encode(
new Uint8Array(buffer),
width,
height
)
const imageData = oxipng.optimise(simplePng, opts.level, false)
oxipng.cleanup()
return imageData
},
}
},
defaultEncoderOptions: {
level: 2,
},
autoOptimize: {
option: 'level',
min: 6,
max: 1,
},
},
} as const