@thermal-print/escpos 0.3.1-beta.6 → 0.4.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-adapters/escbematech-adapter.d.ts +6 -0
- package/dist/command-adapters/escbematech-adapter.d.ts.map +1 -1
- package/dist/command-adapters/escbematech-adapter.js +10 -1
- package/dist/command-adapters/types.d.ts +12 -0
- package/dist/command-adapters/types.d.ts.map +1 -1
- package/dist/commands/escbematech.d.ts +2 -0
- package/dist/commands/escbematech.d.ts.map +1 -1
- package/dist/commands/escbematech.js +4 -3
- package/dist/converter.d.ts +19 -2
- package/dist/converter.d.ts.map +1 -1
- package/dist/converter.js +4 -3
- package/dist/generator.d.ts +92 -18
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +245 -40
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/styles.d.ts +76 -10
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +180 -35
- package/dist/traverser.d.ts +19 -2
- package/dist/traverser.d.ts.map +1 -1
- package/dist/traverser.js +184 -95
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
package/dist/generator.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { encodeText } from "./commands/escpos";
|
|
2
|
-
import { extractTextStyle, extractViewStyle, generateDividerLine, isBold, isDashedBorder, mapTextAlign, } from "./styles";
|
|
2
|
+
import { baseFontLevel, calculateSpacing, columnsForLevel, extractTextStyle, extractViewStyle, FONT_LEVEL_COLUMN_UNITS, FONT_LEVEL_SIZES, generateDividerLine, isBold, isDashedBorder, mapTextAlign, resolveFontLevel, wrapText, } from "./styles";
|
|
3
3
|
import { ESCPOSCommandAdapter } from "./command-adapters";
|
|
4
|
+
/**
|
|
5
|
+
* Decodes base64 without Buffer, so the generator also runs in the browser.
|
|
6
|
+
* `atob` is a global in browsers and in Node >= 16.
|
|
7
|
+
*/
|
|
8
|
+
function base64ToBytes(base64) {
|
|
9
|
+
const decode = globalThis.atob;
|
|
10
|
+
if (typeof decode !== "function") {
|
|
11
|
+
throw new Error("base64 decoding requires a global atob() (browser, or Node >= 16)");
|
|
12
|
+
}
|
|
13
|
+
const binary = decode(base64);
|
|
14
|
+
const bytes = new Uint8Array(binary.length);
|
|
15
|
+
for (let i = 0; i < binary.length; i++) {
|
|
16
|
+
bytes[i] = binary.charCodeAt(i);
|
|
17
|
+
}
|
|
18
|
+
return bytes;
|
|
19
|
+
}
|
|
4
20
|
/**
|
|
5
21
|
* Simple buffer implementation for accumulating ESC/POS commands
|
|
6
22
|
*/
|
|
@@ -21,10 +37,15 @@ class ESCPOSBuffer {
|
|
|
21
37
|
this.buffer.push(...bytes);
|
|
22
38
|
}
|
|
23
39
|
/**
|
|
24
|
-
* Get the
|
|
40
|
+
* Get the accumulated bytes.
|
|
41
|
+
*
|
|
42
|
+
* Uint8Array, not Buffer: the generator runs in the browser too (the pdv-web
|
|
43
|
+
* inside the Android WebView), where `Buffer` does not exist. A Node Buffer
|
|
44
|
+
* IS a Uint8Array, so consumers that already fed the result to a serial port
|
|
45
|
+
* or an IPC channel keep working.
|
|
25
46
|
*/
|
|
26
|
-
|
|
27
|
-
return
|
|
47
|
+
toUint8Array() {
|
|
48
|
+
return Uint8Array.from(this.buffer);
|
|
28
49
|
}
|
|
29
50
|
/**
|
|
30
51
|
* Get buffer size
|
|
@@ -45,18 +66,34 @@ class ESCPOSBuffer {
|
|
|
45
66
|
* Pure JavaScript implementation - no Node.js dependencies
|
|
46
67
|
*/
|
|
47
68
|
export class ESCPOSGenerator {
|
|
48
|
-
constructor(paperWidth = 42, encoding = "cp860", debug = false, commandAdapter, fontMode = "medium", compact = false) {
|
|
49
|
-
|
|
69
|
+
constructor(paperWidth = 42, encoding = "cp860", debug = false, commandAdapter, fontMode = "medium", compact = false, styleMode = "legacy") {
|
|
70
|
+
/** Horizontal padding in force, in Font A columns, as a stack of View frames. */
|
|
71
|
+
this.paddingStack = [];
|
|
72
|
+
this.reservedLeft = 0;
|
|
73
|
+
this.reservedRight = 0;
|
|
74
|
+
/** Whether the next addText() starts a new printed line (so it gets the indent). */
|
|
75
|
+
this.atLineStart = true;
|
|
50
76
|
this.compact = compact;
|
|
77
|
+
this.styleMode = styleMode;
|
|
51
78
|
this.buffer = new ESCPOSBuffer();
|
|
52
79
|
// Use provided adapter or default to ESC/POS
|
|
53
80
|
this.commandAdapter = commandAdapter || new ESCPOSCommandAdapter();
|
|
81
|
+
this.baseLevel = baseFontLevel(fontMode);
|
|
82
|
+
this.currentLevel = this.baseLevel;
|
|
83
|
+
// Which ESC/POS font the document starts in. In "rico" it always follows
|
|
84
|
+
// fontMode; in "legacy" an adapter may pin its historical font so the
|
|
85
|
+
// printers already in the field keep receiving the same bytes
|
|
86
|
+
// (ESC/Bematech hardcoded Font B before DEV-2390).
|
|
87
|
+
const fontFromMode = FONT_LEVEL_SIZES[this.baseLevel].font;
|
|
88
|
+
const baseFont = styleMode === "rico"
|
|
89
|
+
? fontFromMode
|
|
90
|
+
: this.commandAdapter.getLegacyDefaultFont?.() ?? fontFromMode;
|
|
54
91
|
this.context = {
|
|
55
92
|
paperWidth,
|
|
56
93
|
basePaperWidth: paperWidth,
|
|
57
94
|
currentAlign: "left",
|
|
58
95
|
currentSize: { width: 1, height: 1 },
|
|
59
|
-
currentFont:
|
|
96
|
+
currentFont: baseFont,
|
|
60
97
|
currentBold: false,
|
|
61
98
|
encoding,
|
|
62
99
|
debug,
|
|
@@ -119,13 +156,126 @@ export class ESCPOSGenerator {
|
|
|
119
156
|
this.applyPrintMode();
|
|
120
157
|
}
|
|
121
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Move the printer to a font level (font + character size in one ESC ! ).
|
|
161
|
+
* Also re-derives the line width, since a Font B line fits 4/3 of the
|
|
162
|
+
* characters a Font A line does and a 2x2 line fits half.
|
|
163
|
+
*/
|
|
164
|
+
setFontLevel(level) {
|
|
165
|
+
this.currentLevel = level;
|
|
166
|
+
const size = FONT_LEVEL_SIZES[level];
|
|
167
|
+
const changed = this.context.currentFont !== size.font ||
|
|
168
|
+
this.context.currentSize.width !== size.width ||
|
|
169
|
+
this.context.currentSize.height !== size.height;
|
|
170
|
+
this.context.currentFont = size.font;
|
|
171
|
+
this.context.currentSize = { width: size.width, height: size.height };
|
|
172
|
+
this.syncPaperWidth();
|
|
173
|
+
if (changed) {
|
|
174
|
+
this.applyPrintMode();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Set font level and bold together, emitting at most one ESC ! command.
|
|
179
|
+
* Used by "rico" mode, where both can change on the same element.
|
|
180
|
+
*/
|
|
181
|
+
setPrintState(level, bold) {
|
|
182
|
+
const size = FONT_LEVEL_SIZES[level];
|
|
183
|
+
const changed = this.context.currentFont !== size.font ||
|
|
184
|
+
this.context.currentSize.width !== size.width ||
|
|
185
|
+
this.context.currentSize.height !== size.height ||
|
|
186
|
+
this.context.currentBold !== bold;
|
|
187
|
+
this.currentLevel = level;
|
|
188
|
+
this.context.currentFont = size.font;
|
|
189
|
+
this.context.currentSize = { width: size.width, height: size.height };
|
|
190
|
+
this.context.currentBold = bold;
|
|
191
|
+
this.syncPaperWidth();
|
|
192
|
+
if (changed) {
|
|
193
|
+
this.applyPrintMode();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Recomputes the usable line width from the current level and padding. */
|
|
197
|
+
syncPaperWidth() {
|
|
198
|
+
this.context.paperWidth = columnsForLevel(this.context.basePaperWidth, this.baseLevel, this.currentLevel, this.reservedLeft + this.reservedRight);
|
|
199
|
+
}
|
|
200
|
+
/** A reserved width in Font A columns, in characters of the CURRENT level. */
|
|
201
|
+
paddingChars(reserved) {
|
|
202
|
+
if (reserved <= 0)
|
|
203
|
+
return 0;
|
|
204
|
+
return Math.round(reserved / FONT_LEVEL_COLUMN_UNITS[this.currentLevel]);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The indent for a line that is starting, in characters of the CURRENT level.
|
|
208
|
+
* A right-aligned line hangs off the right edge, so its left inset is the
|
|
209
|
+
* printer's business and spaces on the left would only push it further in.
|
|
210
|
+
*/
|
|
211
|
+
indentChars() {
|
|
212
|
+
if (this.context.currentAlign === "right")
|
|
213
|
+
return 0;
|
|
214
|
+
return this.paddingChars(this.reservedLeft);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Close a printed line by padding it out to the right inset.
|
|
218
|
+
*
|
|
219
|
+
* ESC a centres or right-aligns whatever bytes reach it, counting the spaces
|
|
220
|
+
* we prepended: a centred line carrying only the left indent lands half that
|
|
221
|
+
* indent to the right of the box it belongs to. Padding the right side as
|
|
222
|
+
* well makes the printer align the text inside the padded box instead of
|
|
223
|
+
* inside the paper. Left-aligned lines need nothing — they already start at
|
|
224
|
+
* the indent.
|
|
225
|
+
*/
|
|
226
|
+
endTextLine() {
|
|
227
|
+
if (this.atLineStart || this.context.currentAlign === "left")
|
|
228
|
+
return;
|
|
229
|
+
const right = this.paddingChars(this.reservedRight);
|
|
230
|
+
if (right > 0) {
|
|
231
|
+
this.buffer.pushArray(encodeText(" ".repeat(right)));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Print a COMPLETE line: the left indent, the text, and the right inset. */
|
|
235
|
+
addTextLine(text) {
|
|
236
|
+
this.addText(text);
|
|
237
|
+
this.endTextLine();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Reserve horizontal padding for the children of a View (or a Page).
|
|
241
|
+
* Widths and wrapping inside the frame shrink, and every line printed while
|
|
242
|
+
* the frame is open is indented by paddingLeft.
|
|
243
|
+
*/
|
|
244
|
+
pushHorizontalPadding(leftColumns, rightColumns) {
|
|
245
|
+
// Never let padding eat more than half the paper — a Page margin arrives in
|
|
246
|
+
// points and a bad value would otherwise leave a one-character column.
|
|
247
|
+
const maxTotal = Math.floor((this.context.basePaperWidth * FONT_LEVEL_COLUMN_UNITS[this.baseLevel]) / 2);
|
|
248
|
+
const available = Math.max(0, maxTotal - this.reservedLeft - this.reservedRight);
|
|
249
|
+
const left = Math.max(0, Math.min(leftColumns, available));
|
|
250
|
+
const right = Math.max(0, Math.min(rightColumns, available - left));
|
|
251
|
+
this.paddingStack.push({ left, right });
|
|
252
|
+
this.reservedLeft += left;
|
|
253
|
+
this.reservedRight += right;
|
|
254
|
+
this.syncPaperWidth();
|
|
255
|
+
}
|
|
256
|
+
/** Release the innermost horizontal padding frame. */
|
|
257
|
+
popHorizontalPadding() {
|
|
258
|
+
const frame = this.paddingStack.pop();
|
|
259
|
+
if (!frame)
|
|
260
|
+
return;
|
|
261
|
+
this.reservedLeft -= frame.left;
|
|
262
|
+
this.reservedRight -= frame.right;
|
|
263
|
+
this.syncPaperWidth();
|
|
264
|
+
}
|
|
265
|
+
/** Feed blank lines for a vertical margin/padding/height given in points. */
|
|
266
|
+
addSpacing(points) {
|
|
267
|
+
const lines = calculateSpacing(points);
|
|
268
|
+
if (lines > 0) {
|
|
269
|
+
this.addLineFeed(lines);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
122
272
|
/**
|
|
123
273
|
* Apply current print mode (size + bold + font) using command adapter.
|
|
124
|
-
*
|
|
274
|
+
* The font comes from context.currentFont, which starts at the fontMode base
|
|
275
|
+
* and only moves in "rico" mode, when a fontSize asks for another level.
|
|
125
276
|
*/
|
|
126
277
|
applyPrintMode() {
|
|
127
|
-
|
|
128
|
-
const useFontB = this.fontMode === "small";
|
|
278
|
+
const useFontB = this.context.currentFont === 1;
|
|
129
279
|
const command = this.commandAdapter.getCharacterSizeCommand(this.context.currentSize.width, this.context.currentSize.height, this.context.currentBold, useFontB);
|
|
130
280
|
this.buffer.pushArray(command);
|
|
131
281
|
}
|
|
@@ -135,6 +285,10 @@ export class ESCPOSGenerator {
|
|
|
135
285
|
*/
|
|
136
286
|
resetFormatting() {
|
|
137
287
|
this.setAlign("left");
|
|
288
|
+
if (this.styleMode === "rico") {
|
|
289
|
+
this.setPrintState(this.baseLevel, false);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
138
292
|
this.setBold(false);
|
|
139
293
|
this.setSize({ width: 1, height: 1 });
|
|
140
294
|
}
|
|
@@ -143,9 +297,10 @@ export class ESCPOSGenerator {
|
|
|
143
297
|
*/
|
|
144
298
|
addText(text) {
|
|
145
299
|
if (text) {
|
|
146
|
-
|
|
147
|
-
const encodedBytes = encodeText(text);
|
|
300
|
+
const indent = this.atLineStart ? this.indentChars() : 0;
|
|
301
|
+
const encodedBytes = encodeText(indent > 0 ? " ".repeat(indent) + text : text);
|
|
148
302
|
this.buffer.pushArray(encodedBytes);
|
|
303
|
+
this.atLineStart = false;
|
|
149
304
|
}
|
|
150
305
|
}
|
|
151
306
|
/**
|
|
@@ -158,6 +313,7 @@ export class ESCPOSGenerator {
|
|
|
158
313
|
}
|
|
159
314
|
const command = this.commandAdapter.getLineFeedCommand(count);
|
|
160
315
|
this.buffer.pushArray(command);
|
|
316
|
+
this.atLineStart = true;
|
|
161
317
|
}
|
|
162
318
|
/**
|
|
163
319
|
* Add line feed using command adapter
|
|
@@ -165,6 +321,7 @@ export class ESCPOSGenerator {
|
|
|
165
321
|
addLineFeed(lines = 1) {
|
|
166
322
|
const command = this.commandAdapter.getLineFeedCommand(lines);
|
|
167
323
|
this.buffer.pushArray(command);
|
|
324
|
+
this.atLineStart = true;
|
|
168
325
|
}
|
|
169
326
|
/**
|
|
170
327
|
* Add divider line.
|
|
@@ -172,7 +329,7 @@ export class ESCPOSGenerator {
|
|
|
172
329
|
*/
|
|
173
330
|
addDivider(dashed = false) {
|
|
174
331
|
this.setAlign("left");
|
|
175
|
-
const line = generateDividerLine(this.
|
|
332
|
+
const line = generateDividerLine(this.getPaperWidth(), dashed);
|
|
176
333
|
this.addText(line);
|
|
177
334
|
this.addNewline();
|
|
178
335
|
}
|
|
@@ -201,8 +358,11 @@ export class ESCPOSGenerator {
|
|
|
201
358
|
* Add image from base64 or data URI
|
|
202
359
|
* Converts image to monochrome bitmap and prints using ESC/POS raster graphics
|
|
203
360
|
* @param source - Base64 string, data URI, or object with uri property
|
|
361
|
+
* @param maxWidthColumns - width budget in characters; comes from the parent
|
|
362
|
+
* View's percentage width. Without it an image inside a `width: "30%"` View
|
|
363
|
+
* printed across the whole paper.
|
|
204
364
|
*/
|
|
205
|
-
async addImage(source) {
|
|
365
|
+
async addImage(source, maxWidthColumns) {
|
|
206
366
|
try {
|
|
207
367
|
// Import Jimp dynamically to avoid loading if not needed
|
|
208
368
|
const { Jimp } = await import("jimp");
|
|
@@ -227,14 +387,16 @@ export class ESCPOSGenerator {
|
|
|
227
387
|
base64Data = base64Match[1];
|
|
228
388
|
}
|
|
229
389
|
}
|
|
230
|
-
// Convert base64 to
|
|
231
|
-
const
|
|
232
|
-
// Load image with Jimp (v1.x API)
|
|
233
|
-
|
|
390
|
+
// Convert base64 to bytes (isomorphic: no Buffer)
|
|
391
|
+
const imageBytes = base64ToBytes(base64Data);
|
|
392
|
+
// Load image with Jimp (v1.x API). Jimp.read accepts an ArrayBuffer, and
|
|
393
|
+
// `imageBytes` owns its buffer exactly, so handing over `.buffer` is a
|
|
394
|
+
// zero-copy pass of the same bytes.
|
|
395
|
+
const image = await Jimp.read(imageBytes.buffer);
|
|
234
396
|
// Get paper width in pixels (assuming 8 dots per mm for 80mm thermal printer)
|
|
235
397
|
// Standard 80mm paper = ~576 pixels at 8 dots/mm (72 dpi)
|
|
236
398
|
// We'll use 384 pixels as max width (48 chars * 8 pixels per char)
|
|
237
|
-
const maxWidth = this.
|
|
399
|
+
const maxWidth = Math.max(1, maxWidthColumns ?? this.getPaperWidth()) * 8;
|
|
238
400
|
// Resize image to fit paper width while maintaining aspect ratio
|
|
239
401
|
if (image.width > maxWidth) {
|
|
240
402
|
await image.resize({ w: maxWidth });
|
|
@@ -278,46 +440,86 @@ export class ESCPOSGenerator {
|
|
|
278
440
|
}
|
|
279
441
|
}
|
|
280
442
|
/**
|
|
281
|
-
* Apply text styles from style object.
|
|
282
|
-
*
|
|
283
|
-
*
|
|
443
|
+
* Apply text styles from a style object.
|
|
444
|
+
*
|
|
445
|
+
* @param style - the element's own style
|
|
446
|
+
* @param options.inheritedAlign - alignment from an ancestor View's alignItems,
|
|
447
|
+
* used only when the element does not set textAlign itself. Before DEV-2390
|
|
448
|
+
* `alignItems: "center"` on a View simply never reached its children.
|
|
449
|
+
* @param options.text - the text about to be printed. In "rico" mode it is
|
|
450
|
+
* used to check whether a 2x2 line actually fits; on 58mm paper a double
|
|
451
|
+
* size title only has ~16 columns, and wrapping it looks worse than
|
|
452
|
+
* printing it one level down.
|
|
284
453
|
*/
|
|
285
|
-
applyTextStyle(style) {
|
|
454
|
+
applyTextStyle(style, options) {
|
|
286
455
|
const textStyle = extractTextStyle(style);
|
|
287
|
-
// Set alignment
|
|
288
|
-
const align =
|
|
456
|
+
// Set alignment — the element's own textAlign wins over the inherited one
|
|
457
|
+
const align = style?.textAlign
|
|
458
|
+
? mapTextAlign(style.textAlign)
|
|
459
|
+
: options?.inheritedAlign ?? "left";
|
|
289
460
|
this.setAlign(align);
|
|
290
|
-
// Set bold
|
|
291
461
|
const bold = isBold(textStyle);
|
|
292
|
-
this.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
462
|
+
if (this.styleMode !== "rico") {
|
|
463
|
+
// Legacy: fontSize is ignored, the whole document prints at fontMode size
|
|
464
|
+
this.setBold(bold);
|
|
465
|
+
this.setSize({ width: 1, height: 1 });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.setPrintState(this.resolveTextLevel(textStyle.fontSize, options?.text), bold);
|
|
296
469
|
}
|
|
297
470
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* - medium: Font A 1x1 (48 cols)
|
|
471
|
+
* The font level a piece of text should print at, honouring the 2x2 fallback.
|
|
472
|
+
* Public so the row layout can size its columns before printing them.
|
|
301
473
|
*/
|
|
302
|
-
|
|
303
|
-
|
|
474
|
+
resolveTextLevel(fontSize, text) {
|
|
475
|
+
const level = resolveFontLevel(this.baseLevel, fontSize);
|
|
476
|
+
if (level === 2 && text) {
|
|
477
|
+
const doubleWidth = columnsForLevel(this.context.basePaperWidth, this.baseLevel, 2, this.reservedLeft + this.reservedRight);
|
|
478
|
+
if (wrapText(text, doubleWidth).length > 1) {
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return level;
|
|
483
|
+
}
|
|
484
|
+
/** The font level currently loaded in the printer. */
|
|
485
|
+
getFontLevel() {
|
|
486
|
+
return this.currentLevel;
|
|
487
|
+
}
|
|
488
|
+
/** The document's base font level (what fontMode asked for). */
|
|
489
|
+
getBaseFontLevel() {
|
|
490
|
+
return this.baseLevel;
|
|
491
|
+
}
|
|
492
|
+
/** Whether richer styling (fontSize, spacing, per-column styles) is enabled. */
|
|
493
|
+
isRichStyleMode() {
|
|
494
|
+
return this.styleMode === "rico";
|
|
304
495
|
}
|
|
305
496
|
/**
|
|
306
497
|
* Apply spacing from view style
|
|
307
498
|
*/
|
|
308
499
|
applyViewSpacing(style, type) {
|
|
309
500
|
const viewStyle = extractViewStyle(style);
|
|
501
|
+
const rich = this.styleMode === "rico";
|
|
502
|
+
const padding = viewStyle.padding;
|
|
503
|
+
const margin = viewStyle.margin;
|
|
504
|
+
// CSS box model order, the same one @thermal-print/pdf follows:
|
|
505
|
+
// margin -> border -> padding -> content -> padding -> border -> margin
|
|
310
506
|
if (type === "before") {
|
|
311
|
-
|
|
507
|
+
if (rich)
|
|
508
|
+
this.addSpacing(viewStyle.marginTop ?? margin);
|
|
312
509
|
if (viewStyle.borderTop) {
|
|
313
510
|
this.addDivider(isDashedBorder(viewStyle.borderTop));
|
|
314
511
|
}
|
|
512
|
+
if (rich)
|
|
513
|
+
this.addSpacing(viewStyle.paddingTop ?? padding);
|
|
315
514
|
}
|
|
316
515
|
else {
|
|
317
|
-
|
|
516
|
+
if (rich)
|
|
517
|
+
this.addSpacing(viewStyle.paddingBottom ?? padding);
|
|
318
518
|
if (viewStyle.borderBottom) {
|
|
319
519
|
this.addDivider(isDashedBorder(viewStyle.borderBottom));
|
|
320
520
|
}
|
|
521
|
+
if (rich)
|
|
522
|
+
this.addSpacing(viewStyle.marginBottom ?? margin);
|
|
321
523
|
}
|
|
322
524
|
}
|
|
323
525
|
/**
|
|
@@ -354,7 +556,7 @@ export class ESCPOSGenerator {
|
|
|
354
556
|
}
|
|
355
557
|
/**
|
|
356
558
|
* Add raw ESC/POS command
|
|
357
|
-
* @param data - Raw
|
|
559
|
+
* @param data - Raw bytes to send to printer
|
|
358
560
|
*/
|
|
359
561
|
addRawCommand(data) {
|
|
360
562
|
this.buffer.pushArray(Array.from(data));
|
|
@@ -363,8 +565,11 @@ export class ESCPOSGenerator {
|
|
|
363
565
|
* Get the final buffer
|
|
364
566
|
*/
|
|
365
567
|
getBuffer() {
|
|
366
|
-
const buffer = this.buffer.
|
|
367
|
-
//
|
|
568
|
+
const buffer = this.buffer.toUint8Array();
|
|
569
|
+
// Defensive: drop leading line feeds so a receipt could never start with
|
|
570
|
+
// blank paper. The buffer opens with the adapter's init command, so in
|
|
571
|
+
// practice nothing is ever stripped here — in particular a marginTop on the
|
|
572
|
+
// first View survives, which the "rico" spacing tests rely on.
|
|
368
573
|
let start = 0;
|
|
369
574
|
while (start < buffer.length && buffer[start] === 0x0a) {
|
|
370
575
|
start++;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ESC/POS command generation and thermal printer control
|
|
5
5
|
*
|
|
6
|
-
* Main API: printNodesToESCPOS(printNode, options) ->
|
|
6
|
+
* Main API: printNodesToESCPOS(printNode, options) -> Uint8Array
|
|
7
7
|
*/
|
|
8
8
|
export { printNodesToESCPOS } from './converter';
|
|
9
|
-
export type { PrintNodeToESCPOSOptions, FontMode } from './converter';
|
|
9
|
+
export type { PrintNodeToESCPOSOptions, FontMode, StyleMode } from './converter';
|
|
10
10
|
export { ESCPOSGenerator } from './generator';
|
|
11
11
|
export { TreeTraverser } from './traverser';
|
|
12
12
|
export type { CommandAdapter, CharacterSize } from './command-adapters/types';
|
|
13
13
|
export { ESCPOSCommandAdapter } from './command-adapters/escpos-adapter';
|
|
14
14
|
export { ESCBematechCommandAdapter } from './command-adapters/escbematech-adapter';
|
|
15
|
-
export { extractTextStyle, extractViewStyle, isBold, mapFontSizeToESCPOS, mapTextAlign, calculateSpacing, isDashedBorder, generateDividerLine, mergeStyles, parseWidth, alignTextInColumn, wrapText, } from './styles';
|
|
15
|
+
export { extractTextStyle, extractViewStyle, isBold, baseFontLevel, fontSizeLevelDelta, resolveFontLevel, mapFontSizeToESCPOS, columnsForLevel, FONT_LEVEL_SIZES, FONT_LEVEL_COLUMN_UNITS, POINTS_PER_LINE, POINTS_PER_COLUMN, mapTextAlign, parseSize, parsePercentageWidth, calculateSpacing, calculateHorizontalSpacing, isDashedBorder, generateDividerLine, mergeStyles, parseWidth, distributeColumnWidths, distributeGaps, alignTextInColumn, wrapText, } from './styles';
|
|
16
|
+
export type { ESCPOSFontSize, FontLevel } from './styles';
|
|
16
17
|
export { encodeCP860 } from './encodings/cp860';
|
|
17
18
|
export type * from './types';
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGjF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AAGnF,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,0BAA0B,EAC1B,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,cAAc,EACd,iBAAiB,EACjB,QAAQ,GACT,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,mBAAmB,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ESC/POS command generation and thermal printer control
|
|
5
5
|
*
|
|
6
|
-
* Main API: printNodesToESCPOS(printNode, options) ->
|
|
6
|
+
* Main API: printNodesToESCPOS(printNode, options) -> Uint8Array
|
|
7
7
|
*/
|
|
8
8
|
// Main conversion function
|
|
9
9
|
export { printNodesToESCPOS } from './converter';
|
|
@@ -13,6 +13,6 @@ export { TreeTraverser } from './traverser';
|
|
|
13
13
|
export { ESCPOSCommandAdapter } from './command-adapters/escpos-adapter';
|
|
14
14
|
export { ESCBematechCommandAdapter } from './command-adapters/escbematech-adapter';
|
|
15
15
|
// Style utilities
|
|
16
|
-
export { extractTextStyle, extractViewStyle, isBold, mapFontSizeToESCPOS, mapTextAlign, calculateSpacing, isDashedBorder, generateDividerLine, mergeStyles, parseWidth, alignTextInColumn, wrapText, } from './styles';
|
|
16
|
+
export { extractTextStyle, extractViewStyle, isBold, baseFontLevel, fontSizeLevelDelta, resolveFontLevel, mapFontSizeToESCPOS, columnsForLevel, FONT_LEVEL_SIZES, FONT_LEVEL_COLUMN_UNITS, POINTS_PER_LINE, POINTS_PER_COLUMN, mapTextAlign, parseSize, parsePercentageWidth, calculateSpacing, calculateHorizontalSpacing, isDashedBorder, generateDividerLine, mergeStyles, parseWidth, distributeColumnWidths, distributeGaps, alignTextInColumn, wrapText, } from './styles';
|
|
17
17
|
// Encodings
|
|
18
18
|
export { encodeCP860 } from './encodings/cp860';
|
package/dist/styles.d.ts
CHANGED
|
@@ -20,25 +20,76 @@ export interface ESCPOSFontSize {
|
|
|
20
20
|
height: number;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* The three visual sizes an ESC ! command can reach on the printers we support.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - Expandida: Font A 2x2 (12x24, 24 cols) — fontSize >= 20
|
|
25
|
+
* Level 0 — Font B 1x1: the condensed font.
|
|
26
|
+
* Level 1 — Font A 1x1: the normal font.
|
|
27
|
+
* Level 2 — Font A 2x2: the double-size font (ESC ! caps out at 2x2).
|
|
29
28
|
*
|
|
30
|
-
*
|
|
29
|
+
* The number of columns each level fits is NOT hardcoded here: it is derived
|
|
30
|
+
* from the paperWidth the caller passes, which is already calibrated per
|
|
31
|
+
* printer/paper (42 for Font A on 80mm, 56 for Font B on 80mm, 32 on 58mm).
|
|
31
32
|
*/
|
|
32
|
-
export
|
|
33
|
+
export type FontLevel = 0 | 1 | 2;
|
|
34
|
+
export declare const FONT_LEVEL_SIZES: Record<FontLevel, ESCPOSFontSize>;
|
|
35
|
+
/**
|
|
36
|
+
* How many Font A 1x1 columns one character of each level occupies.
|
|
37
|
+
* Font B fits 4/3 of the characters Font A does (56 vs 42 on the MP-4200 TH),
|
|
38
|
+
* so one Font B character is 3/4 of a column.
|
|
39
|
+
*/
|
|
40
|
+
export declare const FONT_LEVEL_COLUMN_UNITS: Record<FontLevel, number>;
|
|
41
|
+
/** The level a document sits at before any per-element fontSize is applied. */
|
|
42
|
+
export declare function baseFontLevel(fontMode: "small" | "medium"): FontLevel;
|
|
43
|
+
/**
|
|
44
|
+
* How many levels a fontSize moves relative to the document's base level.
|
|
45
|
+
*
|
|
46
|
+
* fontSize is RELATIVE, not absolute: the same `fontSize: 22` means "one step
|
|
47
|
+
* bigger than this document's body text", so a receipt printed in `fontMode:
|
|
48
|
+
* "small"` does not suddenly jump to double size.
|
|
49
|
+
*/
|
|
50
|
+
export declare function fontSizeLevelDelta(fontSize?: number | string): -1 | 0 | 1;
|
|
51
|
+
/** Clamps base level + fontSize delta into the three levels ESC ! can express. */
|
|
52
|
+
export declare function resolveFontLevel(baseLevel: FontLevel, fontSize?: number | string): FontLevel;
|
|
53
|
+
/**
|
|
54
|
+
* Maps fontSize to the ESC/POS font + character size multipliers of its level.
|
|
55
|
+
*
|
|
56
|
+
* @param fontSize - fontSize from the component style (relative to baseLevel)
|
|
57
|
+
* @param baseLevel - the document's base level (see baseFontLevel)
|
|
58
|
+
*/
|
|
59
|
+
export declare function mapFontSizeToESCPOS(fontSize?: number | string, baseLevel?: FontLevel): ESCPOSFontSize;
|
|
60
|
+
/**
|
|
61
|
+
* Characters that fit on one line at `level`, on a paper that fits
|
|
62
|
+
* `baseColumns` characters at `baseLevel`.
|
|
63
|
+
*
|
|
64
|
+
* @param reservedColumns - columns taken by horizontal padding, in Font A units
|
|
65
|
+
*/
|
|
66
|
+
export declare function columnsForLevel(baseColumns: number, baseLevel: FontLevel, level: FontLevel, reservedColumns?: number): number;
|
|
33
67
|
/**
|
|
34
68
|
* Maps textAlign to ESC/POS alignment
|
|
35
69
|
*/
|
|
36
70
|
export declare function mapTextAlign(textAlign?: string): "left" | "center" | "right";
|
|
37
71
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
72
|
+
* Points per printed line, and per character column.
|
|
73
|
+
*
|
|
74
|
+
* margin/padding/height arrive in POINTS, the same unit @react-pdf/renderer
|
|
75
|
+
* and @thermal-print/pdf use — a 10pt body line is 12pt tall with the default
|
|
76
|
+
* 1.2 line height, and an 80mm page is 226pt wide for 42 Font A columns.
|
|
77
|
+
* Converting with those two constants is what makes a `marginTop: 12` produce
|
|
78
|
+
* the same visual gap in the PDF preview and on the thermal printer.
|
|
40
79
|
*/
|
|
41
|
-
export declare
|
|
80
|
+
export declare const POINTS_PER_LINE = 12;
|
|
81
|
+
export declare const POINTS_PER_COLUMN: number;
|
|
82
|
+
/** Parses a size that may arrive as a number or as "12px" / "12pt". */
|
|
83
|
+
export declare function parseSize(value?: number | string): number;
|
|
84
|
+
/**
|
|
85
|
+
* Converts a vertical spacing (margin/padding/height) in points to line feeds.
|
|
86
|
+
* Capped at 6 lines so a stray `marginTop: 400` cannot eject a page of paper.
|
|
87
|
+
*/
|
|
88
|
+
export declare function calculateSpacing(value?: number | string): number;
|
|
89
|
+
/** Converts a horizontal spacing (padding left/right) in points to columns. */
|
|
90
|
+
export declare function calculateHorizontalSpacing(value?: number | string): number;
|
|
91
|
+
/** Parses a percentage width ("30%") into a fraction (0.3). Returns undefined otherwise. */
|
|
92
|
+
export declare function parsePercentageWidth(width?: string | number): number | undefined;
|
|
42
93
|
/**
|
|
43
94
|
* Determines if a border is dashed
|
|
44
95
|
*/
|
|
@@ -56,6 +107,21 @@ export declare function mergeStyles(...styles: any[]): any;
|
|
|
56
107
|
* Uses Math.round() to minimize rounding errors
|
|
57
108
|
*/
|
|
58
109
|
export declare function parseWidth(width: string | number | undefined, totalWidth: number): number;
|
|
110
|
+
/**
|
|
111
|
+
* Splits the row width across its columns.
|
|
112
|
+
*
|
|
113
|
+
* Columns with an explicit width (fixed or percentage) take it; whatever is
|
|
114
|
+
* left over is shared evenly by the columns that declared none. Before
|
|
115
|
+
* DEV-2390 a column without a width got `parseWidth(undefined) === totalWidth`,
|
|
116
|
+
* so a three-column row asked for three full paper widths and printed each
|
|
117
|
+
* cell padded to the whole line.
|
|
118
|
+
*/
|
|
119
|
+
export declare function distributeColumnWidths(widths: (string | number | undefined)[], totalWidth: number): number[];
|
|
120
|
+
/**
|
|
121
|
+
* Splits the free space of a space-between row across its gaps.
|
|
122
|
+
* Every gap gets at least one space; the remainder goes to the leftmost gaps.
|
|
123
|
+
*/
|
|
124
|
+
export declare function distributeGaps(contentWidth: number, totalWidth: number, gapCount: number): number[];
|
|
59
125
|
/**
|
|
60
126
|
* Aligns text within a column width (using CP860 byte length for accurate padding)
|
|
61
127
|
*/
|
package/dist/styles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,GAAG,GAAG,SAAS,CAOtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,GAAG,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"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"}
|