diff --git a/package-lock.json b/package-lock.json index a9af5ad..b378db0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,17 @@ "license": "ISC", "dependencies": { "@clack/prompts": "^1.8.1", - "@hpcc-js/wasm-graphviz": "^1.29.1", + "@hpcc-js/wasm-graphviz": "^1.29.2", "chalk": "^6.0.1", - "dotenv": "^18.0.4", + "dotenv": "^18.0.8", "pdfconvert-wasm": "^0.3.0", "sharp": "^0.35.5", + "skelehash-wasm": "^0.1.0", "thumbhash": "^0.1.1" }, "devDependencies": { "better-sqlite3": "^13.0.3", - "mssql": "^12.7.2" + "mssql": "^12.7.9" }, "peerDependencies": { "better-sqlite3": "^12.0.0", @@ -339,9 +340,9 @@ } }, "node_modules/@hpcc-js/wasm-graphviz": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.1.tgz", - "integrity": "sha512-koAQL0wlryEMeBs4/cQC3GB5ziITv66gR9DU/SWYjjup+iQ+Nbjcqxeaef+sffN4F4cBYZ3P31B5z7+cbSABbw==", + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.2.tgz", + "integrity": "sha512-AkArBAZERT5OxH52/+kvCHB07cZ6ZNd9GnOvjw+S880bR88D6i27XfK6xj4dBhUDxNA2QPPMdgEcky3x4TJ3KA==", "license": "Apache-2.0" }, "node_modules/@img/colour": { @@ -1152,9 +1153,9 @@ } }, "node_modules/dotenv": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.4.tgz", - "integrity": "sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==", + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", + "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", "license": "BSD-2-Clause", "bin": { "dotenv": "dist/index.cjs" @@ -1454,9 +1455,9 @@ "license": "MIT" }, "node_modules/mssql": { - "version": "12.7.2", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.2.tgz", - "integrity": "sha512-zhpPw+WXBWLw7d7J6Y0aWgQy0EV8HKDGNSvmkAjP8bSM4H41X0fweAeKWI+bg9Ri+JHCbsf/QScjmACBFcEo8A==", + "version": "12.7.9", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.9.tgz", + "integrity": "sha512-ryuy8HyUpsUgg16mkjNumuGK0/kzZ9TNkKSm/lfNcb8v5RII/U4ErOquH/kAvUqXbap9BWV0EnN5JxdewGrx7g==", "dev": true, "license": "MIT", "dependencies": { @@ -1650,6 +1651,12 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/skelehash-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.0.tgz", + "integrity": "sha512-QdxF9YsJvKlT93O70dXFaWnsKVX7KDD0RgYf9cwgOT6kGnSMOdsO0rsdge+5Tg+vv6BjivbJ/2o5P1KcQsavEw==", + "license": "MIT" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", diff --git a/package.json b/package.json index 63f0f82..4aef207 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dotenv": "^18.0.8", "pdfconvert-wasm": "^0.3.0", "sharp": "^0.35.5", + "skelehash-wasm": "^0.1.0", "thumbhash": "^0.1.1" }, "peerDependencies": { diff --git a/src/store/article/addImage.js b/src/store/article/addImage.js index 7f31f17..e3baec2 100644 --- a/src/store/article/addImage.js +++ b/src/store/article/addImage.js @@ -103,6 +103,7 @@ export default function addImage(provider) { companyuserId, fullHash, sort: effectiveSort, + skelehash: processed.skelehash, }); } @@ -124,6 +125,7 @@ export default function addImage(provider) { }, fullHash, thumbHash: processed.thumb.hash, + skelehash: processed.skelehash, }; }; diff --git a/src/store/article/deleteImage.js b/src/store/article/deleteImage.js index 34b52bd..7c4dc44 100644 --- a/src/store/article/deleteImage.js +++ b/src/store/article/deleteImage.js @@ -1,3 +1,5 @@ +import sharp from 'sharp'; +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'; import resolveArticleBySku from './resolveArticleBySku.js'; @@ -24,6 +26,55 @@ export default function deleteImage(provider) { return async (googleAuthId, sku, fullHash, tx) => { const runDelete = async (runner) => { const { articleId, companyuserId } = await resolve(googleAuthId, sku, runner); + + let thumbBuffer; + if (provider?.dialect === 'sqlite3') { + const exists = await runner.query( + 'SELECT ap.id FROM articlepic ap INNER JOIN pic p ON p.id = ap.pic_id WHERE ap.article_id = @articleId AND p.hash = @fullHash', + { articleId, fullHash }, + ); + if (!exists.rows[0]) { + throw new Error('Image not found'); + } + thumbBuffer = await (runner.readPicThumb ? runner.readPicThumb(fullHash) : provider.readPicThumb?.(fullHash)); + } else { + const picResult = await runner.query( + 'SELECT p.thumbbinary FROM pic p INNER JOIN articlepic ap ON ap.pic_id = p.id WHERE ap.article_id = @articleId AND p.hash = @fullHash', + { articleId, fullHash }, + ); + if (!picResult.rows[0]) { + throw new Error('Image not found'); + } + thumbBuffer = picResult.rows[0]?.thumbbinary; + } + + 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); + skelehash = res?.base62; + } + + if (!skelehash) { + const historyRows = await runner.query( + 'SELECT meta FROM articlehistory WHERE article_id = @articleId AND meta LIKE @pattern', + { articleId, pattern: `%"${fullHash}"%` }, + ); + for (const row of historyRows.rows || []) { + try { + const m = typeof row.meta === 'string' ? JSON.parse(row.meta) : row.meta; + if (m?.fullHash === fullHash && m?.skelehash) { + skelehash = m.skelehash; + break; + } + } catch {} + } + } + + if (!skelehash) { + throw new Error(`Failed to resolve skelehash for image ${fullHash}`); + } + const result = await runner.query(sql, { articleId, fullHash }); if (!result.rowsAffected) { @@ -42,6 +93,7 @@ export default function deleteImage(provider) { articleId, companyuserId, fullHash, + skelehash, }); }; diff --git a/src/util/processAvifImage.js b/src/util/processAvifImage.js index 513649e..7d13909 100644 --- a/src/util/processAvifImage.js +++ b/src/util/processAvifImage.js @@ -1,5 +1,6 @@ import sharp from 'sharp'; import { rgbaToThumbHash } from 'thumbhash'; +import { encodeRGBA } from 'skelehash-wasm'; const FULL_MAX = 1000; const THUMB_MAX = 200; @@ -52,11 +53,22 @@ export default async function processAvifImage(inputBuffer) { .toBuffer({ resolveWithObject: true }), ]); - const thumbHashBytes = rgbaToThumbHash( - thumbRgba.info.width, - thumbRgba.info.height, - thumbRgba.data, - ); + const [thumbHashBytes, skele] = await Promise.all([ + rgbaToThumbHash( + thumbRgba.info.width, + thumbRgba.info.height, + thumbRgba.data, + ), + encodeRGBA( + thumbRgba.info.width, + thumbRgba.info.height, + thumbRgba.data, + ), + ]); + + if (!skele?.base62) { + throw new Error('encodeRGBA failed to produce a base62 skelehash'); + } return { full: { @@ -77,5 +89,6 @@ export default async function processAvifImage(inputBuffer) { h: smallThumb.info.height, buffer: smallThumb.data, }, + skelehash: skele.base62, }; }