@@ -20,6 +20,21 @@ const photon = require('@silvia-odwyer/photon-node');
2020const { createImageLibrary } = await import ( 'purejsimage' ) ;
2121const { allCodecs } = await import ( 'purejsimage/codecs/all' ) ;
2222const pureJsImage = createImageLibrary ( allCodecs ) ;
23+ // jsquash: Squoosh's libavif and libjxl builds (their threaded builds are off in Node: one thread), the modules given
24+ const jsquash = { } ;
25+ for ( const [ name , file ] of [ [ 'avif/encode' , 'avif/codec/enc/avif_enc.wasm' ] , [ 'avif/decode' , 'avif/codec/dec/avif_dec.wasm' ] ,
26+ [ 'jxl/encode' , 'jxl/codec/enc/jxl_enc.wasm' ] , [ 'jxl/decode' , 'jxl/codec/dec/jxl_dec.wasm' ] ] ) {
27+ const codec = await import ( `@jsquash/${ name } .js` ) ;
28+ await codec . init ( await WebAssembly . compile ( fs . readFileSync ( new URL ( `./node_modules/@jsquash/${ file } ` , import . meta. url ) ) ) ) ;
29+ jsquash [ name ] = codec . default ;
30+ }
31+
32+ /** libavif's or libjxl's RGBA of an AVIF or JPEG XL (for the quality of the encoded files) */
33+ export const referenceDecode = async ( data , format ) => ( await jsquash [ `${ format } /decode` ] ( data ) ) . data ;
34+
35+ // lossy AVIF and JPEG XL at the same nominal quality in every library (their scales differ: the file sizes come with
36+ // their PSNR)
37+ export const AVIF_QUALITY = 60 , JXL_QUALITY = 90 ;
2338
2439// the same work in every library: halve the size (bilinear where it can be chosen), rotate by 30° (growing the
2540// image to fit), blur with a radius/sigma of 5, place a semi-transparent overlay, cut out a quarter
@@ -33,13 +48,15 @@ const font = fs.readFileSync(FONT_FILE), svg = fs.readFileSync(SVG_FILE, 'utf8')
3348const svgScale = SVG_WIDTH / Number ( svg . match ( / < s v g [ ^ > ] * \s w i d t h = " ( \d + ) / ) [ 1 ] ) ;
3449const svgAtWidth = svg . replace ( / < s v g [ ^ > ] * > / , tag => tag . replace ( / \s ( w i d t h | h e i g h t ) = " ( \d + ) " / g, ( _ , name , value ) => ` ${ name } ="${ Math . round ( value * svgScale ) } "` ) ) ;
3550
36- const imageScript = ( IS , { version, package : pkg , kind, blur = true , webp = true , bilinear = true } ) => ( {
51+ const imageScript = ( IS , { version, package : pkg , kind, blur = true , webp = true , bilinear = true , modern = true } ) => ( {
3752 name : `ImageScript ${ version } ` , package : pkg , kind,
53+ ...modern && { decodes : [ 'avif' , 'jxl' ] } ,
3854 decode : data => IS . Image . decode ( data ) ,
3955 encode : {
4056 png : image => image . encode ( ) ,
4157 jpeg : image => image . encodeJPEG ( 80 ) ,
42- ...webp && { webp : image => image . encodeWebP ( ) }
58+ ...webp && { webp : image => image . encodeWebP ( ) } ,
59+ ...modern && { avif : image => image . encodeAVIF ( { quality : AVIF_QUALITY } ) , jxl : image => image . encodeJXL ( { quality : JXL_QUALITY } ) }
4360 } ,
4461 resize : image => image . clone ( ) . resize ( ...RESIZE , bilinear ? IS . Image . RESIZE_BILINEAR : IS . Image . RESIZE_NEAREST_NEIGHBOR ) ,
4562 rotate : image => image . clone ( ) . rotate ( ANGLE ) ,
@@ -71,7 +88,7 @@ const drawn = (width, height, draw) => {
7188
7289export const libraries = [
7390 imageScript ( ImageScript , { version : '3' , package : 'imagescript' , kind : 'JavaScript' } ) ,
74- imageScript ( ImageScriptV1 , { version : '1' , package : 'imagescript-v1' , kind : 'WebAssembly' , blur : false , webp : false , bilinear : false } ) ,
91+ imageScript ( ImageScriptV1 , { version : '1' , package : 'imagescript-v1' , kind : 'WebAssembly' , blur : false , webp : false , bilinear : false , modern : false } ) ,
7592 {
7693 name : 'Jimp' , package : 'jimp' , kind : 'JavaScript' ,
7794 decode : data => Jimp . fromBuffer ( data ) ,
@@ -104,12 +121,14 @@ export const libraries = [
104121 // PureJsImage works on pipelines from encoded data to encoded data, without decoded images: a decoded image is
105122 // a BMP, its fastest uncompressed format (a BMP to BMP pipeline takes about 40 ms on the photo), and every
106123 // operation runs a pipeline from it back to a BMP, like sharp's raw pixels
107- name : 'PureJsImage' , package : 'purejsimage' , kind : 'JavaScript' ,
124+ name : 'PureJsImage' , package : 'purejsimage' , kind : 'JavaScript' , decodes : [ 'avif' , 'jxl' ] ,
108125 decode : async data => toBmp ( await pureJsImage . open ( data ) ) ,
109126 encode : {
110127 png : async image => ( await pureJsImage . open ( image ) ) . png ( ) . toUint8Array ( ) ,
111128 jpeg : async image => ( await pureJsImage . open ( image ) ) . jpeg ( { quality : 80 } ) . toUint8Array ( ) ,
112- webp : async image => ( await pureJsImage . open ( image ) ) . webp ( { lossless : true } ) . toUint8Array ( )
129+ webp : async image => ( await pureJsImage . open ( image ) ) . webp ( { lossless : true } ) . toUint8Array ( ) ,
130+ // (it has no quality option)
131+ avif : async image => ( await pureJsImage . open ( image ) ) . avif ( ) . toUint8Array ( )
113132 } ,
114133 resize : async image => toBmp ( ( await pureJsImage . open ( image ) ) . resize ( { width : RESIZE [ 0 ] , height : RESIZE [ 1 ] , kernel : 'bilinear' , fit : 'fill' } ) ) ,
115134 rotate : async image => toBmp ( ( await pureJsImage . open ( image ) ) . rotate ( ANGLE , { background : 'transparent' } ) ) ,
@@ -132,12 +151,13 @@ export const libraries = [
132151 clone : image => clonePhoton ( image )
133152 } ,
134153 {
135- name : 'sharp' , package : 'sharp' , kind : 'native (libvips)' ,
154+ name : 'sharp' , package : 'sharp' , kind : 'native (libvips)' , decodes : [ 'avif' ] ,
136155 decode : async data => toRaw ( sharp ( data ) . ensureAlpha ( ) ) ,
137156 encode : {
138157 png : image => pipeline ( image ) . png ( ) . toBuffer ( ) ,
139158 jpeg : image => pipeline ( image ) . jpeg ( { quality : 80 } ) . toBuffer ( ) ,
140- webp : image => pipeline ( image ) . webp ( { lossless : true } ) . toBuffer ( )
159+ webp : image => pipeline ( image ) . webp ( { lossless : true } ) . toBuffer ( ) ,
160+ avif : image => pipeline ( image ) . avif ( { quality : AVIF_QUALITY } ) . toBuffer ( )
141161 } ,
142162 resize : image => toRaw ( pipeline ( image ) . resize ( ...RESIZE , { kernel : 'linear' , fit : 'fill' } ) ) ,
143163 rotate : image => toRaw ( pipeline ( image ) . rotate ( ANGLE , { background : { r : 0 , g : 0 , b : 0 , alpha : 0 } } ) ) ,
@@ -156,6 +176,16 @@ export const libraries = [
156176 . gif ( { delay : info . delay , loop : info . loop } ) . toBuffer ( )
157177 }
158178 } ,
179+ {
180+ // AVIF and JPEG XL only: a decoded image is an ImageData, made from raw pixels for the other inputs
181+ name : 'jsquash' , package : '@jsquash/avif' , packages : [ '@jsquash/avif' , '@jsquash/jxl' ] , kind : 'WebAssembly' , decodes : [ 'avif' , 'jxl' ] ,
182+ fromPixels : ( { data, width, height } ) => ( { data : new Uint8ClampedArray ( data ) , width, height, colorSpace : 'srgb' } ) ,
183+ decode : data => data [ 4 ] === 0x66 && data [ 5 ] === 0x74 ? jsquash [ 'avif/decode' ] ( data ) : jsquash [ 'jxl/decode' ] ( data ) ,
184+ encode : {
185+ avif : image => jsquash [ 'avif/encode' ] ( image , { quality : AVIF_QUALITY } ) ,
186+ jxl : image => jsquash [ 'jxl/encode' ] ( image , { quality : JXL_QUALITY } )
187+ }
188+ } ,
159189 {
160190 name : '@napi-rs/canvas' , package : '@napi-rs/canvas' , kind : 'native (Skia)' , collect : true ,
161191 decode : async data => { const image = await canvas . loadImage ( data ) ; return drawn ( image . width , image . height , ctx => ctx . drawImage ( image , 0 , 0 ) ) ; } ,
0 commit comments