u
This commit is contained in:
@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
POS category image delivery
|
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
|
## Query parameters
|
||||||
@@ -12,9 +12,9 @@ Returns category image by hash (`path` query param) scaled to preview size (padd
|
|||||||
| Param | Required | Type | Description |
|
| Param | Required | Type | Description |
|
||||||
|-------|----------|------|-------------|
|
|-------|----------|------|-------------|
|
||||||
| `path` | yes | string | SHA-256 hash of the category image |
|
| `path` | yes | string | SHA-256 hash of the category image |
|
||||||
| `size` | no | integer | Max dimension (width & height) |
|
| `size` | no | integer | Max dimension (width & height, capped to 200) |
|
||||||
| `w` | no | integer | Max width |
|
| `w` | no | integer | Max width (capped to 200) |
|
||||||
| `h` | no | integer | Max height |
|
| `h` | no | integer | Max height (capped to 200) |
|
||||||
| `format` | no | string | Optional target format (defaults to jpeg/png or client Accept preference) |
|
| `format` | no | string | Optional target format (defaults to jpeg/png or client Accept preference) |
|
||||||
|
|
||||||
## Response `200`
|
## Response `200`
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ export default {
|
|||||||
openrouterFreeKey: process.env.OPENROUTER_FREE_KEY,
|
openrouterFreeKey: process.env.OPENROUTER_FREE_KEY,
|
||||||
openrouterFreeModel: process.env.OPENROUTER_FREE_MODEL || 'openrouter/free',
|
openrouterFreeModel: process.env.OPENROUTER_FREE_MODEL || 'openrouter/free',
|
||||||
openrouterPaidModel: process.env.OPENROUTER_PAID_MODEL || 'openai/gpt-6-luna:floor',
|
openrouterPaidModel: process.env.OPENROUTER_PAID_MODEL || 'openai/gpt-6-luna:floor',
|
||||||
|
storageDir: process.env.STORAGE_DIR || resolve(__dirname, '../../data/storage'),
|
||||||
sqlite: {
|
sqlite: {
|
||||||
filename: process.env.SQLITE_PATH || process.env.SQLITE_FILENAME || resolve(__dirname, '../../data/dash.db'),
|
filename: process.env.SQLITE_PATH || process.env.SQLITE_FILENAME || resolve(__dirname, '../../data/dash.db'),
|
||||||
|
storageDir: process.env.STORAGE_DIR || resolve(__dirname, '../../data/storage'),
|
||||||
},
|
},
|
||||||
mssql: {
|
mssql: {
|
||||||
server: process.env.MSSQL_SERVER,
|
server: process.env.MSSQL_SERVER,
|
||||||
@@ -45,6 +47,7 @@ export default {
|
|||||||
port: Number(process.env.MSSQL_PORT) || 1433,
|
port: Number(process.env.MSSQL_PORT) || 1433,
|
||||||
encrypt: process.env.MSSQL_ENCRYPT === 'true',
|
encrypt: process.env.MSSQL_ENCRYPT === 'true',
|
||||||
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === 'true',
|
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === 'true',
|
||||||
|
storageDir: process.env.STORAGE_DIR || resolve(__dirname, '../../data/storage'),
|
||||||
},
|
},
|
||||||
backup: {
|
backup: {
|
||||||
enabled: process.env.BACKUP_ENABLED !== 'false',
|
enabled: process.env.BACKUP_ENABLED !== 'false',
|
||||||
|
|||||||
@@ -140,39 +140,10 @@ export async function getPosImageByHash(hash, { width = 200, height = 200 } = {}
|
|||||||
const provider = await getProvider();
|
const provider = await getProvider();
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
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(
|
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 }
|
{ hash: cleanHash, baseHash }
|
||||||
);
|
);
|
||||||
const row = result.rows[0];
|
const row = result.rows[0];
|
||||||
@@ -180,14 +151,21 @@ export async function getPosImageByHash(hash, { width = 200, height = 200 } = {}
|
|||||||
|
|
||||||
const fullHash = row.hash;
|
const fullHash = row.hash;
|
||||||
const useThumb = width <= 200 && height <= 200;
|
const useThumb = width <= 200 && height <= 200;
|
||||||
const rawBuffer = useThumb
|
let buffer = null;
|
||||||
? (row.thumbbinary || row.fullbinary)
|
|
||||||
: (row.fullbinary || row.thumbbinary);
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
buffer: Buffer.isBuffer(rawBuffer) ? rawBuffer : Buffer.from(rawBuffer),
|
buffer: Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer),
|
||||||
w: row.w,
|
w: row.w,
|
||||||
h: row.h,
|
h: row.h,
|
||||||
fullHash,
|
fullHash,
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ function getMssqlConfig() {
|
|||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
port,
|
port,
|
||||||
|
storageDir: env.storageDir,
|
||||||
pool: {
|
pool: {
|
||||||
max: 10,
|
max: 10,
|
||||||
min: 0,
|
min: 0,
|
||||||
@@ -153,7 +154,7 @@ export async function getStore() {
|
|||||||
if (env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3') {
|
if (env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3') {
|
||||||
provider = new BetterSqlite3Provider(env.sqlite);
|
provider = new BetterSqlite3Provider(env.sqlite);
|
||||||
} else {
|
} else {
|
||||||
provider = new MssqlProvider(getMssqlConfig());
|
provider = new MssqlProvider(getMssqlConfig(), { storageDir: env.storageDir });
|
||||||
}
|
}
|
||||||
await provider.initialize();
|
await provider.initialize();
|
||||||
store = wrapStoreWithTiming(await new LibDashStore(provider));
|
store = wrapStoreWithTiming(await new LibDashStore(provider));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const PIMAGE_DEFAULT_W = 200;
|
|||||||
const PIMAGE_DEFAULT_H = 200;
|
const PIMAGE_DEFAULT_H = 200;
|
||||||
const CIMAGE_DEFAULT_W = 200;
|
const CIMAGE_DEFAULT_W = 200;
|
||||||
const CIMAGE_DEFAULT_H = 200;
|
const CIMAGE_DEFAULT_H = 200;
|
||||||
|
const CIMAGE_MAX_DIMENSION = 200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
@@ -106,7 +107,7 @@ export async function handlePimage(req, res) {
|
|||||||
* tags: [POS]
|
* tags: [POS]
|
||||||
* summary: POS category image delivery
|
* summary: POS category image delivery
|
||||||
* description: |
|
* 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
|
* x-markdown: pos/cimage.md
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: query
|
* - in: query
|
||||||
@@ -122,21 +123,24 @@ export async function handlePimage(req, res) {
|
|||||||
* schema:
|
* schema:
|
||||||
* type: integer
|
* type: integer
|
||||||
* default: 200
|
* default: 200
|
||||||
* description: Max dimension (width & height)
|
* maximum: 200
|
||||||
|
* description: Max dimension (width & height, capped to 200)
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: w
|
* name: w
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
* type: integer
|
* type: integer
|
||||||
* default: 200
|
* default: 200
|
||||||
* description: Max width
|
* maximum: 200
|
||||||
|
* description: Max width (capped to 200)
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: h
|
* name: h
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
* type: integer
|
* type: integer
|
||||||
* default: 200
|
* default: 200
|
||||||
* description: Max height
|
* maximum: 200
|
||||||
|
* description: Max height (capped to 200)
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: format
|
* name: format
|
||||||
* required: false
|
* required: false
|
||||||
@@ -207,16 +211,16 @@ async function servePosImage(req, res, { isCimage = false } = {}) {
|
|||||||
|
|
||||||
if (isCimage) {
|
if (isCimage) {
|
||||||
if (hasW && !hasH) {
|
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;
|
targetH = targetW;
|
||||||
} else if (hasH && !hasW) {
|
} 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;
|
targetW = targetH;
|
||||||
} else if (hasW && hasH) {
|
} else 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 = Math.min(Math.round(Number(req.query.h)), 2000);
|
targetH = Math.min(Math.round(Number(req.query.h)), CIMAGE_MAX_DIMENSION);
|
||||||
} else if (hasSize) {
|
} 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;
|
targetW = s;
|
||||||
targetH = s;
|
targetH = s;
|
||||||
} else {
|
} 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}'.` });
|
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'];
|
const ifNoneMatch = req.headers['if-none-match'];
|
||||||
if (ifNoneMatch === etag || ifNoneMatch === image.fullHash || ifNoneMatch === `"${image.fullHash}"`) {
|
if (ifNoneMatch === etag || ifNoneMatch === image.fullHash || ifNoneMatch === `"${image.fullHash}"`) {
|
||||||
res.setHeader('ETag', etag);
|
res.setHeader('ETag', etag);
|
||||||
@@ -248,53 +252,20 @@ async function servePosImage(req, res, { isCimage = false } = {}) {
|
|||||||
|
|
||||||
const imageSharp = sharp(image.buffer);
|
const imageSharp = sharp(image.buffer);
|
||||||
|
|
||||||
let pipeline;
|
const resizeOptions = isCimage
|
||||||
if (isCimage) {
|
? {
|
||||||
const meta = await imageSharp.metadata();
|
fit: 'contain',
|
||||||
const origW = meta.width || targetW;
|
background: '#ffffff',
|
||||||
const origH = meta.height || targetH;
|
withoutEnlargement: true,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
: {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true,
|
||||||
|
};
|
||||||
|
|
||||||
const innerW = Math.max(1, Math.round(targetW * fillRatio));
|
let pipeline = imageSharp
|
||||||
const innerH = Math.max(1, Math.round(targetH * fillRatio));
|
.resize(targetW, targetH, resizeOptions)
|
||||||
|
.flatten({ background: '#ffffff' });
|
||||||
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' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const accept = String(req.headers.accept || '');
|
const accept = String(req.headers.accept || '');
|
||||||
const formatParam = typeof req.query.format === 'string' ? req.query.format.toLowerCase() : '';
|
const formatParam = typeof req.query.format === 'string' ? req.query.format.toLowerCase() : '';
|
||||||
|
|||||||
Reference in New Issue
Block a user