diff --git a/README.md b/README.md
index 035c488..d341efe 100644
--- a/README.md
+++ b/README.md
@@ -563,7 +563,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
`move` moves a category to a new parent. Both `id` (source) and `parentId` (target) are validated via `resolveCategoryAccess` — the target must be a real category of the same authorized company, else `Parent not found`. Moving into itself or into a descendant (cycle) → `Invalid parent`. The moved category gets the next sibling `sort` under the new parent. Returns `{ id, parentId, sort }`. For `POST /api/category/:id/move`.
-`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `sharp`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `categorypic`. Auth via `resolveCategoryAccess`. Errors: `Category not found`, `Not authorized`.
+`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `wasm-image-optimization`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `categorypic`. Auth via `resolveCategoryAccess`. Errors: `Category not found`, `Not authorized`.
`addImage` returns:
@@ -601,7 +601,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
| `orderImages` | `(googleAuthId: string, sku: string, { fullHashes: string[] })` | `{ fullHashes: string[] }` |
| `delete` | `(googleAuthId: string, sku: string)` | `void` |
-`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `sharp`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
+`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `wasm-image-optimization`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
`addImage` returns `ImageResult`:
diff --git a/package-lock.json b/package-lock.json
index 5ad8ff6..3ab412a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,9 +14,9 @@
"chalk": "^6.0.1",
"dotenv": "^18.0.8",
"pdfconvert-wasm": "^0.3.0",
- "sharp": "^0.35.5",
"skelehash-wasm": "^0.1.1",
- "thumbhash": "^0.1.1"
+ "thumbhash": "^0.1.1",
+ "wasm-image-optimization": "^2.0.13"
},
"devDependencies": {
"better-sqlite3": "^13.0.3",
@@ -339,570 +339,12 @@
"node": ">= 20.12.0"
}
},
- "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/@hpcc-js/wasm-graphviz": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.30.0.tgz",
"integrity": "sha512-T20IKosOZuex2qV049fQokVJy2MWv8w9Md5V/4mRKCXDf49fBCR50VD1yBlPYHaQlHPsFGRdqXN//r59vZliWQ==",
"license": "Apache-2.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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "musl"
- ],
- "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"
- ],
- "libc": [
- "musl"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "glibc"
- ],
- "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"
- ],
- "libc": [
- "musl"
- ],
- "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"
- ],
- "libc": [
- "musl"
- ],
- "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/@js-joda/core": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.3.0.tgz",
@@ -1153,15 +595,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "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/dotenv": {
"version": "18.0.9",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz",
@@ -1626,6 +1059,7 @@
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -1634,55 +1068,6 @@
"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/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -1746,7 +1131,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "devOptional": true,
+ "dev": true,
"license": "0BSD"
},
"node_modules/undici-types": {
@@ -1756,6 +1141,34 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/wasm-image-optimization": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/wasm-image-optimization/-/wasm-image-optimization-2.0.13.tgz",
+ "integrity": "sha512-MGaFZtdxMHZyaL3VUUcNkN8qccJcy33lcXoUIaegav/y1TiK6c2aALbKAjoe4bXHFI7nDl+5+TG0e9Y8j1/h9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "15.0.0",
+ "worker-lib": "2.2.1"
+ },
+ "bin": {
+ "wasm-image-optimization": "dist/cli.js"
+ }
+ },
+ "node_modules/wasm-image-optimization/node_modules/commander": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
+ "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/worker-lib": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/worker-lib/-/worker-lib-2.2.1.tgz",
+ "integrity": "sha512-CauU4vz3vZUBG3A2CY93GY94KLhbH8JtPKtuvKu2w5G07cHkZ73QkTAJ/lEHDL6arT5WDu1qpU++T0+kGDgSCg==",
+ "license": "MIT"
+ },
"node_modules/wsl-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz",
diff --git a/package.json b/package.json
index 8b02c7c..955ed73 100644
--- a/package.json
+++ b/package.json
@@ -30,9 +30,9 @@
"chalk": "^6.0.1",
"dotenv": "^18.0.8",
"pdfconvert-wasm": "^0.3.0",
- "sharp": "^0.35.5",
"skelehash-wasm": "^0.1.1",
- "thumbhash": "^0.1.1"
+ "thumbhash": "^0.1.1",
+ "wasm-image-optimization": "^2.0.13"
},
"peerDependencies": {
"better-sqlite3": "^12.0.0",
diff --git a/scripts/benchmark/workload.js b/scripts/benchmark/workload.js
index 202c219..e1f8ffb 100644
--- a/scripts/benchmark/workload.js
+++ b/scripts/benchmark/workload.js
@@ -1,35 +1,21 @@
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
/**
* Creates sample PNG image buffer.
*/
async function makeSampleImage(width = 200, height = 200, color = { r: 50, g: 120, b: 220 }) {
- return sharp({
- create: {
- width,
- height,
- channels: 4,
- background: { ...color, alpha: 1 },
- },
- })
- .png()
- .toBuffer();
+ const svg = ``;
+ const res = await optimizeImage({ image: svg, format: 'png' });
+ return Buffer.from(res.data);
}
/**
* Creates sample AVIF thumbnail buffer.
*/
async function makeSampleThumb(width = 64, height = 64) {
- return sharp({
- create: {
- width,
- height,
- channels: 4,
- background: { r: 200, g: 60, b: 60, alpha: 1 },
- },
- })
- .avif()
- .toBuffer();
+ const svg = ``;
+ const res = await optimizeImage({ image: svg, format: 'avif' });
+ return Buffer.from(res.data);
}
/**
diff --git a/scripts/test-attributevalue-pic.js b/scripts/test-attributevalue-pic.js
index dea30f3..1f03d89 100644
--- a/scripts/test-attributevalue-pic.js
+++ b/scripts/test-attributevalue-pic.js
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
@@ -42,16 +42,9 @@ async function run() {
assert.ok(val.id > 0);
// Create a 100x100 test image
- const imageBuffer = await sharp({
- create: {
- width: 100,
- height: 100,
- channels: 4,
- background: { r: 25, g: 25, b: 112, alpha: 1 },
- },
- })
- .avif()
- .toBuffer();
+ const svg = ``;
+ const encoded = await optimizeImage({ image: svg, format: 'avif' });
+ const imageBuffer = Buffer.from(encoded.data);
console.log('1. Adding image to attribute value...');
const addResult = await store.attributevalue.addImage(googleAuthId, val.id, imageBuffer);
diff --git a/scripts/test-sqlite-storage.js b/scripts/test-sqlite-storage.js
index 408f581..d86f272 100644
--- a/scripts/test-sqlite-storage.js
+++ b/scripts/test-sqlite-storage.js
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
@@ -72,16 +72,9 @@ async function runTests() {
const manifestId = delivery.id;
const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.');
- const testThumb = await sharp({
- create: {
- width: 64,
- height: 64,
- channels: 4,
- background: { r: 255, g: 0, b: 0, alpha: 1 },
- },
- })
- .avif()
- .toBuffer();
+ const thumbSvg = ``;
+ const testThumbRes = await optimizeImage({ image: thumbSvg, format: 'avif' });
+ const testThumb = Buffer.from(testThumbRes.data);
const attachment = await store.manifestattachment.add(googleAuthId, manifestId, {
name: 'test-doc.txt',
@@ -202,16 +195,9 @@ async function runTests() {
console.log('Article track_stock tests passed!');
- const sampleImage = await sharp({
- create: {
- width: 300,
- height: 200,
- channels: 4,
- background: { r: 0, g: 128, b: 255, alpha: 1 },
- },
- })
- .png()
- .toBuffer();
+ const sampleSvg = ``;
+ const sampleImageRes = await optimizeImage({ image: sampleSvg, format: 'png' });
+ const sampleImage = Buffer.from(sampleImageRes.data);
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
const fullHash = picResult.fullHash;
@@ -223,10 +209,10 @@ async function runTests() {
assert.equal(smallThumbRow.smallthumbw, 32);
assert.equal(smallThumbRow.smallthumbh, 21);
assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row');
- const smallThumbMetadata = await sharp(smallThumbRow.smallthumbbinary).metadata();
- assert.equal(smallThumbMetadata.format, 'heif');
- assert.equal(smallThumbMetadata.width, 32);
- assert.equal(smallThumbMetadata.height, 21);
+ const smallThumbMetadata = await optimizeImage({ image: smallThumbRow.smallthumbbinary, format: 'none' });
+ assert.equal(smallThumbMetadata.originalFormat, 'avif');
+ assert.equal(smallThumbMetadata.originalWidth, 32);
+ assert.equal(smallThumbMetadata.originalHeight, 21);
const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' });
await store.brand.addImage(googleAuthId, brand.id, sampleImage);
diff --git a/src/store/article/deleteImage.js b/src/store/article/deleteImage.js
index 348b26a..91986b1 100644
--- a/src/store/article/deleteImage.js
+++ b/src/store/article/deleteImage.js
@@ -1,4 +1,4 @@
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import { encodeRGBA } from 'skelehash-wasm';
import { DELETE_BY_ARTICLE_AND_HASH as MSSQL_QUERY } from '../../providers/mssql/queries/pic.js';
import { DELETE_BY_ARTICLE_AND_HASH as SQLITE_QUERY } from '../../providers/sqlite3/queries/pic.js';
@@ -49,8 +49,8 @@ export default function deleteImage(provider) {
let skelehash = null;
if (thumbBuffer) {
- const { data, info } = await sharp(thumbBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
- const res = await encodeRGBA(info.width, info.height, data);
+ const raw = await optimizeImage({ image: thumbBuffer, format: 'raw' });
+ const res = await encodeRGBA(raw.width, raw.height, raw.data);
skelehash = res?.base62;
}
diff --git a/src/store/category/generateImage.js b/src/store/category/generateImage.js
index caff921..bafdb30 100644
--- a/src/store/category/generateImage.js
+++ b/src/store/category/generateImage.js
@@ -9,7 +9,7 @@ import {
import resolveCategoryAccess from './resolveCategoryAccess.js';
import addImage from './addImage.js';
import makeRunner from '../runner.js';
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
export default function generateImage(provider) {
const access = resolveCategoryAccess(provider);
@@ -148,32 +148,32 @@ export default function generateImage(provider) {
const TOTAL_SIZE = 1200;
const tileSize = TOTAL_SIZE / targetSize;
- const composites = await Promise.all(
+ const tiles = await Promise.all(
finalPics.map(async (pic, idx) => {
const row = Math.floor(idx / targetSize);
const col = idx % targetSize;
- const tileBuffer = await sharp(pic.buffer)
- .resize(tileSize, tileSize, { fit: 'cover', position: 'center' })
- .toBuffer();
- return {
- input: tileBuffer,
- left: col * tileSize,
- top: row * tileSize,
- };
+ const tile = await optimizeImage({
+ image: pic.buffer,
+ width: tileSize,
+ height: tileSize,
+ fit: 'cover',
+ format: 'png',
+ });
+ const b64 = Buffer.from(tile.data).toString('base64');
+ return ``;
}),
);
- const mosaicBuffer = await sharp({
- create: {
- width: TOTAL_SIZE,
- height: TOTAL_SIZE,
- channels: 4,
- background: { r: 255, g: 255, b: 255, alpha: 1 },
- },
- })
- .composite(composites)
- .png()
- .toBuffer();
+ const mosaicSvg =
+ ``;
+ const mosaicRes = await optimizeImage({
+ image: mosaicSvg,
+ format: 'png',
+ });
+ const mosaicBuffer = Buffer.from(mosaicRes.data);
// Clear existing categorypic rows so the new mosaic becomes the main (first) image
await runner.query('DELETE FROM categorypic WHERE category_id = @categoryId', { categoryId });
diff --git a/src/store/manifestattachment/add.js b/src/store/manifestattachment/add.js
index 9bf551f..a9b3773 100644
--- a/src/store/manifestattachment/add.js
+++ b/src/store/manifestattachment/add.js
@@ -1,5 +1,5 @@
import { createHash } from 'node:crypto';
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import generateDocumentThumb from '../../util/generateDocumentThumb.js';
import * as mssqlQueries from '../../providers/mssql/queries/manifestattachment.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/manifestattachment.js';
@@ -118,9 +118,9 @@ export default function add(provider) {
if (thumbavif && (!thumbx || !thumby)) {
try {
- const meta = await sharp(thumbavif).metadata();
- thumbx = thumbx ?? meta.width ?? null;
- thumby = thumby ?? meta.height ?? null;
+ const meta = await optimizeImage({ image: thumbavif, format: 'none' });
+ thumbx = thumbx ?? meta.originalWidth ?? null;
+ thumby = thumby ?? meta.originalHeight ?? null;
} catch {
// ignore
}
diff --git a/src/util/generateDocumentThumb.js b/src/util/generateDocumentThumb.js
index 6ca1c51..8372c77 100644
--- a/src/util/generateDocumentThumb.js
+++ b/src/util/generateDocumentThumb.js
@@ -1,4 +1,4 @@
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import { pdfToPng } from 'pdfconvert-wasm';
const THUMB_MAX = 200;
@@ -97,18 +97,27 @@ export default async function generateDocumentThumb(content, mimetype, options =
}
try {
- const thumbResult = await sharp(imageBuffer)
- .resize(THUMB_MAX, THUMB_MAX, { fit: 'inside', withoutEnlargement: true })
- .avif({ effort: 4 })
- .toBuffer({ resolveWithObject: true });
+ const meta = await optimizeImage({ image: imageBuffer, format: 'none' });
+ const srcW = meta.originalWidth || 0;
+ const srcH = meta.originalHeight || 0;
+ const needsResize = srcW > THUMB_MAX || srcH > THUMB_MAX;
+ const scale = needsResize ? Math.min(THUMB_MAX / srcW, THUMB_MAX / srcH) : 1;
+ const thumbResult = await optimizeImage({
+ image: imageBuffer,
+ width: needsResize ? Math.max(1, Math.round(srcW * scale)) : undefined,
+ height: needsResize ? Math.max(1, Math.round(srcH * scale)) : undefined,
+ fit: 'fill',
+ format: 'avif',
+ speed: 6,
+ });
return {
- thumbavif: thumbResult.data,
- thumbx: thumbResult.info.width,
- thumby: thumbResult.info.height,
+ thumbavif: Buffer.from(thumbResult.data),
+ thumbx: thumbResult.width,
+ thumby: thumbResult.height,
};
} catch (err) {
- console.warn('generateDocumentThumb: sharp thumbnail generation failed:', err.message);
+ console.warn('generateDocumentThumb: thumbnail generation failed:', err.message);
return { thumbavif: null, thumbx: null, thumby: null };
}
}
diff --git a/src/util/processAvifImage.js b/src/util/processAvifImage.js
index 7d13909..a921849 100644
--- a/src/util/processAvifImage.js
+++ b/src/util/processAvifImage.js
@@ -1,4 +1,4 @@
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import { rgbaToThumbHash } from 'thumbhash';
import { encodeRGBA } from 'skelehash-wasm';
@@ -6,63 +6,105 @@ const FULL_MAX = 1000;
const THUMB_MAX = 200;
const SMALL_THUMB_MAX = 32;
+function fitInsideDims(srcW, srcH, maxW, maxH) {
+ if (!srcW || !srcH || (srcW <= maxW && srcH <= maxH)) {
+ return {};
+ }
+ const scale = Math.min(maxW / srcW, maxH / srcH);
+ return {
+ width: Math.max(1, Math.round(srcW * scale)),
+ height: Math.max(1, Math.round(srcH * scale)),
+ fit: 'fill',
+ };
+}
+
+function unpremultiplyRgba(data) {
+ const out = new Uint8Array(data.length);
+ for (let i = 0; i < data.length; i += 4) {
+ const a = data[i + 3];
+ if (a === 255 || a === 0) {
+ out[i] = data[i];
+ out[i + 1] = data[i + 1];
+ out[i + 2] = data[i + 2];
+ out[i + 3] = a;
+ } else {
+ const f = 255 / a;
+ out[i] = Math.min(255, Math.round(data[i] * f));
+ out[i + 1] = Math.min(255, Math.round(data[i + 1] * f));
+ out[i + 2] = Math.min(255, Math.round(data[i + 2] * f));
+ out[i + 3] = a;
+ }
+ }
+ return out;
+}
+
export default async function processAvifImage(inputBuffer) {
const input = Buffer.isBuffer(inputBuffer)
? inputBuffer
: Buffer.from(inputBuffer);
- const meta = await sharp(input).metadata();
- const w = meta.width ?? 0;
- const h = meta.height ?? 0;
- const isAvif =
- meta.format === 'avif' ||
- (meta.format === 'heif' && meta.compression === 'av1');
+ const meta = await optimizeImage({ image: input, format: 'none' });
+ const w = meta.originalWidth ?? 0;
+ const h = meta.originalHeight ?? 0;
+ const isAvif = meta.originalFormat === 'avif';
const useOriginalFull =
isAvif && w > 0 && h > 0 && w <= FULL_MAX && h <= FULL_MAX;
if (!useOriginalFull) {
console.log(
- `processAvifImage: converting full image (format=${meta.format ?? 'unknown'}, ${w}×${h}, avif=${isAvif})`,
+ `processAvifImage: converting full image (format=${meta.originalFormat ?? 'unknown'}, ${w}×${h}, avif=${isAvif})`,
);
}
- const thumbPipeline = sharp(input)
- .resize(THUMB_MAX, THUMB_MAX, { fit: 'inside', withoutEnlargement: true });
- const smallThumbPipeline = sharp(input)
- .resize(SMALL_THUMB_MAX, SMALL_THUMB_MAX, { fit: 'inside', withoutEnlargement: true });
+ const fullDims = fitInsideDims(w, h, FULL_MAX, FULL_MAX);
+ const thumbDims = fitInsideDims(w, h, THUMB_MAX, THUMB_MAX);
+ const rgbaDims = fitInsideDims(w, h, 100, 100);
+ const smallThumbDims = fitInsideDims(w, h, SMALL_THUMB_MAX, SMALL_THUMB_MAX);
const [full, thumb, thumbRgba, smallThumb] = await Promise.all([
useOriginalFull
? Promise.resolve({
- info: { width: w, height: h },
+ width: w,
+ height: h,
data: input,
})
- : sharp(input)
- .resize(FULL_MAX, FULL_MAX, { fit: 'inside', withoutEnlargement: true })
- .avif({ effort: 2 })
- .toBuffer({ resolveWithObject: true }),
- thumbPipeline.clone().avif({ effort: 2 }).toBuffer({ resolveWithObject: true }),
- thumbPipeline
- .clone()
- .resize(100, 100, { fit: 'inside', withoutEnlargement: true })
- .ensureAlpha()
- .raw()
- .toBuffer({ resolveWithObject: true }),
- smallThumbPipeline
- .avif({ effort: 2 })
- .toBuffer({ resolveWithObject: true }),
+ : optimizeImage({
+ image: input,
+ ...fullDims,
+ format: 'avif',
+ speed: 8,
+ }),
+ optimizeImage({
+ image: input,
+ ...thumbDims,
+ format: 'avif',
+ speed: 8,
+ }),
+ optimizeImage({
+ image: input,
+ ...rgbaDims,
+ format: 'raw',
+ }),
+ optimizeImage({
+ image: input,
+ ...smallThumbDims,
+ format: 'avif',
+ speed: 8,
+ }),
]);
+ const rgbaPixels = unpremultiplyRgba(thumbRgba.data);
+
const [thumbHashBytes, skele] = await Promise.all([
rgbaToThumbHash(
- thumbRgba.info.width,
- thumbRgba.info.height,
- thumbRgba.data,
+ thumbRgba.width,
+ thumbRgba.height,
+ rgbaPixels,
),
encodeRGBA(
- thumbRgba.info.width,
- thumbRgba.info.height,
- thumbRgba.data,
+ thumbRgba.width,
+ thumbRgba.height,
+ rgbaPixels,
),
]);
@@ -70,24 +112,28 @@ export default async function processAvifImage(inputBuffer) {
throw new Error('encodeRGBA failed to produce a base62 skelehash');
}
+ const fullBuf = Buffer.isBuffer(full.data) ? full.data : Buffer.from(full.data);
+ const thumbBuf = Buffer.from(thumb.data);
+ const smallThumbBuf = Buffer.from(smallThumb.data);
+
return {
full: {
- w: full.info.width,
- h: full.info.height,
- size: full.data.length,
- buffer: full.data,
+ w: full.width,
+ h: full.height,
+ size: fullBuf.length,
+ buffer: fullBuf,
},
thumb: {
- w: thumb.info.width,
- h: thumb.info.height,
- size: thumb.data.length,
- buffer: thumb.data,
+ w: thumb.width,
+ h: thumb.height,
+ size: thumbBuf.length,
+ buffer: thumbBuf,
hash: Buffer.from(thumbHashBytes).toString('base64'),
},
smallThumb: {
- w: smallThumb.info.width,
- h: smallThumb.info.height,
- buffer: smallThumb.data,
+ w: smallThumb.width,
+ h: smallThumb.height,
+ buffer: smallThumbBuf,
},
skelehash: skele.base62,
};
diff --git a/test/helpers.js b/test/helpers.js
index 4d580c5..27dd89e 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -1,7 +1,7 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
-import sharp from 'sharp';
+import { optimizeImage } from 'wasm-image-optimization';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
@@ -79,11 +79,9 @@ export async function createCustomer(store, tenant, name = 'Customer') {
/** Generates a small solid-color PNG buffer for image tests. */
export async function makePng({ width = 64, height = 48, r = 200, g = 30, b = 60 } = {}) {
- return sharp({
- create: { width, height, channels: 3, background: { r, g, b } },
- })
- .png()
- .toBuffer();
+ const svg = ``;
+ const res = await optimizeImage({ image: svg, format: 'png' });
+ return Buffer.from(res.data);
}
export const isoNow = () => new Date().toISOString();