diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 00000000..8369d786 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,68 @@ +name: bench + +# runs the library comparison in bench/ and commits the results (results.json, installs.json, charts, README) to the +# default branch. release.yml runs it after a regular release reached npm, measuring the released version; can also +# be run by hand from the Actions tab, measuring the version bench/package.json has (or the one given) +on: + workflow_call: + inputs: + version: + description: ImageScript version to measure (release.yml passes the released one) + type: string + default: '' + workflow_dispatch: + inputs: + version: + description: ImageScript version to measure (empty keeps bench/package.json's) + type: string + default: '' + +permissions: + contents: write + +concurrency: + group: bench + cancel-in-progress: false + +jobs: + bench: + runs-on: ubuntu-latest + defaults: + run: + working-directory: bench + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.repository.default_branch }} + - uses: actions/setup-node@v7 + with: + node-version: 24.x + + - run: npm ci --no-audit --no-fund + # a version that was just published can take a moment to show up on the registry + - if: inputs.version != '' + run: | + for attempt in 1 2 3 4 5; do + npm install --save-exact --no-audit --no-fund "imagescript@$VERSION" && exit 0 + sleep 30 + done + exit 1 + env: + VERSION: ${{ inputs.version }} + + - run: node run.js + - run: node installs.js + - run: node report.js + + # pushes with GITHUB_TOKEN don't trigger other workflows; rebases onto anything pushed meanwhile + - run: | + git add -A . + if git diff --cached --quiet; then echo "nothing changed"; exit 0; fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "benchmark ImageScript $(node -p "require('./node_modules/imagescript/package.json').version")" + for attempt in 1 2 3; do + git push && exit 0 + git pull --rebase + done + exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e13f2bf..b1e65b6f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,8 @@ name: release # publishing a GitHub release with the tag v publishes that version to npm and JSR. The tag is the version: # the release is checked (semver, changelog, not yet on npm/JSR), tested on every supported platform, published with # the tag's version to npm (with provenance, through trusted publishing) and JSR (through GitHub OIDC, no tokens), -# then the docs are deployed and, for regular releases, the version is committed to the default branch +# then the docs are deployed and, for regular releases, the version is committed to the default branch and the +# benchmark in bench/ is run against the release on: release: types: [published] @@ -110,3 +111,14 @@ jobs: git push env: VERSION: ${{ needs.check.outputs.version }} + + # measures the release against the other libraries and commits the results to bench/ on the default branch (after + # the version commit, so the two don't race) + bench: + needs: [check, version] + if: needs.check.outputs.dist-tag == 'latest' + permissions: + contents: write + uses: ./.github/workflows/bench.yml + with: + version: ${{ needs.check.outputs.version }} diff --git a/README.md b/README.md index 1547bd75..07bf986d 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,13 @@ see the [changelog](https://github.com/matmen/ImageScript/blob/master/CHANGELOG. --- +### Benchmarks + +[bench/](bench/README.md) compares ImageScript to Jimp, image-js, Photon, sharp and @napi-rs/canvas: speed of decoding, +encoding, image operations and rendering, encoded file sizes and install sizes. It runs for every release. + +--- + ### Credits The deflate/inflate implementation is derived from [fflate](https://github.com/101arrowz/fflate) and the CRC-32 from diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 00000000..fb208a5a --- /dev/null +++ b/bench/README.md @@ -0,0 +1,82 @@ +# ImageScript benchmark + +How [ImageScript](https://github.com/matmen/ImageScript) compares to other image libraries for Node.js: speed, file +sizes and what installing each takes. ImageScript is plain JavaScript with no dependencies, so it runs wherever +JavaScript does; sharp and @napi-rs/canvas wrap native libraries (libvips, Skia), Photon and ImageScript 1 are +WebAssembly. + +Install size of each library + +Decoding and encoding speed + +Speed of image operations + +Text and SVG rendering speed + +Animated GIF decoding and encoding speed + +Encoded file sizes + +## Numbers + +Operations per second (more is faster), measured 2026-10-15 with Node.js v24.11.1 on Intel(R) Core(TM) i3-8109U CPU @ 3.00GHz, ImageScript 3.1.0. + +| | **ImageScript 3** | **ImageScript 1** | **Jimp** | **image-js** | **Photon** | **sharp** | **@napi-rs/canvas** | +|---|--:|--:|--:|--:|--:|--:|--:| +| decode JPEG | **9.06** | 3.97 | 4.34 | 5.17 | 16.1 | 37.3 | 42.2 | +| decode PNG | **20.4** | 20.6 | 12.2 | 7.46 | 22.0 | 43.7 | 48.1 | +| decode WebP (lossy) | **7.73** | – | – | – | 7.75 | 30.3 | 53.3 | +| encode JPEG (quality 80) | **9.51** | 14.6 | 1.86 | 6.76 | 6.12 | 49.0 | 30.8 | +| encode PNG | **6.67** | 3.17 | 3.33 | 2.37 | 7.57 | 6.74 | 2.00 | +| encode WebP (lossless) | **0.67** | – | – | – | 17.0 | 0.52 | 1.85 | +| resize to half | **14.9** | 249 | 42.5 | 7.09 | 9.57 | 50.6 | 364 | +| rotate by 30° | **6.39** | 4.22 | 1.20 | 0.85 | 2.64 | 10.6 | 31.7 | +| blur (radius 5) | **4.32** | – | 0.01 | 1.00 | 2.54 | 23.7 | 33.5 | +| composite an overlay | **272** | 408 | 22.4 | 103 | 38.3 | 95.3 | 236 | +| crop | **313** | 428 | 34.4 | 46.3 | 41.1 | 928 | 934 | +| clone | **596** | 575 | 700 | 563 | 42.2 | 712 | 273 | +| render text | **774** | 50.1 | – | – | – | 1,896 | 3,164 | +| render SVG | **3.86** | fails | – | – | – | 12.0 | 51.2 | +| decode animated GIF | **4.95** | 6.77 | – | – | – | 12.5 | – | +| encode animated GIF | **3.39** | 1.01 | – | – | – | 0.80 | – | + +Encoded sizes: + +| | **ImageScript 3** | **ImageScript 1** | **Jimp** | **image-js** | **Photon** | **sharp** | **@napi-rs/canvas** | +|---|--:|--:|--:|--:|--:|--:|--:| +| photo PNG | 2.2 MB | 3.0 MB | 2.7 MB | 2.8 MB | 2.8 MB | 2.8 MB | 2.1 MB | +| photo JPEG | 217 kB | 257 kB | 260 kB | 260 kB | 257 kB | 218 kB | 218 kB | +| photo WebP | 1.4 MB | – | – | – | 1.7 MB | 1.2 MB | 1.4 MB | +| graphic PNG | 1.1 MB | 802 kB | 2.0 MB | 736 kB | 2.2 MB | 718 kB | 1.0 MB | +| graphic JPEG | 275 kB | 314 kB | 315 kB | 315 kB | 314 kB | 275 kB | 276 kB | +| graphic WebP | 579 kB | – | – | – | 856 kB | 239 kB | 631 kB | +| animation GIF | 1.3 MB | 1.3 MB | – | – | – | 720 kB | – | + +| | **ImageScript 3** | **ImageScript 1** | **Jimp** | **image-js** | **Photon** | **sharp** | **@napi-rs/canvas** | +|---|---|---|---|---|---|---|---| +| kind | JavaScript | WebAssembly | JavaScript | JavaScript | WebAssembly | native (libvips) | native (Skia) | +| package | `imagescript@3.1.0` | `imagescript@1.3.1` | `jimp@1.6.1` | `image-js@1.7.1` | `@silvia-odwyer/photon-node@0.3.4` | `sharp@0.35.5` | `@napi-rs/canvas@1.0.11` | +| install size | 639 kB | 6.7 MB | 26.7 MB | 47.8 MB | 2.3 MB | 30.2 MB | 35.0 MB | +| packages | 1 | 1 | 64 | 55 | 1 | 9 | 2 | + +## Method + +- Inputs (in `inputs/`, sources in [inputs/SOURCES.md](inputs/SOURCES.md)): a 1920×1080 photo (JPEG, and made from it + when the benchmark starts, PNG and lossy WebP), a 1434×1434 graphic with transparency (PNG), a 445×445 diagram with + transparency placed on the photo (PNG), a 300×300 GIF with 101 frames. +- Rendering: "The quick brown fox jumps over the lazy dog, 0123456789" in Source Sans 3 at 64px, and `tests/svgs/features.svg` (gradients, clipping, + CSS) at 1920 pixels wide. Jimp and Photon can't render TrueType/OpenType fonts or SVGs; ImageScript 1 crashes + on this SVG; @napi-rs/canvas (Skia's SVG module) ignores its CSS, so it draws a little less. +- Every operation starts from the same decoded image and returns a new one: libraries that change images in place + clone first, sharp runs a pipeline from raw pixels back to raw pixels, @napi-rs/canvas draws into a new canvas. +- Where the library lets you choose, resizing is bilinear (ImageScript 1 only has nearest neighbor), blurring is + gaussian with a radius or sigma of 5, rotating grows the image to fit. Each library uses its own defaults + otherwise, e.g. its PNG compression level. +- [tinybench](https://github.com/tinylibs/tinybench), at least 1.5 seconds and 3 runs per measurement, every library + in its own process. +- The release workflow runs the benchmark for every release and commits the results, on a GitHub Actions runner; + runners vary, so compare libraries within one run rather than between runs. +- Install sizes: `npm install ` in an empty project, the apparent size of `node_modules`. + +Run it: `npm install` in this directory, then `node run.js` (on an idle machine; `IMAGESCRIPT=local` measures +this checkout after `npm run build` instead of the version from npm), `node installs.js` and `node report.js`. diff --git a/bench/bench.js b/bench/bench.js new file mode 100644 index 00000000..db864245 --- /dev/null +++ b/bench/bench.js @@ -0,0 +1,67 @@ +// Measures one library (node --expose-gc bench.js ) and prints its results as JSON: operations per second for +// decoding, encoding, the image operations and rendering, and the encoded file sizes. run.js runs every library in +// its own process, so one library's memory can't affect the next one's measurements +import fs from 'node:fs'; +import sharp from 'sharp'; +import { Bench } from 'tinybench'; +import { libraries } from './libs.js'; + +const lib = libraries.find(l => l.name === process.argv[2]); +if (!lib) throw new Error(`unknown library ${process.argv[2]}, one of: ${libraries.map(l => l.name).join(', ')}`); + +const files = { + 'photo.jpg': fs.readFileSync(new URL('./inputs/photo.jpg', import.meta.url)), + 'graphic.png': fs.readFileSync(new URL('./inputs/graphic.png', import.meta.url)), + 'overlay.png': fs.readFileSync(new URL('./inputs/overlay.png', import.meta.url)), + 'animation.gif': fs.readFileSync(new URL('./inputs/animation.gif', import.meta.url)) +}; +files['photo.png'] = await sharp(files['photo.jpg']).png().toBuffer(); +files['photo.webp'] = await sharp(files['photo.jpg']).webp({ quality: 80 }).toBuffer(); + +const speed = {}, sizes = {}; +const formatName = format => ({ jpeg: 'JPEG', png: 'PNG', webp: 'WebP' })[format]; + +// collect: canvases hold their pixels in native memory that V8 doesn't count and only free it in finalizers that run +// on a later event-loop turn, which tinybench's loop never yields to. So between iterations (outside the measured +// time) garbage is collected and the loop gets a turn, instead of the pixels piling up until the process is killed +async function measure(task, fn) { + const bench = new Bench({ time: 1500, iterations: 3, warmupTime: 300, warmupIterations: 1 }); + bench.add(task, fn, lib.collect && globalThis.gc ? { afterEach: async () => { globalThis.gc(); await new Promise(setImmediate); } } : {}); + await bench.run(); + const r = bench.tasks[0].result; + if (r.error) throw r.error; + const ms = r.latency?.mean ?? r.mean; + speed[task] = { opsPerSecond: Math.round(1000 / ms * 100) / 100, ms: Math.round(ms * 100) / 100, samples: r.latency?.samples?.length ?? r.samples?.length }; + console.error(`${task.padEnd(16)} ${lib.name.padEnd(16)} ${(1000 / ms).toFixed(2).padStart(10)} ops/s ${ms.toFixed(2).padStart(9)} ms`); +} + +const decode = data => Promise.resolve().then(() => lib.decode(data)); +const photo = await decode(files['photo.jpg']), overlay = await decode(files['overlay.png']); + +for (const [task, file] of [['decode JPEG', 'photo.jpg'], ['decode PNG', 'photo.png'], ['decode WebP', 'photo.webp']]) + if (await decode(files[file]).then(() => true, () => false)) await measure(task, () => decode(files[file])); +for (const [format, encode] of Object.entries(lib.encode)) + await measure(`encode ${formatName(format)}`, () => encode(photo)); +for (const op of ['resize', 'rotate', 'blur', 'composite', 'crop', 'clone']) + if (lib[op]) await measure(op, () => lib[op](photo, overlay)); +// a library that has the feature but fails on this input (ImageScript 1 on the SVG) is recorded as failing +for (const op of ['text', 'svg']) { + if (!lib[op]) continue; + const error = await Promise.resolve().then(() => lib[op]()).then(() => null, e => e); + if (error) speed[op] = { error: String(error.message ?? error) }; + else await measure(op, () => lib[op]()); +} +if (lib.animation) { + const animation = await lib.animation.decode(files['animation.gif']); + await measure('decode GIF', () => lib.animation.decode(files['animation.gif'])); + await measure('encode GIF', () => lib.animation.encode(animation)); +} + +// encoded sizes, with each library's default settings (JPEG at quality 80, WebP lossless) +for (const [name, file] of [['photo', 'photo.jpg'], ['graphic', 'graphic.png']]) { + const image = await decode(files[file]); + for (const [format, encode] of Object.entries(lib.encode)) sizes[`${name} ${formatName(format)}`] = (await encode(image)).length; +} +if (lib.animation) sizes['animation GIF'] = (await lib.animation.encode(await lib.animation.decode(files['animation.gif']))).length; + +console.log(JSON.stringify({ speed, sizes })); diff --git a/bench/charts/animation.svg b/bench/charts/animation.svg new file mode 100644 index 00000000..9dd28e35 --- /dev/null +++ b/bench/charts/animation.svg @@ -0,0 +1,15 @@ + +Animations + +Animations +operations per second on a 300×300 GIF with 101 frames, more is faster · Node.js 24, Intel Core i3-8109U +decode animated GIFImageScript 34.95ImageScript 16.77Jimpnot supportedimage-jsnot supportedPhotonnot supportedsharp12.5@napi-rs/canvasnot supportedencode animated GIFImageScript 33.39ImageScript 11.01Jimpnot supportedimage-jsnot supportedPhotonnot supportedsharp0.80@napi-rs/canvasnot supported + diff --git a/bench/charts/codecs.svg b/bench/charts/codecs.svg new file mode 100644 index 00000000..f8d23710 --- /dev/null +++ b/bench/charts/codecs.svg @@ -0,0 +1,15 @@ + +Decoding and encoding + +Decoding and encoding +operations per second on a 1920×1080 photo, more is faster · Node.js 24, Intel Core i3-8109U +decode JPEGImageScript 39.06ImageScript 13.97Jimp4.34image-js5.17Photon16.1sharp37.3@napi-rs/canvas42.2decode PNGImageScript 320.4ImageScript 120.6Jimp12.2image-js7.46Photon22.0sharp43.7@napi-rs/canvas48.1decode WebP (lossy)ImageScript 37.73ImageScript 1not supportedJimpnot supportedimage-jsnot supportedPhoton7.75sharp30.3@napi-rs/canvas53.3encode JPEG (quality 80)ImageScript 39.51ImageScript 114.6Jimp1.86image-js6.76Photon6.12sharp49.0@napi-rs/canvas30.8encode PNGImageScript 36.67ImageScript 13.17Jimp3.33image-js2.37Photon7.57sharp6.74@napi-rs/canvas2.00encode WebP (lossless)ImageScript 30.67ImageScript 1not supportedJimpnot supportedimage-jsnot supportedPhoton17.0sharp0.52@napi-rs/canvas1.85 + diff --git a/bench/charts/install.svg b/bench/charts/install.svg new file mode 100644 index 00000000..a32629ef --- /dev/null +++ b/bench/charts/install.svg @@ -0,0 +1,15 @@ + +Install size + +Install size +node_modules after npm install in an empty project (all dependencies and native binaries), less is better +install sizeImageScript 3639 kBImageScript 16.7 MBJimp26.7 MBimage-js47.8 MBPhoton2.3 MBsharp30.2 MB@napi-rs/canvas35.0 MB + diff --git a/bench/charts/operations.svg b/bench/charts/operations.svg new file mode 100644 index 00000000..463bcc96 --- /dev/null +++ b/bench/charts/operations.svg @@ -0,0 +1,15 @@ + +Image operations + +Image operations +operations per second on a 1920×1080 photo, more is faster · Node.js 24, Intel Core i3-8109U +resize to halfImageScript 1: nearest neighbor only, others bilinearImageScript 314.9ImageScript 1249Jimp42.5image-js7.09Photon9.57sharp50.6@napi-rs/canvas364rotate by 30°ImageScript 36.39ImageScript 14.22Jimp1.20image-js0.85Photon2.64sharp10.6@napi-rs/canvas31.7blur (radius 5)ImageScript 34.32ImageScript 1not supportedJimp0.01image-js1.00Photon2.54sharp23.7@napi-rs/canvas33.5composite an overlayImageScript 3272ImageScript 1408Jimp22.4image-js103Photon38.3sharp95.3@napi-rs/canvas236cropImageScript 3313ImageScript 1428Jimp34.4image-js46.3Photon41.1sharp928@napi-rs/canvas934cloneImageScript 3596ImageScript 1575Jimp700image-js563Photon42.2sharp712@napi-rs/canvas273 + diff --git a/bench/charts/rendering.svg b/bench/charts/rendering.svg new file mode 100644 index 00000000..5bfd9ba0 --- /dev/null +++ b/bench/charts/rendering.svg @@ -0,0 +1,15 @@ + +Rendering + +Rendering +operations per second (text at 64px, SVG 1920 wide), more is faster · Node.js 24, Intel Core i3-8109U +render textImageScript 3774ImageScript 150.1Jimpnot supportedimage-jsnot supportedPhotonnot supportedsharp1,896@napi-rs/canvas3,164render SVGImageScript 33.86ImageScript 1fails on this inputJimpnot supportedimage-jsnot supportedPhotonnot supportedsharp12.0@napi-rs/canvas51.2 + diff --git a/bench/charts/sizes.svg b/bench/charts/sizes.svg new file mode 100644 index 00000000..42f26d91 --- /dev/null +++ b/bench/charts/sizes.svg @@ -0,0 +1,15 @@ + +File sizes + +File sizes +encoded with each library's default settings, less is better +photo PNGImageScript 32.2 MBImageScript 13.0 MBJimp2.7 MBimage-js2.8 MBPhoton2.8 MBsharp2.8 MB@napi-rs/canvas2.1 MBphoto JPEG (quality 80)ImageScript 3217 kBImageScript 1257 kBJimp260 kBimage-js260 kBPhoton257 kBsharp218 kB@napi-rs/canvas218 kBphoto WebP (lossless)ImageScript 31.4 MBImageScript 1not supportedJimpnot supportedimage-jsnot supportedPhoton1.7 MBsharp1.2 MB@napi-rs/canvas1.4 MBgraphic PNGImageScript 31.1 MBImageScript 1802 kBJimp2.0 MBimage-js736 kBPhoton2.2 MBsharp718 kB@napi-rs/canvas1.0 MBgraphic JPEG (quality 80)ImageScript 3275 kBImageScript 1314 kBJimp315 kBimage-js315 kBPhoton314 kBsharp275 kB@napi-rs/canvas276 kBgraphic WebP (lossless)ImageScript 3579 kBImageScript 1not supportedJimpnot supportedimage-jsnot supportedPhoton856 kBsharp239 kB@napi-rs/canvas631 kBanimation GIFImageScript 31.3 MBImageScript 11.3 MBJimpnot supportedimage-jsnot supportedPhotonnot supportedsharp720 kB@napi-rs/canvasnot supported + diff --git a/bench/inputs/SOURCES.md b/bench/inputs/SOURCES.md new file mode 100644 index 00000000..4a6df5b2 --- /dev/null +++ b/bench/inputs/SOURCES.md @@ -0,0 +1,14 @@ +# Benchmark inputs + +Unmodified files from Wikimedia Commons. The PNG and WebP versions of the photo are made from `photo.jpg` when the +benchmark runs. + +| File | Source | Author | License | +|---|---|---|---| +| `photo.jpg` | [Various smartphones - Android, Blackberry, Windows Phone, and iOS.jpg](https://commons.wikimedia.org/wiki/File:Various_smartphones_-_Android,_Blackberry,_Windows_Phone,_and_iOS.jpg) | Carlos Varela | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | +| `graphic.png` | [Diagram - Inkluzivní design.png](https://commons.wikimedia.org/wiki/File:Diagram_-_Inkluzivn%C3%AD_design.png) | Adam Hikl | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `overlay.png` | [Extranet Diagram.png](https://commons.wikimedia.org/wiki/File:Extranet_Diagram.png) | Slade74 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `animation.gif` | [3D Wavefunction (2,2,1).gif](https://commons.wikimedia.org/wiki/File:3D_Wavefunction_(2,2,1).gif) | Inductiveload | Public domain | + +The text benchmark uses `tests/fonts/sourcesans3 regular.otf` (SIL Open Font License, see +`tests/fonts/OFL-SourceSans3.md`), and the SVG benchmark uses `tests/svgs/features.svg`. diff --git a/bench/inputs/animation.gif b/bench/inputs/animation.gif new file mode 100644 index 00000000..e91331ab Binary files /dev/null and b/bench/inputs/animation.gif differ diff --git a/bench/inputs/graphic.png b/bench/inputs/graphic.png new file mode 100644 index 00000000..8f135829 Binary files /dev/null and b/bench/inputs/graphic.png differ diff --git a/bench/inputs/overlay.png b/bench/inputs/overlay.png new file mode 100644 index 00000000..531e9912 Binary files /dev/null and b/bench/inputs/overlay.png differ diff --git a/bench/inputs/photo.jpg b/bench/inputs/photo.jpg new file mode 100644 index 00000000..32e6a673 Binary files /dev/null and b/bench/inputs/photo.jpg differ diff --git a/bench/installs.js b/bench/installs.js new file mode 100644 index 00000000..496052cd --- /dev/null +++ b/bench/installs.js @@ -0,0 +1,33 @@ +// Measures what installing each library takes: npm install @ in an empty project (the versions +// installed here), then the apparent size of node_modules and the number of packages in it. Writes installs.json +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { libraries } from './libs.js'; + +const size = dir => fs.readdirSync(dir, { withFileTypes: true }).reduce((sum, entry) => { + const p = path.join(dir, entry.name); + return sum + (entry.isDirectory() ? size(p) : entry.isFile() ? fs.statSync(p).size : 0); +}, 0); + +const installs = {}; +for (const lib of libraries) { + const installed = JSON.parse(fs.readFileSync(new URL(`./node_modules/${lib.package}/package.json`, import.meta.url))); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'imagescript-bench-')); + try { + fs.writeFileSync(path.join(dir, 'package.json'), '{}'); + execFileSync('npm', ['install', `${installed.name}@${installed.version}`, '--no-audit', '--no-fund', '--loglevel=error'], { cwd: dir, stdio: 'inherit' }); + const lock = JSON.parse(fs.readFileSync(path.join(dir, 'package-lock.json'))); + installs[lib.name] = { + package: `${installed.name}@${installed.version}`, + bytes: size(path.join(dir, 'node_modules')), + // the lockfile also lists optional packages for other platforms, which aren't installed + packages: Object.keys(lock.packages).filter(p => p.startsWith('node_modules/') && fs.existsSync(path.join(dir, p))).length + }; + console.log(lib.name, installs[lib.name]); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} +fs.writeFileSync(new URL('./installs.json', import.meta.url), JSON.stringify(installs, null, 2) + '\n'); diff --git a/bench/installs.json b/bench/installs.json new file mode 100644 index 00000000..cfb90997 --- /dev/null +++ b/bench/installs.json @@ -0,0 +1,37 @@ +{ + "ImageScript 3": { + "package": "imagescript@3.1.0", + "bytes": 638838, + "packages": 1 + }, + "ImageScript 1": { + "package": "imagescript@1.3.1", + "bytes": 6680176, + "packages": 1 + }, + "Jimp": { + "package": "jimp@1.6.1", + "bytes": 26735460, + "packages": 64 + }, + "image-js": { + "package": "image-js@1.7.1", + "bytes": 47817457, + "packages": 55 + }, + "Photon": { + "package": "@silvia-odwyer/photon-node@0.3.4", + "bytes": 2266121, + "packages": 1 + }, + "sharp": { + "package": "sharp@0.35.5", + "bytes": 30159789, + "packages": 9 + }, + "@napi-rs/canvas": { + "package": "@napi-rs/canvas@1.0.11", + "bytes": 34977366, + "packages": 2 + } +} diff --git a/bench/libs.js b/bench/libs.js new file mode 100644 index 00000000..6a081a28 --- /dev/null +++ b/bench/libs.js @@ -0,0 +1,174 @@ +// One adapter per library, all with the same operations. Operations take a decoded image and return a new one +// (libraries that change images in place clone first), so every run starts from the same input. Missing operations +// are ones the library doesn't have. +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +const require = createRequire(import.meta.url); + +// IMAGESCRIPT=local measures this checkout (npm run build first) instead of the version from npm +const ImageScript = await import(process.env.IMAGESCRIPT === 'local' ? '../dist/index.js' : 'imagescript'); +const ImageScriptV1 = require('imagescript-v1'); +const { Jimp, ResizeStrategy } = await import('jimp'); +const sharp = (await import('sharp')).default; +const canvas = await import('@napi-rs/canvas'); +const imageJS = await import('image-js'); +const photon = require('@silvia-odwyer/photon-node'); + +// the same work in every library: halve the size (bilinear where it can be chosen), rotate by 30° (growing the +// image to fit), blur with a radius/sigma of 5, place a semi-transparent overlay, cut out a quarter +const RESIZE = [960, 540], ANGLE = 30, BLUR = 5, AT = [100, 100], CROP = [480, 270, 960, 540]; + +// rendering: one line of text in Source Sans 3 at 64px, white; tests/svgs/features.svg (240×160) scaled to 1920 wide +export const FONT_FILE = new URL('../tests/fonts/sourcesans3 regular.otf', import.meta.url); +export const SVG_FILE = new URL('../tests/svgs/features.svg', import.meta.url); +export const TEXT = 'The quick brown fox jumps over the lazy dog, 0123456789', TEXT_SIZE = 64, SVG_WIDTH = 1920; +const font = fs.readFileSync(FONT_FILE), svg = fs.readFileSync(SVG_FILE, 'utf8'); +const svgScale = SVG_WIDTH / Number(svg.match(/]*\swidth="(\d+)/)[1]); +const svgAtWidth = svg.replace(/]*>/, tag => tag.replace(/\s(width|height)="(\d+)"/g, (_, name, value) => ` ${name}="${Math.round(value * svgScale)}"`)); + +const imageScript = (IS, { version, package: pkg, kind, blur = true, webp = true, bilinear = true }) => ({ + name: `ImageScript ${version}`, package: pkg, kind, + decode: data => IS.Image.decode(data), + encode: { + png: image => image.encode(), + jpeg: image => image.encodeJPEG(80), + ...webp && { webp: image => image.encodeWebP() } + }, + resize: image => image.clone().resize(...RESIZE, bilinear ? IS.Image.RESIZE_BILINEAR : IS.Image.RESIZE_NEAREST_NEIGHBOR), + rotate: image => image.clone().rotate(ANGLE), + ...blur && { blur: image => image.clone().blur(BLUR) }, + composite: (image, overlay) => image.clone().composite(overlay, ...AT), + crop: image => image.clone().crop(...CROP), + clone: image => image.clone(), + text: () => IS.Image.renderText(font, TEXT_SIZE, TEXT), + svg: () => IS.Image.renderSVG(svg, SVG_WIDTH, IS.Image.SVG_MODE_WIDTH), + animation: { + decode: data => (IS.Animation ?? IS.GIF).decode(data), + encode: animation => animation.encodeGIF ? animation.encodeGIF() : animation.encode() + } +}); + +// sharp works on pipelines: a decoded image is its raw pixels, every operation runs a pipeline back to raw pixels +const raw = (data, info) => ({ data, width: info.width, height: info.height }); +const pipeline = image => sharp(image.data, { raw: { width: image.width, height: image.height, channels: 4 } }); +const toRaw = async p => { const { data, info } = await p.raw().toBuffer({ resolveWithObject: true }); return raw(data, info); }; + +// canvas: a decoded image is a canvas +const drawn = (width, height, draw) => { + const c = canvas.createCanvas(width, height); + const ctx = c.getContext('2d'); + draw(ctx); + ctx.getImageData(0, 0, 1, 1); // Skia draws lazily: reading a pixel makes it actually render inside the measured call + return c; +}; + +export const libraries = [ + imageScript(ImageScript, { version: '3', package: 'imagescript', kind: 'JavaScript' }), + imageScript(ImageScriptV1, { version: '1', package: 'imagescript-v1', kind: 'WebAssembly', blur: false, webp: false, bilinear: false }), + { + name: 'Jimp', package: 'jimp', kind: 'JavaScript', + decode: data => Jimp.fromBuffer(data), + encode: { + png: image => image.getBuffer('image/png'), + jpeg: image => image.getBuffer('image/jpeg', { quality: 80 }) + }, + resize: image => image.clone().resize({ w: RESIZE[0], h: RESIZE[1], mode: ResizeStrategy.BILINEAR }), + rotate: image => image.clone().rotate(ANGLE), + blur: image => image.clone().gaussian(BLUR), + composite: (image, overlay) => image.clone().composite(overlay, ...AT), + crop: image => image.clone().crop({ x: CROP[0], y: CROP[1], w: CROP[2], h: CROP[3] }), + clone: image => image.clone() + }, + { + name: 'image-js', package: 'image-js', kind: 'JavaScript', + decode: data => imageJS.decode(data).convertColor('RGBA'), + encode: { + png: image => imageJS.encodePng(image), + jpeg: image => imageJS.encodeJpeg(image, { quality: 80 }) + }, + resize: image => image.resize({ width: RESIZE[0], height: RESIZE[1], interpolationType: 'bilinear' }), + rotate: image => image.transformRotate(ANGLE, { fullImage: true }), + blur: image => image.gaussianBlur({ sigma: BLUR }), + composite: (image, overlay) => overlay.copyTo(image.clone(), { origin: { column: AT[0], row: AT[1] } }), + crop: image => image.crop({ origin: { column: CROP[0], row: CROP[1] }, width: CROP[2], height: CROP[3] }), + clone: image => image.clone() + }, + { + name: 'Photon', package: '@silvia-odwyer/photon-node', kind: 'WebAssembly', + decode: data => photon.PhotonImage.new_from_byteslice(new Uint8Array(data)), + encode: { + png: image => image.get_bytes(), + jpeg: image => image.get_bytes_jpeg(80), + webp: image => image.get_bytes_webp() + }, + resize: image => photon.resize(image, ...RESIZE, photon.SamplingFilter.Triangle), + rotate: image => photon.rotate(image, ANGLE), + blur: image => { const copy = clonePhoton(image); photon.gaussian_blur(copy, BLUR); return copy; }, + composite: (image, overlay) => { const copy = clonePhoton(image); photon.watermark(copy, overlay, BigInt(AT[0]), BigInt(AT[1])); return copy; }, + crop: image => photon.crop(image, CROP[0], CROP[1], CROP[0] + CROP[2], CROP[1] + CROP[3]), + clone: image => clonePhoton(image) + }, + { + name: 'sharp', package: 'sharp', kind: 'native (libvips)', + decode: async data => toRaw(sharp(data).ensureAlpha()), + encode: { + png: image => pipeline(image).png().toBuffer(), + jpeg: image => pipeline(image).jpeg({ quality: 80 }).toBuffer(), + webp: image => pipeline(image).webp({ lossless: true }).toBuffer() + }, + resize: image => toRaw(pipeline(image).resize(...RESIZE, { kernel: 'linear', fit: 'fill' })), + rotate: image => toRaw(pipeline(image).rotate(ANGLE, { background: { r: 0, g: 0, b: 0, alpha: 0 } })), + blur: image => toRaw(pipeline(image).blur(BLUR)), + composite: async (image, overlay) => toRaw(pipeline(image).composite([{ + input: overlay.data, raw: { width: overlay.width, height: overlay.height, channels: 4 }, left: AT[0], top: AT[1] + }])), + crop: image => toRaw(pipeline(image).extract({ left: CROP[0], top: CROP[1], width: CROP[2], height: CROP[3] })), + clone: image => raw(Buffer.from(image.data), image), + // pango sizes fonts in points: at 72 dpi a point is a pixel + text: () => toRaw(sharp({ text: { text: `${TEXT}`, font: `Source Sans 3 ${TEXT_SIZE}`, fontfile: fileURLToPath(FONT_FILE), dpi: 72, rgba: true } })), + svg: () => toRaw(sharp(Buffer.from(svg), { density: 72 * svgScale }).ensureAlpha()), + animation: { + decode: async data => { const { data: pixels, info } = await sharp(data, { animated: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); return { pixels, info }; }, + encode: ({ pixels, info }) => sharp(pixels, { raw: { width: info.width, height: info.height, channels: 4, pageHeight: info.pageHeight } }) + .gif({ delay: info.delay, loop: info.loop }).toBuffer() + } + }, + { + name: '@napi-rs/canvas', package: '@napi-rs/canvas', kind: 'native (Skia)', collect: true, + decode: async data => { const image = await canvas.loadImage(data); return drawn(image.width, image.height, ctx => ctx.drawImage(image, 0, 0)); }, + encode: { + png: c => c.encode('png'), + jpeg: c => c.encode('jpeg', 80), + webp: c => c.encode('webp', 100) + }, + resize: c => drawn(...RESIZE, ctx => ctx.drawImage(c, 0, 0, ...RESIZE)), + rotate: c => { + const r = ANGLE * Math.PI / 180, w = Math.ceil(c.width * Math.cos(r) + c.height * Math.sin(r)), h = Math.ceil(c.width * Math.sin(r) + c.height * Math.cos(r)); + return drawn(w, h, ctx => { ctx.translate(w / 2, h / 2); ctx.rotate(-r); ctx.drawImage(c, -c.width / 2, -c.height / 2); }); + }, + blur: c => drawn(c.width, c.height, ctx => { ctx.filter = `blur(${BLUR}px)`; ctx.drawImage(c, 0, 0); }), + composite: (c, overlay) => drawn(c.width, c.height, ctx => { ctx.drawImage(c, 0, 0); ctx.drawImage(overlay, ...AT); }), + crop: c => drawn(CROP[2], CROP[3], ctx => ctx.drawImage(c, -CROP[0], -CROP[1])), + clone: c => drawn(c.width, c.height, ctx => ctx.drawImage(c, 0, 0)), + text: () => { + const measure = canvas.createCanvas(1, 1).getContext('2d'); + measure.font = `${TEXT_SIZE}px "Source Sans 3"`; + const m = measure.measureText(TEXT); + return drawn(Math.ceil(m.width), Math.ceil(m.actualBoundingBoxAscent + m.actualBoundingBoxDescent), ctx => { + ctx.font = measure.font; ctx.fillStyle = '#fff'; ctx.fillText(TEXT, 0, Math.ceil(m.actualBoundingBoxAscent)); + }); + }, + // Skia rasterizes an SVG at its own width and height, so they're set to the output size (the viewBox scales) + svg: async () => { + const image = await canvas.loadImage(Buffer.from(svgAtWidth)); + return drawn(image.width, image.height, ctx => ctx.drawImage(image, 0, 0)); + } + } +]; +canvas.GlobalFonts.register(font, 'Source Sans 3'); + +function clonePhoton(image) { + return new photon.PhotonImage(image.get_raw_pixels(), image.get_width(), image.get_height()); +} + diff --git a/bench/package-lock.json b/bench/package-lock.json new file mode 100644 index 00000000..bd92cf7b --- /dev/null +++ b/bench/package-lock.json @@ -0,0 +1,2147 @@ +{ + "name": "imagescript-bench", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "imagescript-bench", + "dependencies": { + "@napi-rs/canvas": "1.0.11", + "@silvia-odwyer/photon-node": "0.3.4", + "image-js": "1.7.1", + "imagescript": "3.1.0", + "imagescript-v1": "npm:imagescript@1.3.1", + "jimp": "1.6.1", + "sharp": "0.35.5", + "tinybench": "6.2.2" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jimp/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", + "license": "MIT", + "dependencies": { + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^21.3.3", + "mime": "3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/core/node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@jimp/diff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", + "license": "MIT", + "dependencies": { + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/file-ops": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-bmp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-gif": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-jpeg": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-png": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-tiff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "utif2": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/canvas": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.11.tgz", + "integrity": "sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "1.0.11", + "@napi-rs/canvas-darwin-arm64": "1.0.11", + "@napi-rs/canvas-darwin-x64": "1.0.11", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.11", + "@napi-rs/canvas-linux-arm64-gnu": "1.0.11", + "@napi-rs/canvas-linux-arm64-musl": "1.0.11", + "@napi-rs/canvas-linux-riscv64-gnu": "1.0.11", + "@napi-rs/canvas-linux-x64-gnu": "1.0.11", + "@napi-rs/canvas-linux-x64-musl": "1.0.11", + "@napi-rs/canvas-win32-arm64-msvc": "1.0.11", + "@napi-rs/canvas-win32-x64-msvc": "1.0.11" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.11.tgz", + "integrity": "sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.11.tgz", + "integrity": "sha512-J/JylWoJTG8OQqZaiqly4IODyAxNofUlZo0wtSBJtHk6AdymbdIkPynAM1JvQaZpPaG5h+msk9QuK8y4i1ooqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.11.tgz", + "integrity": "sha512-nlecCk3xy06eAGC7krZtzNDm9UNTHKuHQYK8ZYegdjzgSL8anGt/eLJZm86OWaNgZkwQwvDqfSmG8Nqe6MPZNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.11.tgz", + "integrity": "sha512-DKImmG+sTPolE0kGPsYPBxTtOjb3INALp32O2lW2CfSr0bAzSEKKuu+GrQbtilClyoPOahUErS3OrznFDraoWA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.11.tgz", + "integrity": "sha512-55sykACGENj5j1rhj+81AO8JbR9HYgGjRZ7/H+pNr4VE1DvINKYNMTcutk8ZmWxk276jAQvUKJ3+Kdj9GXU3lQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.11.tgz", + "integrity": "sha512-6Jz6jys17yXydO+WdJgu7JhoB4K7/w9sWHByzzTmLjQghUx3TDMWvLv+UcCdnWhg4oprET67EMymnrETdeGNAg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.11.tgz", + "integrity": "sha512-lf2XeEebWiUYVOVEjLXxF7dfXrBBSEOBn9AqBh39VYXrPt5cDqI+1t58IFSjn8npqkg5jIOs8TQbjKABbFW1gQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.11.tgz", + "integrity": "sha512-iDapw7nFSE5tsmQxcH2+KotJhyfVPmHd6vUOQN/Em+8WNbkZJwJ/rDowlJMFnMl0SYSDSfgKCNUGi6Sn5++Smg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.11.tgz", + "integrity": "sha512-26q26AyITo3CGZX4HWFIMgBMA1lu3YN07gxzBm4tW6Hglq/OVyTP50kDrmWuKybCX6dh3m8Ly+c7YhI+sqCgCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.11.tgz", + "integrity": "sha512-cUO96IdsfvIj2S+2PNcDSkTcLhjPj8pIOEWRV1Kj70MOP4wnE9zSk7N4C1EKHlFGRcgdR5AkbnBZj0dfHK9nbQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.11.tgz", + "integrity": "sha512-DFYp/ZQRdKunI/LPc0UkRXG1vlNjJKOJf8O8Ctb8+wm2Nt2ELg9Eamc31D2VoakkfGo8L+z0Iejz6WtFbQwyGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-search": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==", + "license": "CC0-1.0" + }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "license": "MIT" + }, + "node_modules/bresenham-zingl": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/bresenham-zingl/-/bresenham-zingl-0.2.8.tgz", + "integrity": "sha512-AaC4k7+pHgbzlK4fb+l3/fXJd1CbQwhqE5oIoLaknVojyXQv79einSR+fWLR1yCJWOh46v+lLsFRMxHlea8Q5A==", + "license": "MIT" + }, + "node_modules/cheminfo-types": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/cheminfo-types/-/cheminfo-types-1.16.0.tgz", + "integrity": "sha512-j0MiXFnLRVEZg3vUr7SXm6F3HuDNVE5arfQyDMiy1w2fAcuOs3QRgVsw7aOIACvr0aqbvn85cucwt+yOu64IiQ==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, + "node_modules/fast-bmp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fast-bmp/-/fast-bmp-4.0.1.tgz", + "integrity": "sha512-+KtMijJj+uA8Sl6EXAnhza7US7EXSY5Z9NeiJwT1wopVUksyLMXL5iFmn9FjY8FdkstOkpJI9RuEVXkGpIPSwg==", + "license": "MIT", + "dependencies": { + "iobuffer": "^6.0.0" + } + }, + "node_modules/fast-jpeg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-jpeg/-/fast-jpeg-3.0.1.tgz", + "integrity": "sha512-1r4CO4rgRY1CeqaHlXVBPhdGC+rbNwWFJvV9s3HTUC+svqTE7hC1cEH9srp5MPgAmhDdsZ/WtU3WGM8zVobFCA==", + "license": "MIT", + "dependencies": { + "iobuffer": "^6.0.0", + "tiff": "^7.1.0" + } + }, + "node_modules/fast-png": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-8.0.0.tgz", + "integrity": "sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.8.2", + "iobuffer": "^6.0.1" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/fft.js": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/fft.js/-/fft.js-4.0.4.tgz", + "integrity": "sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-js": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/image-js/-/image-js-1.7.1.tgz", + "integrity": "sha512-n8/psrUfO7v7wBjTY2OMhj2OND24T6COvBoXNFCYI5HWq4QTToZWP34yFU2Dy5ESajp8BbmjdHjxMqEoj8cBPg==", + "license": "MIT", + "dependencies": { + "bresenham-zingl": "^0.2.5", + "colord": "^2.10.0", + "fast-bmp": "^4.0.1", + "fast-jpeg": "^3.0.1", + "fast-png": "^8.0.0", + "image-type": "^4.1.0", + "jpeg-js": "^0.4.4", + "js-priority-queue": "^0.1.5", + "median-quickselect": "^1.0.1", + "ml-affine-transform": "^1.0.3", + "ml-convolution": "^2.0.0", + "ml-matrix": "^6.15.0", + "ml-ransac": "^1.0.0", + "ml-regression-multivariate-linear": "^2.0.4", + "ml-regression-polynomial-2d": "^1.0.0", + "ml-spectra-processing": "^14.34.0", + "robust-point-in-polygon": "^1.0.3", + "ssim.js": "^3.5.0", + "tiff": "^7.1.3", + "ts-pattern": "^5.9.0", + "uint8-base64": "^1.0.0" + }, + "optionalDependencies": { + "skia-canvas": "^3.0.8" + } + }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-type": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz", + "integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==", + "license": "MIT", + "dependencies": { + "file-type": "^10.10.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagescript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/imagescript/-/imagescript-3.1.0.tgz", + "integrity": "sha512-5vF6TRHIyYx+EVfAWm3d7mQGolMC6NUWlnp5wp0gHy6GubRRRJAaUx7TovpZlhp1E5wk88J27V7Y8pz/+lT53Q==", + "license": "(AGPL-3.0-or-later OR MIT)", + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/imagescript-v1": { + "name": "imagescript", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/imagescript/-/imagescript-1.3.1.tgz", + "integrity": "sha512-ue/zxSyEzj7je8Nlt2vjY9GEa2BbScFSRZJq7OTVDZFp0r57fyuxrlsF8qWgxTP+kP8WklTw4by/ZEYVX5S13w==", + "license": "(AGPL-3.0-or-later OR MIT)", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/iobuffer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-6.0.1.tgz", + "integrity": "sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==", + "license": "MIT" + }, + "node_modules/is-any-array": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz", + "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==", + "license": "MIT" + }, + "node_modules/jimp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-priority-queue": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/js-priority-queue/-/js-priority-queue-0.1.5.tgz", + "integrity": "sha512-2dPmJT4GbXUpob7AZDR1wFMKz3Biy6oW69mwt5PTtdeoOgDin1i0p5gUV9k0LFeUxDpwkfr+JGMZDpcprjiY5w==", + "license": "Public Domain" + }, + "node_modules/median-quickselect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/median-quickselect/-/median-quickselect-1.0.1.tgz", + "integrity": "sha512-/QL9ptNuLsdA68qO+2o10TKCyu621zwwTFdLvtu8rzRNKsn8zvuGoq/vDxECPyELFG8wu+BpyoMR9BnsJqfVZQ==", + "license": "ISC" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ml-affine-transform": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ml-affine-transform/-/ml-affine-transform-1.0.3.tgz", + "integrity": "sha512-uv13li9HOGuMdSKL+QEdaV25emO9kt17LzGCD+FLQrBsrU8p/r/3kS/GJ7xI+f3/aWghkBK733x0yBG6EwKFJA==", + "license": "MIT", + "dependencies": { + "ml-matrix": "^6.10.4" + } + }, + "node_modules/ml-array-max": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-2.0.0.tgz", + "integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0" + } + }, + "node_modules/ml-array-median": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ml-array-median/-/ml-array-median-1.1.6.tgz", + "integrity": "sha512-V6bV6bTPFRX8v5CaAx/7fuRXC39LLTHfPSVZZafdNaqNz2PFL5zEA7gesjv8dMXh+gwPeUMtB5QPovlTBaa4sw==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0", + "median-quickselect": "^1.0.1" + } + }, + "node_modules/ml-array-median/node_modules/is-any-array": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz", + "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==", + "license": "MIT" + }, + "node_modules/ml-array-min": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-2.0.0.tgz", + "integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0" + } + }, + "node_modules/ml-array-rescale": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz", + "integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0", + "ml-array-max": "^2.0.0", + "ml-array-min": "^2.0.0" + } + }, + "node_modules/ml-convolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-convolution/-/ml-convolution-2.0.0.tgz", + "integrity": "sha512-ExW6zVmN2YGuyO3aBiS1ymybme3nVgv2ccCfynSdgtW5sNp4DOHnfow4K/ErTDMQ1s9tINjv7kvzjnVhQHXJBA==", + "license": "MIT", + "dependencies": { + "fft.js": "^4.0.3", + "next-power-of-two": "^1.0.0" + } + }, + "node_modules/ml-matrix": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.15.0.tgz", + "integrity": "sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw==", + "license": "MIT", + "dependencies": { + "is-any-array": "^3.0.0", + "ml-array-rescale": "^2.0.0" + } + }, + "node_modules/ml-random": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ml-random/-/ml-random-0.5.0.tgz", + "integrity": "sha512-zLJBmNb34LOz+vN6BD8l3aYm/VWYWbmAunrLMPs4dHf4gTl8BWlhil72j56HubPg86zrXioIs4qoHq7Topy6tw==", + "license": "MIT", + "dependencies": { + "ml-xsadd": "^2.0.0" + } + }, + "node_modules/ml-ransac": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ml-ransac/-/ml-ransac-1.0.0.tgz", + "integrity": "sha512-fGi45s7T5+BEqdb5JqZC699w2riy6j0tzuZ2ZP63xavRa/yVKQIbUbj4XiE4XMr9T7HSiq1D9y0uybiyikDPvA==", + "license": "MIT", + "dependencies": { + "ml-array-median": "^1.1.6", + "ml-matrix": "^6.10.4", + "ml-random": "^0.5.0" + } + }, + "node_modules/ml-regression-base": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ml-regression-base/-/ml-regression-base-4.0.1.tgz", + "integrity": "sha512-kYJWRIfB88JDUN+QWBsOlI0dP5tMo/XPxqVU0Xjn6B0rfzPptUnON9xPNnMVEm43DkM621j9v6+Ln9vCk3N93A==", + "license": "MIT", + "dependencies": { + "cheminfo-types": "^1.15.0", + "is-any-array": "^3.0.0" + } + }, + "node_modules/ml-regression-multivariate-linear": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/ml-regression-multivariate-linear/-/ml-regression-multivariate-linear-2.0.4.tgz", + "integrity": "sha512-/vShPAlP+mB7P2mC5TuXwObSJNl/UBI71/bszt9ilTg6yLKy6btDLpAYyJNa6t+JnL5a7q+Yy4dCltfpvqXRIw==", + "license": "MIT", + "dependencies": { + "ml-matrix": "^6.10.1" + } + }, + "node_modules/ml-regression-polynomial-2d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ml-regression-polynomial-2d/-/ml-regression-polynomial-2d-1.0.0.tgz", + "integrity": "sha512-r34kaawZKMwjORIdzpa/o5fnjlTXkkaLJ3B4vvFNeKlvEGPtApqFxbDRmw07e2CjUH2MhGIChpMeXp/XB3Iogg==", + "license": "MIT", + "dependencies": { + "cheminfo-types": "^1.7.3", + "is-any-array": "^2.0.1", + "ml-matrix": "^6.11.0", + "ml-regression-base": "^4.0.0" + } + }, + "node_modules/ml-regression-polynomial-2d/node_modules/is-any-array": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz", + "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==", + "license": "MIT" + }, + "node_modules/ml-spectra-processing": { + "version": "14.35.1", + "resolved": "https://registry.npmjs.org/ml-spectra-processing/-/ml-spectra-processing-14.35.1.tgz", + "integrity": "sha512-yxTB9YpgNmo53U+c2tG1/uKKUl+CNO3hlp+3utuBlhAqDw3AD9c78gI+1rYz+sykOXV76QtAXuAhFZFZjMvgDw==", + "license": "MIT", + "dependencies": { + "binary-search": "^1.3.6", + "cheminfo-types": "^1.16.0", + "fft.js": "^4.0.4", + "is-any-array": "^3.0.0", + "ml-matrix": "^6.14.0", + "ml-xsadd": "^3.0.1" + } + }, + "node_modules/ml-spectra-processing/node_modules/ml-xsadd": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ml-xsadd/-/ml-xsadd-3.0.1.tgz", + "integrity": "sha512-Fz2q6dwgzGM8wYKGArTUTZDGa4lQFA2Vi6orjGeTVRy22ZnQFKlJuwS9n8NRviqz1KHAHAzdKJwbnYhdo38uYg==", + "license": "MIT" + }, + "node_modules/ml-xsadd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-xsadd/-/ml-xsadd-2.0.0.tgz", + "integrity": "sha512-VoAYUqmPRmzKbbqRejjqceGFp3VF81Qe8XXFGU0UXLxB7Mf4GGvyGq5Qn3k4AiQgDEV6WzobqlPOd+j0+m6IrA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/next-power-of-two": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-power-of-two/-/next-power-of-two-1.0.0.tgz", + "integrity": "sha512-+z6QY1SxkDk6CQJAeaIZKmcNubBCRP7J8DMQUBglz/sSkNsZoJ1kULjqk9skNPPplzs4i9PFhYrvNDdtQleF/A==", + "license": "MIT" + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "license": "MIT", + "optional": true + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/robust-orientation": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.2.1.tgz", + "integrity": "sha512-FuTptgKwY6iNuU15nrIJDLjXzCChWB+T4AvksRtwPS/WZ3HuP1CElCm1t+OBfgQKfWbtZIawip+61k7+buRKAg==", + "license": "MIT", + "dependencies": { + "robust-scale": "^1.0.2", + "robust-subtract": "^1.0.0", + "robust-sum": "^1.0.0", + "two-product": "^1.0.2" + } + }, + "node_modules/robust-point-in-polygon": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/robust-point-in-polygon/-/robust-point-in-polygon-1.0.3.tgz", + "integrity": "sha512-pPzz7AevOOcPYnFv4Vs5L0C7BKOq6C/TfAw5EUE58CylbjGiPyMjAnPLzzSuPZ2zftUGwWbmLWPOjPOz61tAcA==", + "license": "MIT", + "dependencies": { + "robust-orientation": "^1.0.2" + } + }, + "node_modules/robust-scale": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz", + "integrity": "sha512-jBR91a/vomMAzazwpsPTPeuTPPmWBacwA+WYGNKcRGSh6xweuQ2ZbjRZ4v792/bZOhRKXRiQH0F48AvuajY0tQ==", + "license": "MIT", + "dependencies": { + "two-product": "^1.0.2", + "two-sum": "^1.0.0" + } + }, + "node_modules/robust-subtract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz", + "integrity": "sha512-xhKUno+Rl+trmxAIVwjQMiVdpF5llxytozXJOdoT4eTIqmqsndQqFb1A0oiW3sZGlhMRhOi6pAD4MF1YYW6o/A==", + "license": "MIT" + }, + "node_modules/robust-sum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz", + "integrity": "sha512-AvLExwpaqUqD1uwLU6MwzzfRdaI6VEZsyvQ3IAQ0ZJ08v1H+DTyqskrf2ZJyh0BDduFVLN7H04Zmc+qTiahhAw==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/simple-xml-to-json": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", + "license": "MIT", + "engines": { + "node": ">=20.12.2" + } + }, + "node_modules/skia-canvas": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/skia-canvas/-/skia-canvas-3.0.8.tgz", + "integrity": "sha512-FSYKxp8Ng2vOeeOBiyPhnn6ui6FirPJXMyjk4PKl8N/OWzVrkMawUgY9zubIWHMdYtyWFn0gfX3QlRwg6HBmdg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.1.1", + "follow-redirects": "^1.15.11", + "https-proxy-agent": "^7.0.6", + "string-split-by": "^1.0.0" + } + }, + "node_modules/ssim.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz", + "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==", + "license": "MIT" + }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "license": "MIT", + "optional": true, + "dependencies": { + "parenthesis": "^3.1.5" + } + }, + "node_modules/strtok3": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.5.0.tgz", + "integrity": "sha512-PgAsgIuizbZN/r4+g4UNvbVCTpJWdoIdI71kIWwKmdIH0vJ0q7ylMVGU8PWj2s9O98y4MtFWiDTIrtNukZMMgQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tiff": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/tiff/-/tiff-7.1.3.tgz", + "integrity": "sha512-YEEq3fT++2pdta/9P/vGG4QRMdZQoe6W6JNaWnIi6NvAsbeNITwFCtmWwL/BZvOi+uo2I3ohyOkD3sZfme+c6g==", + "license": "MIT", + "dependencies": { + "fflate": "^0.8.2", + "iobuffer": "^6.0.0" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-pattern": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.9.2.tgz", + "integrity": "sha512-qAf+7M7Jjk7uebZeIo9uEzyintfuRtgHdzM9dqxRKcMi2dt76Ec+FH2wW3bnU9TuGLkZIJLoLs7cCQnU7Tu8ig==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/two-product": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz", + "integrity": "sha512-vOyrqmeYvzjToVM08iU52OFocWT6eB/I5LUWYnxeAPGXAhAxXYU/Yr/R2uY5/5n4bvJQL9AQulIuxpIsMoT8XQ==", + "license": "MIT" + }, + "node_modules/two-sum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz", + "integrity": "sha512-phP48e8AawgsNUjEY2WvoIWqdie8PoiDZGxTDv70LDr01uX5wLEQbOgSP7Z/B6+SW5oLtbe8qaYX2fKJs3CGTw==", + "license": "MIT" + }, + "node_modules/uint8-base64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uint8-base64/-/uint8-base64-1.0.0.tgz", + "integrity": "sha512-B6ZJfEZL2FAhbIyZZ7WlQq8MPq1X1P1DUA//LsSv/vB4r+Dao5/BPoxgw2t+t1XyvoSfBkE7zI4SYsnGj7BmlQ==", + "license": "MIT" + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/bench/package.json b/bench/package.json new file mode 100644 index 00000000..344d40f4 --- /dev/null +++ b/bench/package.json @@ -0,0 +1,19 @@ +{ + "name": "imagescript-bench", + "private": true, + "description": "ImageScript compared to other image libraries for Node.js", + "type": "module", + "scripts": { + "bench": "node run.js && node installs.js && node report.js" + }, + "dependencies": { + "@napi-rs/canvas": "1.0.11", + "@silvia-odwyer/photon-node": "0.3.4", + "image-js": "1.7.1", + "imagescript": "3.1.0", + "imagescript-v1": "npm:imagescript@1.3.1", + "jimp": "1.6.1", + "sharp": "0.35.5", + "tinybench": "6.2.2" + } +} diff --git a/bench/report.js b/bench/report.js new file mode 100644 index 00000000..c1772a9e --- /dev/null +++ b/bench/report.js @@ -0,0 +1,163 @@ +// Turns results.json (run.js) and installs.json (installs.js) into charts/*.svg and README.md +import fs from 'node:fs'; + +const results = JSON.parse(fs.readFileSync(new URL('./results.json', import.meta.url))); +const installs = JSON.parse(fs.readFileSync(new URL('./installs.json', import.meta.url))); +const { libraries, TEXT, TEXT_SIZE, SVG_WIDTH } = await import('./libs.js'); +const LIBS = libraries.map(l => l.name); +const KIND = Object.fromEntries(libraries.map(l => [l.name, l.kind])); +const HIGHLIGHT = 'ImageScript 3'; +fs.mkdirSync(new URL('./charts/', import.meta.url), { recursive: true }); + +// light and dark values of every color the charts use: one accent for ImageScript, neutral gray for the others +const STYLE = ` + .surface { fill: #fcfcfb } .text { fill: #0b0b0b } .muted { fill: #52514e } .grid { stroke: #e4e3df } + .bar { fill: #8a8983 } .bar.highlight { fill: #2a78d6 } + @media (prefers-color-scheme: dark) { + .surface { fill: #1a1a19 } .text { fill: #f0efec } .muted { fill: #c3c2b7 } .grid { stroke: #383835 } + .bar { fill: #6d6c66 } .bar.highlight { fill: #3987e5 } + } + text { font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif } + .title { font-weight: 600; font-size: 14px } .bold { font-weight: 600 } .small { font-size: 11px }`; + +const esc = s => String(s).replace(/&/g, '&').replace(/ v >= 100 ? Math.round(v).toLocaleString('en') : v >= 10 ? v.toFixed(1) : v.toFixed(2); +const bytes = b => b >= 1e6 ? `${(b / 1e6).toFixed(1)} MB` : `${Math.round(b / 1e3)} kB`; + +// a bar whose data end (right) is rounded by 4px and whose baseline end (left) is square +const bar = (x, y, w, h, cls) => w <= 0 ? '' : w <= 4 + ? `` + : ``; + +/** + * Groups of horizontal bars, one per measurement, each on its own scale (they range from a few to 100,000 per + * second); libraries keep their row in every group, so they're easy to follow + * @param groups [{ title, values: { library: number }, format }] + */ +function barChart(title, subtitle, groups, file) { + const W = 760, labelW = 190, barW = 440, rowH = 22, barH = 14, groupGap = 18, pad = 20; + let y = pad + 58, body = ''; + for (const group of groups) { + const max = Math.max(...Object.values(group.values).filter(v => typeof v === 'number')); + body += `${esc(group.title)}`; + if (group.note) body += `${esc(group.note)}`; + y += 10; + body += ``; + for (const lib of LIBS) { + const v = group.values[lib], hl = lib === HIGHLIGHT; + const cy = y + rowH / 2; + body += `${esc(lib)}`; + if (typeof v !== 'number') body += `${esc(v ?? 'not supported')}`; + else { + const w = Math.max(2, v / max * barW); + body += bar(pad + labelW + 1, cy - barH / 2, w, barH, hl ? 'bar highlight' : 'bar'); + body += `${esc(group.format(v))}`; + } + y += rowH; + } + y += groupGap; + } + const H = y + pad - groupGap + 10; + const svg = ` +${esc(title)} + +${esc(title)} +${esc(subtitle)} +${body} + +`; + fs.writeFileSync(new URL(`./charts/${file}`, import.meta.url), svg); +} + +const SPEED = [ + ['decode JPEG', 'decode JPEG'], ['decode PNG', 'decode PNG'], ['decode WebP', 'decode WebP (lossy)'], + ['encode JPEG', 'encode JPEG (quality 80)'], ['encode PNG', 'encode PNG'], ['encode WebP', 'encode WebP (lossless)'], + ['resize', 'resize to half'], ['rotate', 'rotate by 30°'], ['blur', 'blur (radius 5)'], + ['composite', 'composite an overlay'], ['crop', 'crop'], ['clone', 'clone'], + ['text', 'render text'], ['svg', 'render SVG'], + ['decode GIF', 'decode animated GIF'], ['encode GIF', 'encode animated GIF'] +].filter(([key]) => results.speed[key]); +const ops = key => Object.fromEntries(Object.entries(results.speed[key]).map(([lib, r]) => [lib, r.error ? 'fails on this input' : r.opsPerSecond])); + +const cpu = results.cpu.replace(/\((R|TM)\)|\bCPU\b|@.*$/g, '').replace(/\s+/g, ' ').trim(); +const NOTES = { resize: 'ImageScript 1: nearest neighbor only, others bilinear' }; +const split = (keys, file, title, input = 'on a 1920×1080 photo') => barChart(title, `operations per second ${input}, more is faster · Node.js ${results.node.match(/\d+/)[0]}, ${cpu}`, + SPEED.filter(([key]) => keys.includes(key)).map(([key, label]) => ({ title: label, note: NOTES[key], values: ops(key), format: number })), file); +split(['decode JPEG', 'decode PNG', 'decode WebP', 'encode JPEG', 'encode PNG', 'encode WebP'], 'codecs.svg', 'Decoding and encoding'); +split(['resize', 'rotate', 'blur', 'composite', 'crop', 'clone'], 'operations.svg', 'Image operations'); +split(['text', 'svg'], 'rendering.svg', 'Rendering', `(text at ${TEXT_SIZE}px, SVG ${SVG_WIDTH} wide)`); +split(['decode GIF', 'encode GIF'], 'animation.svg', 'Animations', 'on a 300×300 GIF with 101 frames'); + +barChart('Install size', 'node_modules after npm install in an empty project (all dependencies and native binaries), less is better', + [{ title: 'install size', values: Object.fromEntries(LIBS.map(l => [l, installs[l].bytes])), format: bytes }], 'install.svg'); +barChart('File sizes', 'encoded with each library\'s default settings, less is better', + Object.keys(results.sizes).map(key => ({ title: key.replace('JPEG', 'JPEG (quality 80)').replace('WebP', 'WebP (lossless)'), values: results.sizes[key], format: bytes })), 'sizes.svg'); + +// tables: the same numbers, to read exactly +const table = (rows, columns, cell) => [ + `| | ${columns.map(c => `**${c}**`).join(' | ')} |`, + `|---|${columns.map(() => '--:').join('|')}|`, + ...rows.map(([key, label]) => `| ${label} | ${columns.map(c => cell(key, c)).join(' | ')} |`) +].join('\n'); +const speedCell = (key, lib) => { const r = results.speed[key][lib]; return r?.error ? 'fails' : r ? (lib === HIGHLIGHT ? `**${number(r.opsPerSecond)}**` : number(r.opsPerSecond)) : '–'; }; + +const readme = `# ImageScript benchmark + +How [ImageScript](https://github.com/matmen/ImageScript) compares to other image libraries for Node.js: speed, file +sizes and what installing each takes. ImageScript is plain JavaScript with no dependencies, so it runs wherever +JavaScript does; sharp and @napi-rs/canvas wrap native libraries (libvips, Skia), Photon and ImageScript 1 are +WebAssembly. + +Install size of each library + +Decoding and encoding speed + +Speed of image operations + +Text and SVG rendering speed + +Animated GIF decoding and encoding speed + +Encoded file sizes + +## Numbers + +Operations per second (more is faster), measured ${results.date} with Node.js ${results.node} on ${results.cpu}, ImageScript ${results.versions['ImageScript 3']}. + +${table(SPEED, LIBS, speedCell)} + +Encoded sizes: + +${table(Object.keys(results.sizes).map(k => [k, k]), LIBS, (key, lib) => results.sizes[key][lib] ? bytes(results.sizes[key][lib]) : '–')} + +| | ${LIBS.map(l => `**${l}**`).join(' | ')} | +|---|${LIBS.map(() => '---').join('|')}| +| kind | ${LIBS.map(l => KIND[l]).join(' | ')} | +| package | ${LIBS.map(l => `\`${installs[l].package}\``).join(' | ')} | +| install size | ${LIBS.map(l => bytes(installs[l].bytes)).join(' | ')} | +| packages | ${LIBS.map(l => installs[l].packages).join(' | ')} | + +## Method + +- Inputs (in \`inputs/\`, sources in [inputs/SOURCES.md](inputs/SOURCES.md)): a 1920×1080 photo (JPEG, and made from it + when the benchmark starts, PNG and lossy WebP), a 1434×1434 graphic with transparency (PNG), a 445×445 diagram with + transparency placed on the photo (PNG), a 300×300 GIF with 101 frames. +- Rendering: "${TEXT}" in Source Sans 3 at ${TEXT_SIZE}px, and \`tests/svgs/features.svg\` (gradients, clipping, + CSS) at ${SVG_WIDTH} pixels wide. Jimp and Photon can't render TrueType/OpenType fonts or SVGs; ImageScript 1 crashes + on this SVG; @napi-rs/canvas (Skia's SVG module) ignores its CSS, so it draws a little less. +- Every operation starts from the same decoded image and returns a new one: libraries that change images in place + clone first, sharp runs a pipeline from raw pixels back to raw pixels, @napi-rs/canvas draws into a new canvas. +- Where the library lets you choose, resizing is bilinear (ImageScript 1 only has nearest neighbor), blurring is + gaussian with a radius or sigma of 5, rotating grows the image to fit. Each library uses its own defaults + otherwise, e.g. its PNG compression level. +- [tinybench](https://github.com/tinylibs/tinybench), at least 1.5 seconds and 3 runs per measurement, every library + in its own process. +- The release workflow runs the benchmark for every release and commits the results, on a GitHub Actions runner; + runners vary, so compare libraries within one run rather than between runs. +- Install sizes: \`npm install \` in an empty project, the apparent size of \`node_modules\`. + +Run it: \`npm install\` in this directory, then \`node run.js\` (on an idle machine; \`IMAGESCRIPT=local\` measures +this checkout after \`npm run build\` instead of the version from npm), \`node installs.js\` and \`node report.js\`. +`; +fs.writeFileSync(new URL('./README.md', import.meta.url), readme); +console.log('charts/ and README.md written'); diff --git a/bench/results.json b/bench/results.json new file mode 100644 index 00000000..853d7bc8 --- /dev/null +++ b/bench/results.json @@ -0,0 +1,466 @@ +{ + "date": "2026-10-15", + "node": "v24.11.1", + "cpu": "Intel(R) Core(TM) i3-8109U CPU @ 3.00GHz", + "versions": { + "ImageScript 3": "3.1.0", + "ImageScript 1": "1.3.1", + "Jimp": "1.6.1", + "image-js": "1.7.1", + "Photon": "0.3.4", + "sharp": "0.35.5", + "@napi-rs/canvas": "1.0.11" + }, + "speed": { + "decode JPEG": { + "ImageScript 3": { + "opsPerSecond": 9.06, + "ms": 110.38 + }, + "ImageScript 1": { + "opsPerSecond": 3.97, + "ms": 252.04 + }, + "Jimp": { + "opsPerSecond": 4.34, + "ms": 230.41 + }, + "image-js": { + "opsPerSecond": 5.17, + "ms": 193.45 + }, + "Photon": { + "opsPerSecond": 16.11, + "ms": 62.08 + }, + "sharp": { + "opsPerSecond": 37.29, + "ms": 26.82 + }, + "@napi-rs/canvas": { + "opsPerSecond": 42.23, + "ms": 23.68 + } + }, + "decode PNG": { + "ImageScript 3": { + "opsPerSecond": 20.45, + "ms": 48.9 + }, + "ImageScript 1": { + "opsPerSecond": 20.6, + "ms": 48.54 + }, + "Jimp": { + "opsPerSecond": 12.21, + "ms": 81.9 + }, + "image-js": { + "opsPerSecond": 7.46, + "ms": 134.05 + }, + "Photon": { + "opsPerSecond": 22.01, + "ms": 45.42 + }, + "sharp": { + "opsPerSecond": 43.73, + "ms": 22.87 + }, + "@napi-rs/canvas": { + "opsPerSecond": 48.07, + "ms": 20.8 + } + }, + "decode WebP": { + "ImageScript 3": { + "opsPerSecond": 7.73, + "ms": 129.39 + }, + "Photon": { + "opsPerSecond": 7.75, + "ms": 128.99 + }, + "sharp": { + "opsPerSecond": 30.31, + "ms": 32.99 + }, + "@napi-rs/canvas": { + "opsPerSecond": 53.27, + "ms": 18.77 + } + }, + "encode PNG": { + "ImageScript 3": { + "opsPerSecond": 6.67, + "ms": 150.02 + }, + "ImageScript 1": { + "opsPerSecond": 3.17, + "ms": 315.87 + }, + "Jimp": { + "opsPerSecond": 3.33, + "ms": 300.18 + }, + "image-js": { + "opsPerSecond": 2.37, + "ms": 422.83 + }, + "Photon": { + "opsPerSecond": 7.57, + "ms": 132.14 + }, + "sharp": { + "opsPerSecond": 6.74, + "ms": 148.36 + }, + "@napi-rs/canvas": { + "opsPerSecond": 2, + "ms": 500.78 + } + }, + "encode JPEG": { + "ImageScript 3": { + "opsPerSecond": 9.51, + "ms": 105.17 + }, + "ImageScript 1": { + "opsPerSecond": 14.58, + "ms": 68.58 + }, + "Jimp": { + "opsPerSecond": 1.86, + "ms": 538.83 + }, + "image-js": { + "opsPerSecond": 6.76, + "ms": 147.9 + }, + "Photon": { + "opsPerSecond": 6.12, + "ms": 163.4 + }, + "sharp": { + "opsPerSecond": 49.03, + "ms": 20.4 + }, + "@napi-rs/canvas": { + "opsPerSecond": 30.79, + "ms": 32.48 + } + }, + "encode WebP": { + "ImageScript 3": { + "opsPerSecond": 0.67, + "ms": 1490.36 + }, + "Photon": { + "opsPerSecond": 17.04, + "ms": 58.68 + }, + "sharp": { + "opsPerSecond": 0.52, + "ms": 1909.27 + }, + "@napi-rs/canvas": { + "opsPerSecond": 1.85, + "ms": 541.42 + } + }, + "resize": { + "ImageScript 3": { + "opsPerSecond": 14.92, + "ms": 67.03 + }, + "ImageScript 1": { + "opsPerSecond": 248.62, + "ms": 4.02 + }, + "Jimp": { + "opsPerSecond": 42.49, + "ms": 23.53 + }, + "image-js": { + "opsPerSecond": 7.09, + "ms": 141.07 + }, + "Photon": { + "opsPerSecond": 9.57, + "ms": 104.55 + }, + "sharp": { + "opsPerSecond": 50.61, + "ms": 19.76 + }, + "@napi-rs/canvas": { + "opsPerSecond": 364.44, + "ms": 2.74 + } + }, + "rotate": { + "ImageScript 3": { + "opsPerSecond": 6.39, + "ms": 156.57 + }, + "ImageScript 1": { + "opsPerSecond": 4.22, + "ms": 236.81 + }, + "Jimp": { + "opsPerSecond": 1.2, + "ms": 832.34 + }, + "image-js": { + "opsPerSecond": 0.85, + "ms": 1174.66 + }, + "Photon": { + "opsPerSecond": 2.64, + "ms": 379.32 + }, + "sharp": { + "opsPerSecond": 10.61, + "ms": 94.27 + }, + "@napi-rs/canvas": { + "opsPerSecond": 31.68, + "ms": 31.56 + } + }, + "blur": { + "ImageScript 3": { + "opsPerSecond": 4.32, + "ms": 231.47 + }, + "Jimp": { + "opsPerSecond": 0.01, + "ms": 98763.17 + }, + "image-js": { + "opsPerSecond": 1, + "ms": 996.34 + }, + "Photon": { + "opsPerSecond": 2.54, + "ms": 393.29 + }, + "sharp": { + "opsPerSecond": 23.73, + "ms": 42.15 + }, + "@napi-rs/canvas": { + "opsPerSecond": 33.53, + "ms": 29.82 + } + }, + "composite": { + "ImageScript 3": { + "opsPerSecond": 271.87, + "ms": 3.68 + }, + "ImageScript 1": { + "opsPerSecond": 408.29, + "ms": 2.45 + }, + "Jimp": { + "opsPerSecond": 22.35, + "ms": 44.75 + }, + "image-js": { + "opsPerSecond": 103.39, + "ms": 9.67 + }, + "Photon": { + "opsPerSecond": 38.27, + "ms": 26.13 + }, + "sharp": { + "opsPerSecond": 95.25, + "ms": 10.5 + }, + "@napi-rs/canvas": { + "opsPerSecond": 235.68, + "ms": 4.24 + } + }, + "crop": { + "ImageScript 3": { + "opsPerSecond": 313.14, + "ms": 3.19 + }, + "ImageScript 1": { + "opsPerSecond": 428.49, + "ms": 2.33 + }, + "Jimp": { + "opsPerSecond": 34.42, + "ms": 29.06 + }, + "image-js": { + "opsPerSecond": 46.28, + "ms": 21.61 + }, + "Photon": { + "opsPerSecond": 41.13, + "ms": 24.31 + }, + "sharp": { + "opsPerSecond": 927.53, + "ms": 1.08 + }, + "@napi-rs/canvas": { + "opsPerSecond": 933.99, + "ms": 1.07 + } + }, + "clone": { + "ImageScript 3": { + "opsPerSecond": 596.33, + "ms": 1.68 + }, + "ImageScript 1": { + "opsPerSecond": 575.44, + "ms": 1.74 + }, + "Jimp": { + "opsPerSecond": 700.45, + "ms": 1.43 + }, + "image-js": { + "opsPerSecond": 563.46, + "ms": 1.77 + }, + "Photon": { + "opsPerSecond": 42.24, + "ms": 23.67 + }, + "sharp": { + "opsPerSecond": 711.82, + "ms": 1.4 + }, + "@napi-rs/canvas": { + "opsPerSecond": 273.14, + "ms": 3.66 + } + }, + "text": { + "ImageScript 3": { + "opsPerSecond": 774.4, + "ms": 1.29 + }, + "ImageScript 1": { + "opsPerSecond": 50.08, + "ms": 19.97 + }, + "sharp": { + "opsPerSecond": 1896.45, + "ms": 0.53 + }, + "@napi-rs/canvas": { + "opsPerSecond": 3163.82, + "ms": 0.32 + } + }, + "svg": { + "ImageScript 3": { + "opsPerSecond": 3.86, + "ms": 259.27 + }, + "ImageScript 1": { + "error": "unreachable" + }, + "sharp": { + "opsPerSecond": 12.01, + "ms": 83.26 + }, + "@napi-rs/canvas": { + "opsPerSecond": 51.21, + "ms": 19.53 + } + }, + "decode GIF": { + "ImageScript 3": { + "opsPerSecond": 4.95, + "ms": 202.2 + }, + "ImageScript 1": { + "opsPerSecond": 6.77, + "ms": 147.66 + }, + "sharp": { + "opsPerSecond": 12.52, + "ms": 79.85 + } + }, + "encode GIF": { + "ImageScript 3": { + "opsPerSecond": 3.39, + "ms": 294.83 + }, + "ImageScript 1": { + "opsPerSecond": 1.01, + "ms": 991.94 + }, + "sharp": { + "opsPerSecond": 0.8, + "ms": 1252 + } + } + }, + "sizes": { + "photo PNG": { + "ImageScript 3": 2182990, + "ImageScript 1": 2958715, + "Jimp": 2743139, + "image-js": 2824400, + "Photon": 2846778, + "sharp": 2765606, + "@napi-rs/canvas": 2096793 + }, + "photo JPEG": { + "ImageScript 3": 217174, + "ImageScript 1": 257126, + "Jimp": 260008, + "image-js": 260008, + "Photon": 257126, + "sharp": 217967, + "@napi-rs/canvas": 218459 + }, + "photo WebP": { + "ImageScript 3": 1408976, + "Photon": 1704616, + "sharp": 1205214, + "@napi-rs/canvas": 1436904 + }, + "graphic PNG": { + "ImageScript 3": 1069976, + "ImageScript 1": 801938, + "Jimp": 1993269, + "image-js": 735716, + "Photon": 2214901, + "sharp": 717555, + "@napi-rs/canvas": 1031969 + }, + "graphic JPEG": { + "ImageScript 3": 274899, + "ImageScript 1": 314341, + "Jimp": 314923, + "image-js": 314923, + "Photon": 314341, + "sharp": 275293, + "@napi-rs/canvas": 275785 + }, + "graphic WebP": { + "ImageScript 3": 579344, + "Photon": 855794, + "sharp": 238504, + "@napi-rs/canvas": 630580 + }, + "animation GIF": { + "ImageScript 3": 1330686, + "ImageScript 1": 1274780, + "sharp": 720193 + } + } +} diff --git a/bench/run.js b/bench/run.js new file mode 100644 index 00000000..29e78120 --- /dev/null +++ b/bench/run.js @@ -0,0 +1,37 @@ +// Runs bench.js for every library (or the ones named: node run.js sharp "ImageScript 3"), each in its own process, +// and writes results.json. Libraries that aren't run keep their results from the last run. Run on an idle machine +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import { libraries } from './libs.js'; + +const file = new URL('./results.json', import.meta.url); +const previous = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file)) : { speed: {}, sizes: {} }; +const names = process.argv.length > 2 ? process.argv.slice(2) : libraries.map(l => l.name); + +// os.cpus() reports "unknown" inside some containers, /proc/cpuinfo still has the model +const cpu = os.cpus()[0]?.model; +const results = { + date: new Date().toISOString().slice(0, 10), + node: process.version, + cpu: cpu && cpu !== 'unknown' ? cpu : fs.readFileSync('/proc/cpuinfo', 'utf8').match(/model name\s*:\s*(.+)/)?.[1] ?? 'unknown', + versions: { ...previous.versions }, + speed: previous.speed, + sizes: previous.sizes +}; + +for (const name of names) { + const lib = libraries.find(l => l.name === name); + if (!lib) throw new Error(`unknown library ${name}`); + const output = execFileSync(process.execPath, ['--expose-gc', new URL('./bench.js', import.meta.url).pathname, name], + { stdio: ['ignore', 'pipe', 'inherit'], maxBuffer: 1 << 24 }); + const { speed, sizes } = JSON.parse(output); + for (const table of [results.speed, results.sizes]) for (const task in table) delete table[task][name]; + for (const [task, r] of Object.entries(speed)) (results.speed[task] ??= {})[name] = r; + for (const [task, size] of Object.entries(sizes)) (results.sizes[task] ??= {})[name] = size; + results.versions[name] = process.env.IMAGESCRIPT === 'local' && lib.package === 'imagescript' + ? 'local' : JSON.parse(fs.readFileSync(new URL(`./node_modules/${lib.package}/package.json`, import.meta.url))).version; +} + +fs.writeFileSync(file, JSON.stringify(results, null, 2) + '\n'); +console.log('results.json written');