sharp by wasm

This commit is contained in:
seb
2026-10-28 01:46:48 +01:00
parent ccb81cdb26
commit 2d4d894881
12 changed files with 198 additions and 767 deletions

View File

@@ -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`:

651
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(${color.r},${color.g},${color.b})"/></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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(200,60,60)"/></svg>`;
const res = await optimizeImage({ image: svg, format: 'avif' });
return Buffer.from(res.data);
}
/**

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="rgb(25,25,112)"/></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);

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="rgb(255,0,0)"/></svg>`;
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200"><rect width="300" height="200" fill="rgb(0,128,255)"/></svg>`;
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);

View File

@@ -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;
}

View File

@@ -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 `<image href="data:image/png;base64,${b64}" x="${col * tileSize}" y="${row * tileSize}" width="${tileSize}" height="${tileSize}"/>`;
}),
);
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 =
`<svg xmlns="http://www.w3.org/2000/svg" width="${TOTAL_SIZE}" height="${TOTAL_SIZE}">` +
`<rect width="${TOTAL_SIZE}" height="${TOTAL_SIZE}" fill="#ffffff"/>` +
tiles.join('') +
`</svg>`;
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 });

View File

@@ -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
}

View File

@@ -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 };
}
}

View File

@@ -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,
};

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(${r},${g},${b})"/></svg>`;
const res = await optimizeImage({ image: svg, format: 'png' });
return Buffer.from(res.data);
}
export const isoNow = () => new Date().toISOString();