u
This commit is contained in:
@@ -925,7 +925,7 @@ _none_
|
|||||||
- Params: `query: lastChangedCategory?: str, limit?: int`
|
- Params: `query: lastChangedCategory?: str, limit?: int`
|
||||||
- Returns: `200 {}[]`
|
- Returns: `200 {}[]`
|
||||||
#### POS category image delivery — `GET /v1/cimage`
|
#### POS category image delivery — `GET /v1/cimage`
|
||||||
- Params: `query: path: str, w?: int, h?: int, format?: str`
|
- Params: `query: path: str, size?: int, w?: int, h?: int, format?: str`
|
||||||
- Returns: `200 jpeg`
|
- Returns: `200 jpeg`
|
||||||
#### POS discovery and pairing — `GET /v1/client`
|
#### POS discovery and pairing — `GET /v1/client`
|
||||||
- Params: `query: authCode: str, name?: str`
|
- Params: `query: authCode: str, name?: str`
|
||||||
@@ -946,7 +946,7 @@ _none_
|
|||||||
- Params: `query: mandantId?: str, body: { orders?: {}[] }`
|
- Params: `query: mandantId?: str, body: { orders?: {}[] }`
|
||||||
- Returns: `200 { status: str, externalOrderId: str, message: str }[]`
|
- Returns: `200 { status: str, externalOrderId: str, message: str }[]`
|
||||||
#### POS product image delivery — `GET /v1/pimage`
|
#### POS product image delivery — `GET /v1/pimage`
|
||||||
- Params: `query: path: str, w?: int, h?: int, format?: str`
|
- Params: `query: path: str, size?: int, w?: int, h?: int, format?: str`
|
||||||
- Returns: `200 jpeg`
|
- Returns: `200 jpeg`
|
||||||
#### JTL-POS product catalog synchronization — `GET /v1/product`
|
#### JTL-POS product catalog synchronization — `GET /v1/product`
|
||||||
- Params: `query: lastChangedProduct?: str, limit?: int`
|
- Params: `query: lastChangedProduct?: str, limit?: int`
|
||||||
|
|||||||
@@ -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 with 3:4 portrait ratio.
|
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).
|
||||||
|
|
||||||
|
|
||||||
## Query parameters
|
## Query parameters
|
||||||
@@ -12,6 +12,7 @@ Returns category image by hash (`path` query param) scaled to preview size with
|
|||||||
| 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) |
|
||||||
| `w` | no | integer | Max width |
|
| `w` | no | integer | Max width |
|
||||||
| `h` | no | integer | Max height |
|
| `h` | no | integer | Max height |
|
||||||
| `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) |
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Returns product image by hash (`path` query param) scaled to preview size.
|
|||||||
| Param | Required | Type | Description |
|
| Param | Required | Type | Description |
|
||||||
|-------|----------|------|-------------|
|
|-------|----------|------|-------------|
|
||||||
| `path` | yes | string | SHA-256 hash of the product image |
|
| `path` | yes | string | SHA-256 hash of the product image |
|
||||||
|
| `size` | no | integer | Max dimension (width & height) |
|
||||||
| `w` | no | integer | Max width |
|
| `w` | no | integer | Max width |
|
||||||
| `h` | no | integer | Max height |
|
| `h` | no | integer | Max height |
|
||||||
| `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) |
|
||||||
|
|||||||
BIN
scratch/curl-pimage.jpg
Normal file
BIN
scratch/curl-pimage.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -3,7 +3,7 @@ import * as db from '../../db/posterminal.js';
|
|||||||
|
|
||||||
const PIMAGE_DEFAULT_W = 200;
|
const PIMAGE_DEFAULT_W = 200;
|
||||||
const PIMAGE_DEFAULT_H = 200;
|
const PIMAGE_DEFAULT_H = 200;
|
||||||
const CIMAGE_DEFAULT_W = 150;
|
const CIMAGE_DEFAULT_W = 200;
|
||||||
const CIMAGE_DEFAULT_H = 200;
|
const CIMAGE_DEFAULT_H = 200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +24,13 @@ const CIMAGE_DEFAULT_H = 200;
|
|||||||
* description: SHA-256 hash of the product image
|
* description: SHA-256 hash of the product image
|
||||||
* example: "71de4085ce2b1a53317832421009d844eec0c36a3ec071a31661cc2a212e15e6"
|
* example: "71de4085ce2b1a53317832421009d844eec0c36a3ec071a31661cc2a212e15e6"
|
||||||
* - in: query
|
* - in: query
|
||||||
|
* name: size
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 200
|
||||||
|
* description: Max dimension (width & height)
|
||||||
|
* - in: query
|
||||||
* name: w
|
* name: w
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
@@ -99,7 +106,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 with 3:4 portrait ratio.
|
* 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).
|
||||||
* x-markdown: pos/cimage.md
|
* x-markdown: pos/cimage.md
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: query
|
* - in: query
|
||||||
@@ -110,11 +117,18 @@ export async function handlePimage(req, res) {
|
|||||||
* description: SHA-256 hash of the category image
|
* description: SHA-256 hash of the category image
|
||||||
* example: "71de4085ce2b1a53317832421009d844eec0c36a3ec071a31661cc2a212e15e6"
|
* example: "71de4085ce2b1a53317832421009d844eec0c36a3ec071a31661cc2a212e15e6"
|
||||||
* - in: query
|
* - in: query
|
||||||
|
* name: size
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 200
|
||||||
|
* description: Max dimension (width & height)
|
||||||
|
* - in: query
|
||||||
* name: w
|
* name: w
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
* type: integer
|
* type: integer
|
||||||
* default: 150
|
* default: 200
|
||||||
* description: Max width
|
* description: Max width
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: h
|
* name: h
|
||||||
@@ -184,6 +198,7 @@ async function servePosImage(req, res, { isCimage = false } = {}) {
|
|||||||
return res.status(400).json({ Message: "Missing required query parameter 'path'." });
|
return res.status(400).json({ Message: "Missing required query parameter 'path'." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSize = req.query.size !== undefined && Number.isFinite(Number(req.query.size)) && Number(req.query.size) > 0;
|
||||||
const hasW = req.query.w !== undefined && Number.isFinite(Number(req.query.w)) && Number(req.query.w) > 0;
|
const hasW = req.query.w !== undefined && Number.isFinite(Number(req.query.w)) && Number(req.query.w) > 0;
|
||||||
const hasH = req.query.h !== undefined && Number.isFinite(Number(req.query.h)) && Number(req.query.h) > 0;
|
const hasH = req.query.h !== undefined && Number.isFinite(Number(req.query.h)) && Number(req.query.h) > 0;
|
||||||
|
|
||||||
@@ -193,20 +208,26 @@ 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)), 2000);
|
||||||
targetH = Math.min(Math.round((targetW * 4) / 3), 2000);
|
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)), 2000);
|
||||||
targetW = Math.min(Math.round((targetH * 3) / 4), 2000);
|
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)), 2000);
|
||||||
targetH = Math.min(Math.round(Number(req.query.h)), 2000);
|
targetH = Math.min(Math.round(Number(req.query.h)), 2000);
|
||||||
|
} else if (hasSize) {
|
||||||
|
const s = Math.min(Math.round(Number(req.query.size)), 2000);
|
||||||
|
targetW = s;
|
||||||
|
targetH = s;
|
||||||
} else {
|
} else {
|
||||||
targetW = CIMAGE_DEFAULT_W;
|
targetW = CIMAGE_DEFAULT_W;
|
||||||
targetH = CIMAGE_DEFAULT_H;
|
targetH = CIMAGE_DEFAULT_H;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const widthParam = hasW ? Number(req.query.w) : PIMAGE_DEFAULT_W;
|
const defaultW = hasSize ? Number(req.query.size) : PIMAGE_DEFAULT_W;
|
||||||
const heightParam = hasH ? Number(req.query.h) : PIMAGE_DEFAULT_H;
|
const defaultH = hasSize ? Number(req.query.size) : PIMAGE_DEFAULT_H;
|
||||||
|
const widthParam = hasW ? Number(req.query.w) : defaultW;
|
||||||
|
const heightParam = hasH ? Number(req.query.h) : defaultH;
|
||||||
targetW = Math.min(Math.round(widthParam), 2000);
|
targetW = Math.min(Math.round(widthParam), 2000);
|
||||||
targetH = Math.min(Math.round(heightParam), 2000);
|
targetH = Math.min(Math.round(heightParam), 2000);
|
||||||
}
|
}
|
||||||
@@ -217,7 +238,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' : ''}"`;
|
const etag = `"${image.fullHash}-${targetW}x${targetH}${isCimage ? '-c-v5' : ''}"`;
|
||||||
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);
|
||||||
@@ -226,22 +247,54 @@ async function servePosImage(req, res, { isCimage = false } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imageSharp = sharp(image.buffer);
|
const imageSharp = sharp(image.buffer);
|
||||||
const meta = await imageSharp.metadata();
|
|
||||||
|
|
||||||
const resizeOptions = isCimage
|
let pipeline;
|
||||||
? {
|
if (isCimage) {
|
||||||
fit: 'contain',
|
const meta = await imageSharp.metadata();
|
||||||
background: '#ffffff',
|
const origW = meta.width || targetW;
|
||||||
withoutEnlargement: true,
|
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);
|
||||||
}
|
}
|
||||||
: {
|
|
||||||
fit: 'inside',
|
|
||||||
withoutEnlargement: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let pipeline = imageSharp
|
const innerW = Math.max(1, Math.round(targetW * fillRatio));
|
||||||
.resize(targetW, targetH, resizeOptions)
|
const innerH = Math.max(1, Math.round(targetH * fillRatio));
|
||||||
.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() : '';
|
||||||
@@ -253,7 +306,7 @@ async function servePosImage(req, res, { isCimage = false } = {}) {
|
|||||||
} else if (formatParam === 'webp' || (!formatParam && accept.includes('image/webp'))) {
|
} else if (formatParam === 'webp' || (!formatParam && accept.includes('image/webp'))) {
|
||||||
pipeline = pipeline.webp({ quality: 85 });
|
pipeline = pipeline.webp({ quality: 85 });
|
||||||
contentType = 'image/webp';
|
contentType = 'image/webp';
|
||||||
} else if (formatParam === 'png' || (!formatParam && meta.hasAlpha)) {
|
} else if (formatParam === 'png') {
|
||||||
pipeline = pipeline.png();
|
pipeline = pipeline.png();
|
||||||
contentType = 'image/png';
|
contentType = 'image/png';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user