From fb2dafd920f796ee439871bed53cc7a740a6f78d Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 25 Oct 2026 22:38:01 +0100 Subject: [PATCH] u --- docs/api/routes/pos/cimage.md | 8 ++-- src/config/env.js | 3 ++ src/db/posterminal.js | 52 +++++++--------------- src/db/store.js | 3 +- src/v1/routes/pimage.js | 81 +++++++++++------------------------ 5 files changed, 50 insertions(+), 97 deletions(-) diff --git a/docs/api/routes/pos/cimage.md b/docs/api/routes/pos/cimage.md index e0de29a..ad32561 100644 --- a/docs/api/routes/pos/cimage.md +++ b/docs/api/routes/pos/cimage.md @@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). POS category image delivery -Returns category image by hash (`path` query param) scaled to preview size (padded to 1:1 with adaptive aspect-ratio borders: thinner borders for vertical images, wider borders for horizontal images). +Returns category image by hash (`path` query param) scaled to preview size (padded to 1:1 with white background). ## Query parameters @@ -12,9 +12,9 @@ Returns category image by hash (`path` query param) scaled to preview size (padd | Param | Required | Type | Description | |-------|----------|------|-------------| | `path` | yes | string | SHA-256 hash of the category image | -| `size` | no | integer | Max dimension (width & height) | -| `w` | no | integer | Max width | -| `h` | no | integer | Max height | +| `size` | no | integer | Max dimension (width & height, capped to 200) | +| `w` | no | integer | Max width (capped to 200) | +| `h` | no | integer | Max height (capped to 200) | | `format` | no | string | Optional target format (defaults to jpeg/png or client Accept preference) | ## Response `200` diff --git a/src/config/env.js b/src/config/env.js index f84a693..9f2ac28 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -34,8 +34,10 @@ export default { openrouterFreeKey: process.env.OPENROUTER_FREE_KEY, openrouterFreeModel: process.env.OPENROUTER_FREE_MODEL || 'openrouter/free', openrouterPaidModel: process.env.OPENROUTER_PAID_MODEL || 'openai/gpt-6-luna:floor', + storageDir: process.env.STORAGE_DIR || resolve(__dirname, '../../data/storage'), sqlite: { filename: process.env.SQLITE_PATH || process.env.SQLITE_FILENAME || resolve(__dirname, '../../data/dash.db'), + storageDir: process.env.STORAGE_DIR || resolve(__dirname, '../../data/storage'), }, mssql: { server: process.env.MSSQL_SERVER, @@ -45,6 +47,7 @@ export default { port: Number(process.env.MSSQL_PORT) || 1433, encrypt: process.env.MSSQL_ENCRYPT === 'true', trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === 'true', + storageDir: process.env.STORAGE_DIR || resolve(__dirname, '../../data/storage'), }, backup: { enabled: process.env.BACKUP_ENABLED !== 'false', diff --git a/src/db/posterminal.js b/src/db/posterminal.js index fe74703..300004d 100644 --- a/src/db/posterminal.js +++ b/src/db/posterminal.js @@ -140,39 +140,10 @@ export async function getPosImageByHash(hash, { width = 200, height = 200 } = {} const provider = await getProvider(); const isSqlite = provider?.dialect === 'sqlite3'; - if (isSqlite) { - const result = await provider.query( - 'SELECT w, h, size, hash, thumbw, thumbh, thumbhash FROM pic WHERE hash = @hash OR thumbhash = @hash OR hash = @baseHash OR thumbhash = @baseHash LIMIT 1', - { hash: cleanHash, baseHash } - ); - const row = result.rows[0]; - if (!row) return null; - - const fullHash = row.hash; - const useThumb = width <= 200 && height <= 200; - let buffer = null; - - if (useThumb) { - buffer = await provider.readPicThumb?.(fullHash); - } - if (!buffer) { - buffer = await provider.readPicFull?.(fullHash); - } - if (!buffer && !useThumb) { - buffer = await provider.readPicThumb?.(fullHash); - } - if (!buffer) return null; - - return { - buffer: Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer), - w: row.w, - h: row.h, - fullHash, - }; - } - const result = await provider.query( - 'SELECT TOP 1 w, h, size, hash, thumbw, thumbh, thumbhash, fullbinary, thumbbinary FROM pic WHERE hash = @hash OR thumbhash = @hash OR hash = @baseHash OR thumbhash = @baseHash', + isSqlite + ? 'SELECT w, h, size, hash, thumbw, thumbh, thumbhash FROM pic WHERE hash = @hash OR thumbhash = @hash OR hash = @baseHash OR thumbhash = @baseHash LIMIT 1' + : 'SELECT TOP 1 w, h, size, hash, thumbw, thumbh, thumbhash FROM pic WHERE hash = @hash OR thumbhash = @hash OR hash = @baseHash OR thumbhash = @baseHash', { hash: cleanHash, baseHash } ); const row = result.rows[0]; @@ -180,14 +151,21 @@ export async function getPosImageByHash(hash, { width = 200, height = 200 } = {} const fullHash = row.hash; const useThumb = width <= 200 && height <= 200; - const rawBuffer = useThumb - ? (row.thumbbinary || row.fullbinary) - : (row.fullbinary || row.thumbbinary); + let buffer = null; - if (!rawBuffer) return null; + if (useThumb) { + buffer = await provider.readPicThumb?.(fullHash); + } + if (!buffer) { + buffer = await provider.readPicFull?.(fullHash); + } + if (!buffer && !useThumb) { + buffer = await provider.readPicThumb?.(fullHash); + } + if (!buffer) return null; return { - buffer: Buffer.isBuffer(rawBuffer) ? rawBuffer : Buffer.from(rawBuffer), + buffer: Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer), w: row.w, h: row.h, fullHash, diff --git a/src/db/store.js b/src/db/store.js index 86f7f32..a4e5c73 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -127,6 +127,7 @@ function getMssqlConfig() { user, password, port, + storageDir: env.storageDir, pool: { max: 10, min: 0, @@ -153,7 +154,7 @@ export async function getStore() { if (env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3') { provider = new BetterSqlite3Provider(env.sqlite); } else { - provider = new MssqlProvider(getMssqlConfig()); + provider = new MssqlProvider(getMssqlConfig(), { storageDir: env.storageDir }); } await provider.initialize(); store = wrapStoreWithTiming(await new LibDashStore(provider)); diff --git a/src/v1/routes/pimage.js b/src/v1/routes/pimage.js index 59d68d0..0cf27a6 100644 --- a/src/v1/routes/pimage.js +++ b/src/v1/routes/pimage.js @@ -5,6 +5,7 @@ const PIMAGE_DEFAULT_W = 200; const PIMAGE_DEFAULT_H = 200; const CIMAGE_DEFAULT_W = 200; const CIMAGE_DEFAULT_H = 200; +const CIMAGE_MAX_DIMENSION = 200; /** * @openapi @@ -106,7 +107,7 @@ export async function handlePimage(req, res) { * tags: [POS] * summary: POS category image delivery * description: | - * Returns category image by hash (`path` query param) scaled to preview size (padded to 1:1 with adaptive aspect-ratio borders: thinner borders for vertical images, wider borders for horizontal images). + * Returns category image by hash (`path` query param) scaled to preview size (padded to 1:1 with white background). * x-markdown: pos/cimage.md * parameters: * - in: query @@ -122,21 +123,24 @@ export async function handlePimage(req, res) { * schema: * type: integer * default: 200 - * description: Max dimension (width & height) + * maximum: 200 + * description: Max dimension (width & height, capped to 200) * - in: query * name: w * required: false * schema: * type: integer * default: 200 - * description: Max width + * maximum: 200 + * description: Max width (capped to 200) * - in: query * name: h * required: false * schema: * type: integer * default: 200 - * description: Max height + * maximum: 200 + * description: Max height (capped to 200) * - in: query * name: format * required: false @@ -207,16 +211,16 @@ async function servePosImage(req, res, { isCimage = false } = {}) { if (isCimage) { if (hasW && !hasH) { - targetW = Math.min(Math.round(Number(req.query.w)), 2000); + targetW = Math.min(Math.round(Number(req.query.w)), CIMAGE_MAX_DIMENSION); targetH = targetW; } else if (hasH && !hasW) { - targetH = Math.min(Math.round(Number(req.query.h)), 2000); + targetH = Math.min(Math.round(Number(req.query.h)), CIMAGE_MAX_DIMENSION); targetW = targetH; } else if (hasW && hasH) { - targetW = Math.min(Math.round(Number(req.query.w)), 2000); - targetH = Math.min(Math.round(Number(req.query.h)), 2000); + targetW = Math.min(Math.round(Number(req.query.w)), CIMAGE_MAX_DIMENSION); + targetH = Math.min(Math.round(Number(req.query.h)), CIMAGE_MAX_DIMENSION); } else if (hasSize) { - const s = Math.min(Math.round(Number(req.query.size)), 2000); + const s = Math.min(Math.round(Number(req.query.size)), CIMAGE_MAX_DIMENSION); targetW = s; targetH = s; } else { @@ -238,7 +242,7 @@ async function servePosImage(req, res, { isCimage = false } = {}) { return res.status(404).json({ Message: `No image was found for path '${rawPath}'.` }); } - const etag = `"${image.fullHash}-${targetW}x${targetH}${isCimage ? '-c-v5' : ''}"`; + const etag = `"${image.fullHash}-${targetW}x${targetH}${isCimage ? '-c-v6' : ''}"`; const ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch === etag || ifNoneMatch === image.fullHash || ifNoneMatch === `"${image.fullHash}"`) { res.setHeader('ETag', etag); @@ -248,53 +252,20 @@ async function servePosImage(req, res, { isCimage = false } = {}) { const imageSharp = sharp(image.buffer); - let pipeline; - if (isCimage) { - const meta = await imageSharp.metadata(); - const origW = meta.width || targetW; - const origH = meta.height || targetH; - const r = origW / origH; - - // Adaptive fill ratio: - // Vertical images (r < 1) get much less border (height fills up to ~94% of tile). - // Horizontal images (r > 1) get a bit more border (width fills down to ~65% of tile). - // Square images (r = 1) get ~75% fill (~12.5% border on all sides). - let fillRatio; - if (r < 1.0) { - const t = Math.max(0, Math.min(1, (1.0 - r) / 0.5)); - fillRatio = 0.75 + t * (0.94 - 0.75); - } else { - const t = Math.max(0, Math.min(1, (r - 1.0) / 0.8)); - fillRatio = 0.75 - t * (0.75 - 0.65); + const resizeOptions = isCimage + ? { + fit: 'contain', + background: '#ffffff', + withoutEnlargement: true, } + : { + fit: 'inside', + withoutEnlargement: true, + }; - const innerW = Math.max(1, Math.round(targetW * fillRatio)); - const innerH = Math.max(1, Math.round(targetH * fillRatio)); - - const intermediateBuffer = await imageSharp - .resize(innerW, innerH, { - fit: 'contain', - background: '#ffffff', - withoutEnlargement: true, - }) - .flatten({ background: '#ffffff' }) - .toBuffer(); - - pipeline = sharp(intermediateBuffer) - .resize(targetW, targetH, { - fit: 'contain', - background: '#ffffff', - withoutEnlargement: true, - }) - .flatten({ background: '#ffffff' }); - } else { - pipeline = imageSharp - .resize(targetW, targetH, { - fit: 'inside', - withoutEnlargement: true, - }) - .flatten({ background: '#ffffff' }); - } + let pipeline = imageSharp + .resize(targetW, targetH, resizeOptions) + .flatten({ background: '#ffffff' }); const accept = String(req.headers.accept || ''); const formatParam = typeof req.query.format === 'string' ? req.query.format.toLowerCase() : '';