u
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
.env
|
.env
|
||||||
data/
|
data/
|
||||||
logs/
|
logs/
|
||||||
|
print_spool/
|
||||||
@@ -43,6 +43,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` |
|
| [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` |
|
||||||
| [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` |
|
| [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` |
|
||||||
| [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` |
|
| [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` |
|
||||||
|
| [`routes/article/:sku/print.md`](routes/article/:sku/print.md) | `POST /article/{sku}/print` |
|
||||||
| [`routes/article/supplier/list.md`](routes/article/supplier/list.md) | `GET /article/{sku}/supplier` |
|
| [`routes/article/supplier/list.md`](routes/article/supplier/list.md) | `GET /article/{sku}/supplier` |
|
||||||
| [`routes/article/supplier/create.md`](routes/article/supplier/create.md) | `POST /article/{sku}/supplier` |
|
| [`routes/article/supplier/create.md`](routes/article/supplier/create.md) | `POST /article/{sku}/supplier` |
|
||||||
| [`routes/article/supplier/order.md`](routes/article/supplier/order.md) | `PUT /article/{sku}/supplier/order` |
|
| [`routes/article/supplier/order.md`](routes/article/supplier/order.md) | `PUT /article/{sku}/supplier/order` |
|
||||||
@@ -103,6 +104,12 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/events.md`](routes/events.md) | `GET /events` |
|
| [`routes/events.md`](routes/events.md) | `GET /events` |
|
||||||
| [`routes/health.md`](routes/health.md) | `GET /health` |
|
| [`routes/health.md`](routes/health.md) | `GET /health` |
|
||||||
| [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` |
|
| [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` |
|
||||||
|
| [`routes/labeltemplate/list.md`](routes/labeltemplate/list.md) | `GET /labeltemplate` |
|
||||||
|
| [`routes/labeltemplate/create.md`](routes/labeltemplate/create.md) | `POST /labeltemplate` |
|
||||||
|
| [`routes/labeltemplate/delete.md`](routes/labeltemplate/delete.md) | `DELETE /labeltemplate/{id}` |
|
||||||
|
| [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) | `GET /labeltemplate/{id}` |
|
||||||
|
| [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PATCH /labeltemplate/{id}` |
|
||||||
|
| [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PUT /labeltemplate/{id}` |
|
||||||
| [`routes/ledger.get.md`](routes/ledger.get.md) | `GET /ledger` |
|
| [`routes/ledger.get.md`](routes/ledger.get.md) | `GET /ledger` |
|
||||||
| [`routes/ledger.post.md`](routes/ledger.post.md) | `POST /ledger` |
|
| [`routes/ledger.post.md`](routes/ledger.post.md) | `POST /ledger` |
|
||||||
| [`routes/ledger/manifestarticle/:manifestarticleId.md`](routes/ledger/manifestarticle/:manifestarticleId.md) | `GET /ledger/manifestarticle/{manifestarticleId}` |
|
| [`routes/ledger/manifestarticle/:manifestarticleId.md`](routes/ledger/manifestarticle/:manifestarticleId.md) | `GET /ledger/manifestarticle/{manifestarticleId}` |
|
||||||
|
|||||||
@@ -4624,6 +4624,415 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/labeltemplate": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"LabelTemplate"
|
||||||
|
],
|
||||||
|
"summary": "List label templates",
|
||||||
|
"description": "Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present. `content.objects` is omitted to reduce payload size.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "labeltemplate/list.md",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of label templates",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"labeltemplates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "No company selected",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"LabelTemplate"
|
||||||
|
],
|
||||||
|
"summary": "Create label template",
|
||||||
|
"description": "Creates a new ZPL label template for the selected company.\nA 50×50 1-bit PNG thumbnail is automatically rendered from the content\nand stored with the template.\n",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "labeltemplate/create.md",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplateCreateInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created label template",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"labeltemplate": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input or missing fields",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/labeltemplate/{id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"LabelTemplate"
|
||||||
|
],
|
||||||
|
"summary": "Get label template by ID",
|
||||||
|
"description": "Retrieves a single label template for the authenticated user's selected company.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "labeltemplate/get.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "Label template ID"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Label template details",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"labeltemplate": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID or no company selected",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label template not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"LabelTemplate"
|
||||||
|
],
|
||||||
|
"summary": "Update label template (partial)",
|
||||||
|
"description": "Updates specified fields of an existing label template.\nWhen `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.\n",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "labeltemplate/update.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "Label template ID"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplateUpdateInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Updated label template",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"labeltemplate": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label template not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"LabelTemplate"
|
||||||
|
],
|
||||||
|
"summary": "Update label template",
|
||||||
|
"description": "Replaces an existing label template.\nWhen `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.\n",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "labeltemplate/update.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "Label template ID"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplateUpdateInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Updated label template",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"labeltemplate": {
|
||||||
|
"$ref": "#/components/schemas/LabelTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label template not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"LabelTemplate"
|
||||||
|
],
|
||||||
|
"summary": "Delete label template",
|
||||||
|
"description": "Deletes a label template for the authenticated user's selected company.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "labeltemplate/delete.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "Label template ID"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Label template deleted"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid ID or no company selected",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label template not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/health": {
|
"/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -11200,6 +11609,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/article/{sku}/print": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Article"
|
||||||
|
],
|
||||||
|
"summary": "Print article label",
|
||||||
|
"description": "Prints a label for the article using a label template.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "sku",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"templateId",
|
||||||
|
"printerId",
|
||||||
|
"quantity"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"templateId": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"printerId": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Print job dispatched"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid parameters"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Printer, Template or Article not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/selected-company": {
|
"/auth/selected-company": {
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
39
docs/api/routes/article/:sku/print.md
Normal file
39
docs/api/routes/article/:sku/print.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# `POST /article/{sku}/print`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Print article label
|
||||||
|
|
||||||
|
Prints a label for the article using a label template.
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `sku` | yes | string | |
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"templateId": 1,
|
||||||
|
"printerId": 1,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Print job dispatched
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid parameters |
|
||||||
|
| 404 | Printer, Template or Article not found |
|
||||||
|
| 500 | Server error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
@@ -5,54 +5,26 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
Create label template
|
Create label template
|
||||||
|
|
||||||
Creates a new ZPL label template for the selected company.
|
Creates a new ZPL label template for the selected company.
|
||||||
A 50×50 1-bit PNG thumbnail is automatically rendered from the content and stored with the template.
|
A 50×50 1-bit PNG thumbnail is automatically rendered from the content
|
||||||
|
and stored with the template.
|
||||||
|
|
||||||
|
|
||||||
## Body (JSON)
|
## Body (JSON)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{}
|
||||||
"name": "…",
|
|
||||||
"width": 448,
|
|
||||||
"height": 200,
|
|
||||||
"dpi": 203,
|
|
||||||
"type": "zpl",
|
|
||||||
"content": {
|
|
||||||
"objects": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Required | Type | Description |
|
|
||||||
|-------|----------|------|-------------|
|
|
||||||
| `name` | yes | string | Template name |
|
|
||||||
| `width` | yes | integer | Canvas width in pixels |
|
|
||||||
| `height` | yes | integer | Canvas height in pixels |
|
|
||||||
| `dpi` | yes | integer | Printer DPI — `203` or `300` |
|
|
||||||
| `type` | yes | string | Template type — `"zpl"` |
|
|
||||||
| `content` | yes | object | Label editor JSON content |
|
|
||||||
|
|
||||||
## Response `201`
|
## Response `201`
|
||||||
|
|
||||||
Created label template
|
Created label template
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"labeltemplate": {
|
"labeltemplate": {}
|
||||||
"id": 1,
|
|
||||||
"company_id": 1,
|
|
||||||
"name": "…",
|
|
||||||
"width": 448,
|
|
||||||
"height": 200,
|
|
||||||
"dpi": 203,
|
|
||||||
"type": "zpl",
|
|
||||||
"content": {},
|
|
||||||
"thumb": "…"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`thumb` is a base64-encoded 50×50 1-bit PNG string, or `null` if rendering failed.
|
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
| Status | Description |
|
| Status | Description |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
Get label template by ID
|
Get label template by ID
|
||||||
|
|
||||||
Retrieves a single label template for the authenticated user's selected company. The `thumb` field is always included inline as a base64-encoded 50×50 1-bit PNG string (`null` if not yet rendered).
|
Retrieves a single label template for the authenticated user's selected company.
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
|
|
||||||
@@ -18,17 +18,7 @@ Label template details
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"labeltemplate": {
|
"labeltemplate": {}
|
||||||
"id": 1,
|
|
||||||
"company_id": 1,
|
|
||||||
"name": "…",
|
|
||||||
"width": 448,
|
|
||||||
"height": 200,
|
|
||||||
"dpi": 203,
|
|
||||||
"type": "zpl",
|
|
||||||
"content": {},
|
|
||||||
"thumb": "…"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
List label templates
|
List label templates
|
||||||
|
|
||||||
Lists all ZPL label templates for the selected company. The `thumb` field is always included inline as a base64-encoded 50×50 1-bit PNG string (`null` if not yet rendered).
|
Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present. `content.objects` is omitted to reduce payload size.
|
||||||
|
|
||||||
## Response `200`
|
## Response `200`
|
||||||
|
|
||||||
@@ -13,17 +13,7 @@ List of label templates
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"labeltemplates": [
|
"labeltemplates": [
|
||||||
{
|
{}
|
||||||
"id": 1,
|
|
||||||
"company_id": 1,
|
|
||||||
"name": "…",
|
|
||||||
"width": 448,
|
|
||||||
"height": 200,
|
|
||||||
"dpi": 203,
|
|
||||||
"type": "zpl",
|
|
||||||
"content": {},
|
|
||||||
"thumb": "…"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# `PATCH /labeltemplate/{id}`
|
# `PUT /labeltemplate/{id}`
|
||||||
|
|
||||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
Update label template (partial)
|
Update label template
|
||||||
|
|
||||||
Updates specified fields of an existing label template for the selected company.
|
Replaces an existing label template.
|
||||||
When `content`, `width`, or `height` is provided, the thumbnail is automatically re-rendered.
|
When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||||
|
|
||||||
Also available as `PUT /labeltemplate/{id}`.
|
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
|
|
||||||
@@ -17,47 +16,17 @@ Also available as `PUT /labeltemplate/{id}`.
|
|||||||
|
|
||||||
## Body (JSON)
|
## Body (JSON)
|
||||||
|
|
||||||
All fields are optional.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{}
|
||||||
"name": "…",
|
|
||||||
"width": 448,
|
|
||||||
"height": 200,
|
|
||||||
"dpi": 203,
|
|
||||||
"type": "zpl",
|
|
||||||
"content": {
|
|
||||||
"objects": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `name` | string | Template name |
|
|
||||||
| `width` | integer | Canvas width in pixels |
|
|
||||||
| `height` | integer | Canvas height in pixels |
|
|
||||||
| `dpi` | integer | Printer DPI — `203` or `300` |
|
|
||||||
| `type` | string | Template type — `"zpl"` |
|
|
||||||
| `content` | object | Label editor JSON content |
|
|
||||||
|
|
||||||
## Response `200`
|
## Response `200`
|
||||||
|
|
||||||
Updated label template
|
Updated label template
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"labeltemplate": {
|
"labeltemplate": {}
|
||||||
"id": 1,
|
|
||||||
"company_id": 1,
|
|
||||||
"name": "…",
|
|
||||||
"width": 448,
|
|
||||||
"height": 200,
|
|
||||||
"dpi": 203,
|
|
||||||
"type": "zpl",
|
|
||||||
"content": {},
|
|
||||||
"thumb": "…"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
import { renderLabel } from 'react-canvas-label-editor';
|
||||||
import * as db from '../../db/article.js';
|
import * as db from '../../db/article.js';
|
||||||
import * as picDb from '../../db/pic.js';
|
import * as picDb from '../../db/pic.js';
|
||||||
|
import * as printerDb from '../../db/printer.js';
|
||||||
|
import * as labeltemplateDb from '../../db/labeltemplate.js';
|
||||||
import { emit } from '../sse.js';
|
import { emit } from '../sse.js';
|
||||||
import { buildArtikelCsv, toWindows1252 } from '../../util/artikelCsv.js';
|
import { buildArtikelCsv, toWindows1252 } from '../../util/artikelCsv.js';
|
||||||
|
|
||||||
@@ -67,6 +72,22 @@ function articleErrorStatus(error) {
|
|||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function interpolateString(str, article) {
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
const net = article.net_price ?? article.prices?.[0]?.net;
|
||||||
|
const taxRate = article.prices?.[0]?.taxRate ?? 0;
|
||||||
|
const gross = net != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null;
|
||||||
|
const ean = (article.eans && article.eans.length > 0) ? article.eans[0] : '';
|
||||||
|
|
||||||
|
return str
|
||||||
|
.replace(/\{\{(?:ARTICLE_NAME|article_name|name)\}\}/gi, article.name || '')
|
||||||
|
.replace(/\{\{SKU\}\}/gi, article.sku || '')
|
||||||
|
.replace(/\{\{(?:NET_PRICE|net_price)\}\}/gi, net != null ? net.toString() : '')
|
||||||
|
.replace(/\{\{(?:GROSS_PRICE|gross_price)\}\}/gi, gross != null ? gross.toString() : '')
|
||||||
|
.replace(/\{\{(?:PRICE|price)\}\}/gi, gross != null ? gross.toString() : (net != null ? net.toString() : ''))
|
||||||
|
.replace(/\{\{(?:EAN|ean)\}\}/gi, ean);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers article routes (requires auth middleware).
|
* Registers article routes (requires auth middleware).
|
||||||
* @param {import('express').Application} app - Express app
|
* @param {import('express').Application} app - Express app
|
||||||
@@ -1502,4 +1523,134 @@ export function register(app, basePath = '') {
|
|||||||
res.status(status).json({ error: error.message });
|
res.status(status).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /article/{sku}/print:
|
||||||
|
* post:
|
||||||
|
* tags: [Article]
|
||||||
|
* summary: Print article label
|
||||||
|
* description: Prints a label for the article using a label template.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: sku
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [templateId, printerId, quantity]
|
||||||
|
* properties:
|
||||||
|
* templateId:
|
||||||
|
* type: integer
|
||||||
|
* printerId:
|
||||||
|
* type: integer
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Print job dispatched
|
||||||
|
* 400:
|
||||||
|
* description: Invalid parameters
|
||||||
|
* 404:
|
||||||
|
* description: Printer, Template or Article not found
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/article/:sku/print`, express.json(), async (req, res) => {
|
||||||
|
const companyId = req.user?.selectedCompanyId;
|
||||||
|
if (companyId == null) {
|
||||||
|
return res.status(400).json({ error: 'No company selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sku: parsedSku, error: skuError } = parseSku(req.params.sku);
|
||||||
|
if (skuError) return res.status(400).json({ error: skuError });
|
||||||
|
const sku = parsedSku;
|
||||||
|
|
||||||
|
const { templateId, printerId, quantity } = req.body;
|
||||||
|
|
||||||
|
const parsedTemplateId = parsePositiveInt(templateId, 'templateId');
|
||||||
|
if (parsedTemplateId.error) return res.status(400).json({ error: parsedTemplateId.error });
|
||||||
|
|
||||||
|
const parsedPrinterId = parsePositiveInt(printerId, 'printerId');
|
||||||
|
if (parsedPrinterId.error) return res.status(400).json({ error: parsedPrinterId.error });
|
||||||
|
|
||||||
|
const parsedQuantity = parsePositiveInt(quantity, 'quantity');
|
||||||
|
if (parsedQuantity.error) return res.status(400).json({ error: parsedQuantity.error });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const printer = await printerDb.getById(req.user.id, parsedPrinterId.id);
|
||||||
|
if (!printer || printer.company_id !== companyId) {
|
||||||
|
return res.status(404).json({ error: 'Printer not found' });
|
||||||
|
}
|
||||||
|
if (printer.type !== 'zpl') {
|
||||||
|
return res.status(400).json({ error: 'Printer is not a ZPL printer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = await labeltemplateDb.getById(req.user.id, parsedTemplateId.id);
|
||||||
|
if (!template || template.company_id !== companyId) {
|
||||||
|
return res.status(404).json({ error: 'Label template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let article;
|
||||||
|
try {
|
||||||
|
const result = await db.getBySku(req.user.id, sku);
|
||||||
|
article = result?.article;
|
||||||
|
} catch (err) {
|
||||||
|
const status = articleErrorStatus(err);
|
||||||
|
if (status === 500) console.error('Get article by sku failed for print:', err);
|
||||||
|
return res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
if (!article) {
|
||||||
|
return res.status(404).json({ error: 'Article not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and interpolate canvas objects
|
||||||
|
const content = template.content;
|
||||||
|
const objects = Array.isArray(content?.objects)
|
||||||
|
? content.objects
|
||||||
|
: Array.isArray(content)
|
||||||
|
? content
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const interpolatedObjects = objects.map(obj => {
|
||||||
|
const cloned = { ...obj };
|
||||||
|
if (typeof cloned.text === 'string') {
|
||||||
|
cloned.text = interpolateString(cloned.text, article);
|
||||||
|
}
|
||||||
|
if (typeof cloned.code === 'string') {
|
||||||
|
cloned.code = interpolateString(cloned.code, article);
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
});
|
||||||
|
|
||||||
|
const printPayload = await renderLabel({
|
||||||
|
width: template.width,
|
||||||
|
height: template.height,
|
||||||
|
objects: interpolatedObjects,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock network dispatch
|
||||||
|
const printDir = path.join(process.cwd(), 'print_spool', `${printer.address}_${printer.type}`);
|
||||||
|
await fs.mkdir(printDir, { recursive: true });
|
||||||
|
|
||||||
|
for (let i = 0; i < parsedQuantity.id; i++) {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const filename = parsedQuantity.id > 1 ? `${timestamp}_${i + 1}.png` : `${timestamp}.png`;
|
||||||
|
const filePath = path.join(printDir, filename);
|
||||||
|
await fs.writeFile(filePath, printPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Print article label failed:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ export function register(app, basePath = '') {
|
|||||||
* get:
|
* get:
|
||||||
* tags: [LabelTemplate]
|
* tags: [LabelTemplate]
|
||||||
* summary: List label templates
|
* summary: List label templates
|
||||||
* description: Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present.
|
* description: Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present. `content.objects` is omitted to reduce payload size.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
|
* x-markdown: labeltemplate/list.md
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: List of label templates
|
* description: List of label templates
|
||||||
@@ -97,6 +98,7 @@ export function register(app, basePath = '') {
|
|||||||
* description: Retrieves a single label template for the authenticated user's selected company.
|
* description: Retrieves a single label template for the authenticated user's selected company.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
|
* x-markdown: labeltemplate/get.md
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
@@ -169,6 +171,7 @@ export function register(app, basePath = '') {
|
|||||||
* and stored with the template.
|
* and stored with the template.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
|
* x-markdown: labeltemplate/create.md
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
@@ -271,6 +274,7 @@ export function register(app, basePath = '') {
|
|||||||
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
|
* x-markdown: labeltemplate/update.md
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
@@ -320,6 +324,7 @@ export function register(app, basePath = '') {
|
|||||||
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
|
* x-markdown: labeltemplate/update.md
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
@@ -374,6 +379,7 @@ export function register(app, basePath = '') {
|
|||||||
* description: Deletes a label template for the authenticated user's selected company.
|
* description: Deletes a label template for the authenticated user's selected company.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
|
* x-markdown: labeltemplate/delete.md
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: id
|
* name: id
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { renderLabel } from 'react-canvas-label-editor';
|
import { renderLabel } from 'react-canvas-label-editor';
|
||||||
|
|
||||||
const THUMB_SIZE = 50;
|
const THUMB_W = 150;
|
||||||
|
const THUMB_H = 80;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a 50×50 1-bit indexed PNG thumbnail for a label template.
|
* Renders a 150×80 thumbnail for a label template.
|
||||||
*
|
*
|
||||||
* Workflow:
|
* Workflow:
|
||||||
* 1. Render the label at its native pixel resolution via renderLabel().
|
* 1. Render the label at its native pixel resolution via renderLabel().
|
||||||
* 2. Resize to 50×50 using Lanczos3 (covers).
|
* 2. Resize to fit within 150×80 preserving aspect ratio (fit: 'contain').
|
||||||
* 3. Convert to 1-bit PNG:
|
* Letterbox areas remain transparent.
|
||||||
* - Greyscale → threshold at 128 → 1-bit PNG.
|
* 3. Process raw RGBA pixels:
|
||||||
* - Pixels outside the label area (transparent in the RGBA source) are
|
* - Transparent letterbox padding (alpha < 128) → fully transparent (0,0,0,0).
|
||||||
* rendered WHITE (255) so they read as 0 in the final 1-bit output,
|
* - Label pixels (alpha >= 128) → fully opaque (alpha=255) with 4-bit greyscale
|
||||||
* keeping the file as small as possible.
|
* (16 levels: 0, 17, 34, ..., 255) for smooth anti-aliased text and barcodes.
|
||||||
|
* 4. Output as palette PNG with transparency support.
|
||||||
|
*
|
||||||
|
* The result is a "4-bit grey + 1-bit alpha" image:
|
||||||
|
* letterbox area is transparent, and the label area is anti-aliased 4-bit greyscale.
|
||||||
*
|
*
|
||||||
* @param {object} opts
|
* @param {object} opts
|
||||||
* @param {number} opts.width - Label width in pixels.
|
* @param {number} opts.width - Label width in pixels.
|
||||||
* @param {number} opts.height - Label height in pixels.
|
* @param {number} opts.height - Label height in pixels.
|
||||||
* @param {Array} opts.objects - Label editor objects array.
|
* @param {Array} opts.objects - Label editor objects array.
|
||||||
* @returns {Promise<Buffer>} PNG buffer.
|
* @returns {Promise<Buffer>} PNG buffer.
|
||||||
*/
|
*/
|
||||||
@@ -25,19 +30,47 @@ export async function renderThumb({ width, height, objects }) {
|
|||||||
// 1. Render at native resolution → RGBA PNG buffer
|
// 1. Render at native resolution → RGBA PNG buffer
|
||||||
const nativePng = await renderLabel({ width, height, objects });
|
const nativePng = await renderLabel({ width, height, objects });
|
||||||
|
|
||||||
// 2. Resize to 50×50, convert to greyscale, flatten transparency to white,
|
// 2. Resize to fit within 150×80, preserving aspect ratio.
|
||||||
// then threshold to 1-bit and output as PNG.
|
// Transparent background for letterbox padding.
|
||||||
const thumb = await sharp(nativePng)
|
const { data, info } = await sharp(nativePng)
|
||||||
.resize(THUMB_SIZE, THUMB_SIZE, {
|
.ensureAlpha()
|
||||||
|
.resize(THUMB_W, THUMB_H, {
|
||||||
fit: 'contain',
|
fit: 'contain',
|
||||||
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
kernel: sharp.kernel.lanczos3,
|
kernel: sharp.kernel.lanczos3,
|
||||||
})
|
})
|
||||||
.greyscale()
|
.raw()
|
||||||
// Threshold: pixels ≥ 128 → white (0 in 1-bit), < 128 → black (1 in 1-bit)
|
.toBuffer({ resolveWithObject: true });
|
||||||
.threshold(128)
|
|
||||||
// PNG with 1-bit colour depth; compression level 9 for smallest file
|
const { width: w, height: h } = info;
|
||||||
.png({ colours: 2, compressionLevel: 9, adaptiveFiltering: false })
|
|
||||||
|
// 3. Per-pixel: transparent letterbox remains transparent (alpha=0),
|
||||||
|
// label area is opaque (alpha=255) with 4-bit greyscale (16 levels).
|
||||||
|
const rgba = Buffer.alloc(w * h * 4);
|
||||||
|
for (let i = 0; i < w * h; i++) {
|
||||||
|
const base = i * 4;
|
||||||
|
const a = data[base + 3];
|
||||||
|
|
||||||
|
if (a < 128) {
|
||||||
|
// Transparent letterbox area — leave as (0,0,0,0)
|
||||||
|
} else {
|
||||||
|
const factor = a === 255 ? 1 : 255 / a;
|
||||||
|
const r = Math.min(255, Math.round(data[base] * factor));
|
||||||
|
const g = Math.min(255, Math.round(data[base + 1] * factor));
|
||||||
|
const b = Math.min(255, Math.round(data[base + 2] * factor));
|
||||||
|
const grey = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||||
|
// Quantize to 4-bit grey (16 levels: 0, 17, 34, ..., 255)
|
||||||
|
const qGrey = Math.round(grey / 17) * 17;
|
||||||
|
rgba[base] = qGrey;
|
||||||
|
rgba[base + 1] = qGrey;
|
||||||
|
rgba[base + 2] = qGrey;
|
||||||
|
rgba[base + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Encode as palette PNG with transparency support (16 greys + 1 transparent entry).
|
||||||
|
const thumb = await sharp(rgba, { raw: { width: w, height: h, channels: 4 } })
|
||||||
|
.png({ palette: true, colours: 17, compressionLevel: 9 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
return thumb;
|
return thumb;
|
||||||
|
|||||||
Reference in New Issue
Block a user