This commit is contained in:
seb
2026-10-09 18:26:01 +02:00
parent 07c4f0bff9
commit 178055f1d8
13 changed files with 80 additions and 25 deletions

View File

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

View File

@@ -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<object>}>`
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
## `brand.create`

View File

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

6
API.md
View File

@@ -74,7 +74,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `list(googleAuthId: number, tx?: object): Promise<Array<object>>`
- `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<void>`
- `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<object>`
- `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<object>}>`
- `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<void>`
@@ -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<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>`
- `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<object>}>`
- `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<object>}>`
- `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<void>`
- `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}>`

View File

@@ -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');

View File

@@ -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
)
`;

View File

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

View File

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

View File

@@ -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
);
`;

View File

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

View File

@@ -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<object>}>}
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>}
*/
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],
};
};

View File

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

View File

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