diff --git a/API.article.md b/API.article.md index ce5ebac..2be41ef 100644 --- a/API.article.md +++ b/API.article.md @@ -32,7 +32,7 @@ Adds a child article to a parent article (creates and attaches with auto-generat *Source: `src/store/article/addImage.js`* -Adds an image to an article by its SKU, processing it into AVIF variants. +Adds an image to an article by its SKU, processing full, thumb, and 40px-edge small-thumb AVIF variants. **Parameters** @@ -45,13 +45,13 @@ Adds an image to an article by its SKU, processing it into AVIF variants. **Returns** -`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info. +`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info. **Example** ```js const result = await store.article.addImage(42, 'SKU-1', imageBuffer); -// => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash } +// => { picId, sort, full: { w, h, size }, thumb: { w, h }, smallThumb: { w, h }, fullHash, thumbHash } ``` ## `article.attachChild` diff --git a/API.brand.md b/API.brand.md index 5283b90..faafa3f 100644 --- a/API.brand.md +++ b/API.brand.md @@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/brand/addImage.js`* -Adds an image to a brand, processing it into AVIF full and thumb variants. +Adds an image to a brand, processing full, thumb, and 40px-edge small-thumb AVIF variants. **Parameters** @@ -19,7 +19,7 @@ Adds an image to a brand, processing it into AVIF full and thumb variants. **Returns** -`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array}>` +`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array}>` ## `brand.create` diff --git a/API.category.md b/API.category.md index 3be5750..eb4c51a 100644 --- a/API.category.md +++ b/API.category.md @@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/category/addImage.js`* -Adds an image to a category, processing it into AVIF full and thumb variants. +Adds an image to a category, processing full, thumb, and 40px-edge small-thumb AVIF variants. **Parameters** @@ -19,7 +19,7 @@ Adds an image to a category, processing it into AVIF full and thumb variants. **Returns** -`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>` — The added image info. +`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>` — The added image info. **Example** diff --git a/API.md b/API.md index 7b42b97..da3282e 100644 --- a/API.md +++ b/API.md @@ -74,7 +74,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `list(googleAuthId: number, tx?: object): Promise>` - `update(id: number, input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>` - [category](API.category.md) - - `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>` + - `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>` - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string}, tx?: object): Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>` - `delete(input: {googleAuthId: number, id: number, recursive?: boolean}, tx?: object): Promise` - `deleteImage(googleAuthId: number, categoryId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>` @@ -86,7 +86,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>` - [article](API.article.md) - `addChild(googleAuthId: number, parentSku: string, options: {attach?: boolean, attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise` - - `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` + - `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` - `attachChild(googleAuthId: number, parentSku: string, options: {sku: string, childSku?: string, attributeValueId?: number, value?: string, reparent?: boolean, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<{article: object, children: Array}>` - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, desc?: string, seo?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise` @@ -129,7 +129,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `list(googleAuthId: number, options?: {frontendOnly?: boolean, maskWriteonly?: boolean}, tx?: object): Promise>` - `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>` - [brand](API.brand.md) - - `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array}>` + - `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array}>` - `create(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `list(googleAuthId: number, opts?: {offset?: number, limit?: number}, tx?: object): Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>, total: number, offset: number, limit: number}>` diff --git a/scripts/test-sqlite-storage.js b/scripts/test-sqlite-storage.js index e9ab55b..1bbdbe6 100644 --- a/scripts/test-sqlite-storage.js +++ b/scripts/test-sqlite-storage.js @@ -28,7 +28,7 @@ async function runTests() { assert(existsSync(join(storageDir, 'fullbinary')), 'fullbinary dir should exist'); assert(existsSync(join(storageDir, 'thumbbinary')), 'thumbbinary dir should exist'); - // Verify sqlite tables do NOT contain blob columns + // Large file and image blobs stay on disk; the 40px small-thumb stays inline in pic. const fileTableInfo = provider.db.pragma('table_info("file")'); const fileColNames = fileTableInfo.map((c) => c.name); console.log('file table columns:', fileColNames); @@ -40,6 +40,9 @@ async function runTests() { console.log('pic table columns:', picColNames); assert(!picColNames.includes('fullbinary'), 'pic table should NOT have fullbinary column'); assert(!picColNames.includes('thumbbinary'), 'pic table should NOT have thumbbinary column'); + assert(picColNames.includes('smallthumbw'), 'pic table should have smallthumbw column'); + assert(picColNames.includes('smallthumbh'), 'pic table should have smallthumbh column'); + assert(picColNames.includes('smallthumbbinary'), 'pic table should have smallthumbbinary column'); // Setup base data: google_auth, company, unit, article, manifest const googleAuthId = await store.google_auth.add('test@example.com'); @@ -199,6 +202,18 @@ async function runTests() { const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage); const fullHash = picResult.fullHash; + assert.equal(picResult.smallThumb.w, 40); + assert.equal(picResult.smallThumb.h, 27); + const smallThumbRow = provider.db.prepare( + 'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?', + ).get(fullHash); + assert.equal(smallThumbRow.smallthumbw, 40); + assert.equal(smallThumbRow.smallthumbh, 27); + 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, 40); + assert.equal(smallThumbMetadata.height, 27); console.log('Article pic added with hash:', fullHash); const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif'); diff --git a/src/providers/mssql/queries/pic.js b/src/providers/mssql/queries/pic.js index de2a6ad..f1824fd 100644 --- a/src/providers/mssql/queries/pic.js +++ b/src/providers/mssql/queries/pic.js @@ -1,10 +1,14 @@ export const CREATE = ` INSERT INTO pic ( - w, h, size, hash, thumbw, thumbh, thumbhash, source, fullbinary, thumbbinary + w, h, size, hash, thumbw, thumbh, thumbhash, + smallthumbw, smallthumbh, smallthumbbinary, + source, fullbinary, thumbbinary ) OUTPUT INSERTED.id VALUES ( - @w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source, @fullbinary, @thumbbinary + @w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, + @smallthumbw, @smallthumbh, @smallthumbbinary, + @source, @fullbinary, @thumbbinary ) `; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 6882502..bf8d930 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -95,9 +95,12 @@ BEGIN thumbw INT NOT NULL, thumbh INT NOT NULL, thumbhash NVARCHAR(64) NOT NULL, + smallthumbw INT NOT NULL, + smallthumbh INT NOT NULL, source NVARCHAR(MAX) NOT NULL, fullbinary VARBINARY(MAX) NOT NULL, thumbbinary VARBINARY(MAX) NOT NULL, + smallthumbbinary VARBINARY(MAX) NOT NULL, CONSTRAINT UQ_pic_hash UNIQUE (hash) ); END @@ -1220,4 +1223,3 @@ export const SCHEMA_SCRIPTS = [ INIT_SHIPPING, INIT_PACKAGE, ]; - diff --git a/src/providers/sqlite3/queries/pic.js b/src/providers/sqlite3/queries/pic.js index a3cade7..3810687 100644 --- a/src/providers/sqlite3/queries/pic.js +++ b/src/providers/sqlite3/queries/pic.js @@ -8,10 +8,12 @@ export const { export const CREATE = ` INSERT INTO pic ( - w, h, size, hash, thumbw, thumbh, thumbhash, source + w, h, size, hash, thumbw, thumbh, thumbhash, + smallthumbw, smallthumbh, smallthumbbinary, source ) VALUES ( - @w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source + @w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, + @smallthumbw, @smallthumbh, @smallthumbbinary, @source ) RETURNING id `; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 65d9714..e3ba269 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -43,6 +43,9 @@ CREATE TABLE IF NOT EXISTS pic ( thumbw INTEGER NOT NULL, thumbh INTEGER NOT NULL, thumbhash TEXT NOT NULL, + smallthumbw INTEGER NOT NULL, + smallthumbh INTEGER NOT NULL, + smallthumbbinary BLOB NOT NULL, source TEXT NOT NULL ); `; diff --git a/src/store/article/addImage.js b/src/store/article/addImage.js index 7d67713..2cf895e 100644 --- a/src/store/article/addImage.js +++ b/src/store/article/addImage.js @@ -22,17 +22,17 @@ export default function addImage(provider) { const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC; /** - * Adds an image to an article by its SKU, processing it into AVIF variants. + * Adds an image to an article by its SKU, processing full, thumb, and 40px-edge small-thumb AVIF variants. * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. * @param {Buffer} image - The source image buffer. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info. + * @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info. * * @example * const result = await store.article.addImage(42, 'SKU-1', imageBuffer); - * // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash } + * // => { picId, sort, full: { w, h, size }, thumb: { w, h }, smallThumb: { w, h }, fullHash, thumbHash } */ return async (googleAuthId, sku, image, tx) => { const runAdd = async (runner) => { @@ -71,6 +71,9 @@ export default function addImage(provider) { thumbw: processed.thumb.w, thumbh: processed.thumb.h, thumbhash: processed.thumb.hash, + smallthumbw: processed.smallThumb.w, + smallthumbh: processed.smallThumb.h, + smallthumbbinary: processed.smallThumb.buffer, source: '', fullbinary: processed.full.buffer, thumbbinary: processed.thumb.buffer, @@ -107,6 +110,10 @@ export default function addImage(provider) { w: processed.thumb.w, h: processed.thumb.h, }, + smallThumb: { + w: processed.smallThumb.w, + h: processed.smallThumb.h, + }, fullHash, thumbHash: processed.thumb.hash, }; @@ -121,4 +128,3 @@ export default function addImage(provider) { return runAdd(makeRunner(provider)); }; } - diff --git a/src/store/brand/addImage.js b/src/store/brand/addImage.js index 528bd16..7bd3eee 100644 --- a/src/store/brand/addImage.js +++ b/src/store/brand/addImage.js @@ -22,13 +22,13 @@ export default function addImage(provider) { const addBrandPicSql = isSqlite ? SQLITE_ADD_BRANDPIC : MSSQL_ADD_BRANDPIC; /** - * Adds an image to a brand, processing it into AVIF full and thumb variants. + * Adds an image to a brand, processing full, thumb, and 40px-edge small-thumb AVIF variants. * * @param {number} googleAuthId - The id of the Google auth user. * @param {number} brandId - The id of the brand. * @param {Buffer} image - The source image buffer. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array}>} + * @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array}>} */ return async (googleAuthId, brandId, image, tx) => { const runAdd = async (runner) => { @@ -69,6 +69,9 @@ export default function addImage(provider) { thumbw: processed.thumb.w, thumbh: processed.thumb.h, thumbhash: processed.thumb.hash, + smallthumbw: processed.smallThumb.w, + smallthumbh: processed.smallThumb.h, + smallthumbbinary: processed.smallThumb.buffer, source: '', fullbinary: processed.full.buffer, thumbbinary: processed.thumb.buffer, @@ -88,6 +91,8 @@ export default function addImage(provider) { thumbW: processed.thumb.w, thumbH: processed.thumb.h, thumbHash: processed.thumb.hash, + smallThumbW: processed.smallThumb.w, + smallThumbH: processed.smallThumb.h, }; return { @@ -96,6 +101,8 @@ export default function addImage(provider) { thumbW: processed.thumb.w, thumbH: processed.thumb.h, thumbHash: processed.thumb.hash, + smallThumbW: processed.smallThumb.w, + smallThumbH: processed.smallThumb.h, pics: [picObj], }; }; diff --git a/src/store/category/addImage.js b/src/store/category/addImage.js index 1448751..1c6d982 100644 --- a/src/store/category/addImage.js +++ b/src/store/category/addImage.js @@ -22,13 +22,13 @@ export default function addImage(provider) { const addCategoryPicSql = isSqlite ? SQLITE_ADD_CATEGORYPIC : MSSQL_ADD_CATEGORYPIC; /** - * Adds an image to a category, processing it into AVIF full and thumb variants. + * Adds an image to a category, processing full, thumb, and 40px-edge small-thumb AVIF variants. * * @param {number} googleAuthId - The id of the Google auth user. * @param {number} categoryId - The id of the category. * @param {Buffer} image - The source image buffer. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>} The added image info. + * @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>} The added image info. * * @example * const result = await store.category.addImage(42, 5, imageBuffer); @@ -71,6 +71,9 @@ export default function addImage(provider) { thumbw: processed.thumb.w, thumbh: processed.thumb.h, thumbhash: processed.thumb.hash, + smallthumbw: processed.smallThumb.w, + smallthumbh: processed.smallThumb.h, + smallthumbbinary: processed.smallThumb.buffer, source: '', fullbinary: processed.full.buffer, thumbbinary: processed.thumb.buffer, @@ -98,6 +101,8 @@ export default function addImage(provider) { thumbW: processed.thumb.w, thumbH: processed.thumb.h, thumbHash: processed.thumb.hash, + smallThumbW: processed.smallThumb.w, + smallThumbH: processed.smallThumb.h, }; }; diff --git a/src/util/processAvifImage.js b/src/util/processAvifImage.js index 94eaa60..79e54b3 100644 --- a/src/util/processAvifImage.js +++ b/src/util/processAvifImage.js @@ -3,6 +3,7 @@ import { rgbaToThumbHash } from 'thumbhash'; const FULL_MAX = 1000; const THUMB_MAX = 200; +const SMALL_THUMB_MAX = 40; export default async function processAvifImage(inputBuffer) { const input = Buffer.isBuffer(inputBuffer) @@ -26,8 +27,10 @@ export default async function processAvifImage(inputBuffer) { 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 [full, thumb, thumbRgba] = await Promise.all([ + const [full, thumb, thumbRgba, smallThumb] = await Promise.all([ useOriginalFull ? Promise.resolve({ info: { width: w, height: h }, @@ -44,6 +47,9 @@ export default async function processAvifImage(inputBuffer) { .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }), + smallThumbPipeline + .avif({ effort: 2 }) + .toBuffer({ resolveWithObject: true }), ]); const thumbHashBytes = rgbaToThumbHash( @@ -66,5 +72,10 @@ export default async function processAvifImage(inputBuffer) { buffer: thumb.data, hash: Buffer.from(thumbHashBytes).toString('base64'), }, + smallThumb: { + w: smallThumb.info.width, + h: smallThumb.info.height, + buffer: smallThumb.data, + }, }; }