Compare commits
3 Commits
26adfc5aa9
...
8827d6fc14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8827d6fc14 | ||
|
|
16f70603bc | ||
|
|
ce7f5aad2a |
@@ -240,6 +240,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/printeragent/_id.patch.md`](routes/printeragent/_id.patch.md) | `PATCH /printeragent/{id}` |
|
| [`routes/printeragent/_id.patch.md`](routes/printeragent/_id.patch.md) | `PATCH /printeragent/{id}` |
|
||||||
| [`routes/printerjob.get.md`](routes/printerjob.get.md) | `GET /printerjob` |
|
| [`routes/printerjob.get.md`](routes/printerjob.get.md) | `GET /printerjob` |
|
||||||
| [`routes/printerjob.post.md`](routes/printerjob.post.md) | `POST /printerjob` |
|
| [`routes/printerjob.post.md`](routes/printerjob.post.md) | `POST /printerjob` |
|
||||||
|
| [`routes/printerjob/cancel-all.md`](routes/printerjob/cancel-all.md) | `POST /printerjob/cancel-all` |
|
||||||
| [`routes/printerjob/_id.delete.md`](routes/printerjob/_id.delete.md) | `DELETE /printerjob/{id}` |
|
| [`routes/printerjob/_id.delete.md`](routes/printerjob/_id.delete.md) | `DELETE /printerjob/{id}` |
|
||||||
| [`routes/printerjob/_id.get.md`](routes/printerjob/_id.get.md) | `GET /printerjob/{id}` |
|
| [`routes/printerjob/_id.get.md`](routes/printerjob/_id.get.md) | `GET /printerjob/{id}` |
|
||||||
| [`routes/printerjob/_id/cancel.md`](routes/printerjob/_id/cancel.md) | `POST /printerjob/{id}/cancel` |
|
| [`routes/printerjob/_id/cancel.md`](routes/printerjob/_id/cancel.md) | `POST /printerjob/{id}/cancel` |
|
||||||
@@ -361,7 +362,7 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A
|
|||||||
| `modelconversation-<id>` | `POST /modelconversation/{id}/message` |
|
| `modelconversation-<id>` | `POST /modelconversation/{id}/message` |
|
||||||
| `order` | `DELETE /order/{id}/file/{fileId}`, `POST /order/{id}/pdf`, `DELETE /shipment/{id}`, `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}` |
|
| `order` | `DELETE /order/{id}/file/{fileId}`, `POST /order/{id}/pdf`, `DELETE /shipment/{id}`, `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}` |
|
||||||
| `printer` | `POST /printer`, `PUT /printer/order`, `DELETE /printer/{id}`, `PATCH /printer/{id}`, `PUT /printer/{id}` |
|
| `printer` | `POST /printer`, `PUT /printer/order`, `DELETE /printer/{id}`, `PATCH /printer/{id}`, `PUT /printer/{id}` |
|
||||||
| `printerjob` | `POST /printerjob`, `DELETE /printerjob/{id}`, `POST /printerjob/{id}/cancel`, `POST /zpljob` |
|
| `printerjob` | `POST /printerjob`, `POST /printerjob/cancel-all`, `DELETE /printerjob/{id}`, `POST /printerjob/{id}/cancel`, `POST /zpljob` |
|
||||||
| `propertyname` | `POST /propertyname`, `PUT /propertyname/order`, `DELETE /propertyname/{id}`, `PATCH /propertyname/{id}` |
|
| `propertyname` | `POST /propertyname`, `PUT /propertyname/order`, `DELETE /propertyname/{id}`, `PATCH /propertyname/{id}` |
|
||||||
| `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` |
|
| `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` |
|
||||||
| `shipment` | `DELETE /shipment/{id}`, `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` |
|
| `shipment` | `DELETE /shipment/{id}`, `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 305 API endpoints across all modules.
|
Overview of all 306 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ _none_
|
|||||||
- Params: `query: printeragentId?: int`
|
- Params: `query: printeragentId?: int`
|
||||||
- Returns: `200 { agentprinters: T13[] }`
|
- Returns: `200 { agentprinters: T13[] }`
|
||||||
#### Reorder agent printers — `PUT /agentprinter/order`
|
#### Reorder agent printers — `PUT /agentprinter/order`
|
||||||
- Params: `body: { printeragentId: int, agentprinterIds: int[] }`
|
- Params: `body: { agentprinterIds: int[], printeragentId?: int }`
|
||||||
- Returns: `200 { agentprinterIds: int[] }`
|
- Returns: `200 { agentprinterIds: int[] }`
|
||||||
#### Delete agent printer — `DELETE /agentprinter/{id}`
|
#### Delete agent printer — `DELETE /agentprinter/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
@@ -795,6 +795,9 @@ _none_
|
|||||||
#### Create print job — `POST /printerjob`
|
#### Create print job — `POST /printerjob`
|
||||||
- Params: `body: { agentprinterId: int, data: str, type?: str }`
|
- Params: `body: { agentprinterId: int, data: str, type?: str }`
|
||||||
- Returns: `201 { printerjob: T22 }`
|
- Returns: `201 { printerjob: T22 }`
|
||||||
|
#### Cancel all pending and processing printer jobs — `POST /printerjob/cancel-all`
|
||||||
|
- Params: `body?: { agentprinterId?: int }`
|
||||||
|
- Returns: `200 { success: bool, cancelledCount: int, jobs: T22[] }`
|
||||||
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
|
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { success: bool, deletedJob: T22 }`
|
- Returns: `200 { success: bool, deletedJob: T22 }`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview
|
# Dash API Routes Overview
|
||||||
|
|
||||||
Overview of all 225 API endpoints across all modules.
|
Overview of all 226 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
- Params: `query: printeragentId?: int`
|
- Params: `query: printeragentId?: int`
|
||||||
- Returns: `200 { agentprinters: T9[] }`
|
- Returns: `200 { agentprinters: T9[] }`
|
||||||
#### Reorder agent printers — `PUT /agentprinter/order`
|
#### Reorder agent printers — `PUT /agentprinter/order`
|
||||||
- Params: `body: { printeragentId: int, agentprinterIds: int[] }`
|
- Params: `body: { agentprinterIds: int[], printeragentId?: int }`
|
||||||
- Returns: `200 { agentprinterIds: int[] }`
|
- Returns: `200 { agentprinterIds: int[] }`
|
||||||
#### Delete agent printer — `DELETE /agentprinter/{id}`
|
#### Delete agent printer — `DELETE /agentprinter/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
@@ -574,6 +574,9 @@ _none_
|
|||||||
#### Create print job — `POST /printerjob`
|
#### Create print job — `POST /printerjob`
|
||||||
- Params: `body: { agentprinterId: int, data: str, type?: str }`
|
- Params: `body: { agentprinterId: int, data: str, type?: str }`
|
||||||
- Returns: `201 { printerjob: T16 }`
|
- Returns: `201 { printerjob: T16 }`
|
||||||
|
#### Cancel all pending and processing printer jobs — `POST /printerjob/cancel-all`
|
||||||
|
- Params: `body?: { agentprinterId?: int }`
|
||||||
|
- Returns: `200 { success: bool, cancelledCount: int, jobs: T16[] }`
|
||||||
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
|
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { success: bool, deletedJob: T16 }`
|
- Returns: `200 { success: bool, deletedJob: T16 }`
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Reorders agent printers for a specific printer agent within the selected company
|
|||||||
|
|
||||||
| Field | Required | Type | Description |
|
| Field | Required | Type | Description |
|
||||||
|-------|----------|------|-------------|
|
|-------|----------|------|-------------|
|
||||||
| `printeragentId` | yes | integer | |
|
| `printeragentId` | no | integer | Optional agent id. If omitted, reorders all printers in the company. |
|
||||||
| `agentprinterIds` | yes | integer[] | |
|
| `agentprinterIds` | yes | integer[] | |
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
52
docs/api/routes/printerjob/cancel-all.md
Normal file
52
docs/api/routes/printerjob/cancel-all.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# `POST /printerjob/cancel-all`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Cancel all pending and processing printer jobs
|
||||||
|
|
||||||
|
Cancels all queued or in-flight print jobs for the selected company, optionally filtered by agentprinterId.
|
||||||
|
Notifies connected agents in real time.
|
||||||
|
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
| Field | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `agentprinterId` | no | integer | |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agentprinterId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signals
|
||||||
|
|
||||||
|
On success, emits SSE signal: `printerjob` (see [`GET /events`](../events.md)).
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
All matching jobs cancelled
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"cancelledCount": 1,
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"agentprinterId": 1,
|
||||||
|
"printeragentId": 1,
|
||||||
|
"type": "zpl",
|
||||||
|
"state": "pending",
|
||||||
|
"data": "…",
|
||||||
|
"createdAt": "2026-08-10T00:00:00.000Z",
|
||||||
|
"completedAt": "2026-08-10T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
@@ -215,7 +215,12 @@ const activeAgentEvents = new Map();
|
|||||||
export function notifyAgentJob(agentId, action, job) {
|
export function notifyAgentJob(agentId, action, job) {
|
||||||
const aId = Number(agentId);
|
const aId = Number(agentId);
|
||||||
const conns = activeAgentEvents.get(aId);
|
const conns = activeAgentEvents.get(aId);
|
||||||
if (!conns || conns.size === 0) return false;
|
const count = conns ? conns.size : 0;
|
||||||
|
log.info(`notifyAgentJob: agentId=${aId} action=${action} jobId=${job?.id} activeConnections=${count}`);
|
||||||
|
if (!conns || conns.size === 0) {
|
||||||
|
log.warn(`notifyAgentJob: No active SSE connection for agent ${aId} (jobId=${job?.id}); agent must poll/reconnect`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
action,
|
action,
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
@@ -228,6 +233,7 @@ export function notifyAgentJob(agentId, action, job) {
|
|||||||
if (!clientRes.writableEnded) {
|
if (!clientRes.writableEnded) {
|
||||||
clientRes.write(`event: job\ndata: ${payload}\n\n`);
|
clientRes.write(`event: job\ndata: ${payload}\n\n`);
|
||||||
clientRes.write(`event: printerjob\ndata: ${payload}\n\n`);
|
clientRes.write(`event: printerjob\ndata: ${payload}\n\n`);
|
||||||
|
clientRes.flush?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -399,6 +405,21 @@ async function handleState(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rawState?.job?.id && rawState?.job?.state) {
|
||||||
|
try {
|
||||||
|
const updatedJob = await printerjobDb.updateState(
|
||||||
|
{ agentId: req.agent.id },
|
||||||
|
rawState.job.id,
|
||||||
|
rawState.job.state
|
||||||
|
);
|
||||||
|
if (req.agent?.companyId) {
|
||||||
|
emit('printerjob', { companyId: req.agent.companyId, data: { job: updatedJob } });
|
||||||
|
}
|
||||||
|
} catch (jobErr) {
|
||||||
|
log.debug(`Agent reported job state update for ${rawState.job.id}:`, jobErr?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (rawPrinters && req.agent.companyId) {
|
if (rawPrinters && req.agent.companyId) {
|
||||||
updateAgentPrinters(req.agent.companyId, req.agent.id, req.agent.name, rawPrinters);
|
updateAgentPrinters(req.agent.companyId, req.agent.id, req.agent.name, rawPrinters);
|
||||||
} else if (anyAgentPrinterCreated && req.agent.companyId) {
|
} else if (anyAgentPrinterCreated && req.agent.companyId) {
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import express from 'express';
|
|||||||
import { renderLabel } from 'react-canvas-label-editor';
|
import { renderLabel } from 'react-canvas-label-editor';
|
||||||
import * as db from '../../../db/article.js';
|
import * as db from '../../../db/article.js';
|
||||||
import * as printerDb from '../../../db/printer.js';
|
import * as printerDb from '../../../db/printer.js';
|
||||||
|
import * as agentprinterDb from '../../../db/agentprinter.js';
|
||||||
import * as labeltemplateDb from '../../../db/labeltemplate.js';
|
import * as labeltemplateDb from '../../../db/labeltemplate.js';
|
||||||
|
import * as printerjobDb from '../../../db/printerjob.js';
|
||||||
|
import { emit } from '../../sse.js';
|
||||||
|
import { notifyAgentJob } from '../../agent/index.js';
|
||||||
|
import { pngToZpl } from '../../../services/zplService.js';
|
||||||
|
import { applyZplCopies } from '../zpljob.js';
|
||||||
import {
|
import {
|
||||||
AVIF_BODY_LIMIT,
|
AVIF_BODY_LIMIT,
|
||||||
emitArticleSignal,
|
emitArticleSignal,
|
||||||
@@ -364,7 +370,8 @@ export function register(app, basePath = '') {
|
|||||||
if (skuError) return res.status(400).json({ error: skuError });
|
if (skuError) return res.status(400).json({ error: skuError });
|
||||||
const sku = parsedSku;
|
const sku = parsedSku;
|
||||||
|
|
||||||
const { templateId, printerId, quantity } = req.body ?? {};
|
const rawPrinterId = req.body?.printerId ?? req.body?.agentprinterId;
|
||||||
|
const { templateId, quantity } = req.body ?? {};
|
||||||
|
|
||||||
if (templateId === undefined || templateId === null) {
|
if (templateId === undefined || templateId === null) {
|
||||||
return res.status(400).json({ error: 'templateId is required' });
|
return res.status(400).json({ error: 'templateId is required' });
|
||||||
@@ -373,10 +380,10 @@ export function register(app, basePath = '') {
|
|||||||
return res.status(400).json({ error: 'templateId must be an integer' });
|
return res.status(400).json({ error: 'templateId must be an integer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (printerId === undefined || printerId === null) {
|
if (rawPrinterId === undefined || rawPrinterId === null) {
|
||||||
return res.status(400).json({ error: 'printerId is required' });
|
return res.status(400).json({ error: 'printerId is required' });
|
||||||
}
|
}
|
||||||
if (typeof printerId !== 'number' || !Number.isInteger(printerId) || printerId <= 0) {
|
if (typeof rawPrinterId !== 'number' || !Number.isInteger(rawPrinterId) || rawPrinterId <= 0) {
|
||||||
return res.status(400).json({ error: 'printerId must be an integer' });
|
return res.status(400).json({ error: 'printerId must be an integer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,16 +394,24 @@ export function register(app, basePath = '') {
|
|||||||
return res.status(400).json({ error: 'quantity must be an integer' });
|
return res.status(400).json({ error: 'quantity must be an integer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPrinterId = { id: printerId };
|
const parsedPrinterId = { id: rawPrinterId };
|
||||||
const parsedTemplateId = { id: templateId };
|
const parsedTemplateId = { id: templateId };
|
||||||
const parsedQuantity = { id: quantity };
|
const parsedQuantity = { id: quantity };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const printer = await printerDb.getById(req.user.id, parsedPrinterId.id);
|
let printer = await printerDb.getById(req.user.id, parsedPrinterId.id);
|
||||||
|
let agentPrinter = null;
|
||||||
if (!printer || printer.company_id !== companyId) {
|
if (!printer || printer.company_id !== companyId) {
|
||||||
|
agentPrinter = await agentprinterDb.getById(req.user.id, parsedPrinterId.id);
|
||||||
|
if (agentPrinter) {
|
||||||
|
printer = agentPrinter;
|
||||||
|
} else {
|
||||||
return res.status(404).json({ error: 'Printer not found' });
|
return res.status(404).json({ error: 'Printer not found' });
|
||||||
}
|
}
|
||||||
if (printer.type !== 'zpl') {
|
} else if (printer.printeragentId != null) {
|
||||||
|
agentPrinter = printer;
|
||||||
|
}
|
||||||
|
if (printer.type && printer.type.toLowerCase() !== 'zpl') {
|
||||||
return res.status(400).json({ error: 'Printer is not a ZPL printer' });
|
return res.status(400).json({ error: 'Printer is not a ZPL printer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,8 +433,17 @@ export function register(app, basePath = '') {
|
|||||||
return res.status(404).json({ error: 'Article not found' });
|
return res.status(404).json({ error: 'Article not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and interpolate canvas objects
|
// Extract and interpolate canvas objects or raw ZPL
|
||||||
const content = template.content;
|
const content = template.content;
|
||||||
|
let zplData;
|
||||||
|
|
||||||
|
if (typeof content === 'string' && content.includes('^XA')) {
|
||||||
|
let interpolated = interpolateString(content, article);
|
||||||
|
if (parsedQuantity.id > 1) {
|
||||||
|
interpolated = applyZplCopies(interpolated, parsedQuantity.id);
|
||||||
|
}
|
||||||
|
zplData = interpolated;
|
||||||
|
} else {
|
||||||
const objects = Array.isArray(content?.objects)
|
const objects = Array.isArray(content?.objects)
|
||||||
? content.objects
|
? content.objects
|
||||||
: Array.isArray(content)
|
: Array.isArray(content)
|
||||||
@@ -443,16 +467,33 @@ export function register(app, basePath = '') {
|
|||||||
objects: interpolatedObjects,
|
objects: interpolatedObjects,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock network dispatch
|
zplData = await pngToZpl(printPayload, parsedQuantity.id);
|
||||||
const printDir = path.join(process.cwd(), 'print_spool', `${printer.address}_${printer.type}`);
|
}
|
||||||
|
|
||||||
|
if (agentPrinter) {
|
||||||
|
const job = await printerjobDb.create(req.user.id, {
|
||||||
|
agentprinterId: agentPrinter.id,
|
||||||
|
type: 'zpl',
|
||||||
|
data: zplData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.printeragentId) {
|
||||||
|
notifyAgentJob(job.printeragentId, 'created', job);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('printerjob', { companyId, data: { job } });
|
||||||
|
return res.status(200).json({ success: true, printerjob: job });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for legacy network printers with static IP / address
|
||||||
|
const spoolKey = printer.address ? `${printer.address}_${printer.type}` : `printer_${printer.id}_${printer.type || 'zpl'}`;
|
||||||
|
const printDir = path.join(process.cwd(), 'print_spool', spoolKey);
|
||||||
await fs.mkdir(printDir, { recursive: true });
|
await fs.mkdir(printDir, { recursive: true });
|
||||||
|
|
||||||
for (let i = 0; i < parsedQuantity.id; i++) {
|
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const filename = parsedQuantity.id > 1 ? `${timestamp}_${i + 1}.png` : `${timestamp}.png`;
|
const filename = `${timestamp}.zpl`;
|
||||||
const filePath = path.join(printDir, filename);
|
const filePath = path.join(printDir, filename);
|
||||||
await fs.writeFile(filePath, printPayload);
|
await fs.writeFile(filePath, zplData, 'utf8');
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ success: true });
|
res.status(200).json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
80
src/services/zplService.js
Normal file
80
src/services/zplService.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { createCanvas, loadImage } from 'canvas';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a PNG image buffer into Zebra ZPL II Graphic Field (^GFA) format.
|
||||||
|
* Uses Zebra's alternative compression scheme (ACS run-length encoding on hex)
|
||||||
|
* to minimize payload size over the network and serial/USB ports.
|
||||||
|
*
|
||||||
|
* @param {Buffer} pngBuffer - Rendered 1-bit or RGBA PNG buffer.
|
||||||
|
* @param {number} [quantity=1] - Number of copies to print (^PQ).
|
||||||
|
* @returns {Promise<string>} Full ZPL string ready to send to a Zebra printer.
|
||||||
|
*/
|
||||||
|
export async function pngToZpl(pngBuffer, quantity = 1) {
|
||||||
|
const img = await loadImage(pngBuffer);
|
||||||
|
const canvas = createCanvas(img.width, img.height);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const imgData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
|
const { data, width: w, height: h } = imgData;
|
||||||
|
|
||||||
|
const bytesPerRow = Math.ceil(w / 8);
|
||||||
|
const totalBytes = bytesPerRow * h;
|
||||||
|
const buffer = new Uint8Array(totalBytes);
|
||||||
|
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const idx = (y * w + x) * 4;
|
||||||
|
const r = data[idx];
|
||||||
|
const g = data[idx + 1];
|
||||||
|
const b = data[idx + 2];
|
||||||
|
const a = data[idx + 3];
|
||||||
|
// Ink threshold: non-transparent and dark pixels are black (1)
|
||||||
|
const isBlack = a > 128 && (r * 0.299 + g * 0.587 + b * 0.114) < 128;
|
||||||
|
if (isBlack) {
|
||||||
|
const byteIdx = y * bytesPerRow + Math.floor(x / 8);
|
||||||
|
const bitIdx = 7 - (x % 8);
|
||||||
|
buffer[byteIdx] |= 1 << bitIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexMap = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0').toUpperCase());
|
||||||
|
let hex = '';
|
||||||
|
for (let i = 0; i < totalBytes; i++) {
|
||||||
|
hex += hexMap[buffer[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zebra ACS run-length encoding:
|
||||||
|
// Repeated characters are replaced by letters:
|
||||||
|
// 'G'..'Y' = 1..19 repeats
|
||||||
|
// 'g'..'y' = 20..380 repeats (step 20)
|
||||||
|
// 'z' = 400 repeats
|
||||||
|
let acs = '';
|
||||||
|
const re = /([0-9A-F])\1{2,}/g;
|
||||||
|
let match = re.exec(hex);
|
||||||
|
let offset = 0;
|
||||||
|
while (match) {
|
||||||
|
acs += hex.substring(offset, match.index);
|
||||||
|
let l = match[0].length;
|
||||||
|
while (l >= 400) {
|
||||||
|
acs += 'z';
|
||||||
|
l -= 400;
|
||||||
|
}
|
||||||
|
if (l >= 20) {
|
||||||
|
acs += '_ghijklmnopqrstuvwxy'[Math.floor(l / 20)];
|
||||||
|
l = l % 20;
|
||||||
|
}
|
||||||
|
if (l) {
|
||||||
|
acs += '_GHIJKLMNOPQRSTUVWXY'[l];
|
||||||
|
}
|
||||||
|
acs += match[1];
|
||||||
|
offset = re.lastIndex;
|
||||||
|
match = re.exec(hex);
|
||||||
|
}
|
||||||
|
acs += hex.substring(offset);
|
||||||
|
|
||||||
|
const qty = Number(quantity);
|
||||||
|
const pq = Number.isInteger(qty) && qty > 1 ? `^PQ${qty}\n` : '';
|
||||||
|
return `^XA^PW${w}^LL${h}^LS0^FT0,0^GFA,${totalBytes},${totalBytes},${bytesPerRow},${acs}\n${pq}^XZ`;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user