@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/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 buffer as a Node.js Buffer
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
- toBuffer() {
27
- return Buffer.from(this.buffer);
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
- this.fontMode = fontMode;
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: 0,
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
- * Uses fontMode to determine Font A vs Font B base.
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
- // For "small" mode, use Font B (bit 0 = 1). For "medium", use Font A (bit 0 = 0).
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
- // Encode text to CP860 for Portuguese support
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.context.paperWidth, dashed);
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 buffer
231
- const imageBuffer = Buffer.from(base64Data, "base64");
232
- // Load image with Jimp (v1.x API)
233
- const image = await Jimp.read(imageBuffer);
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.context.paperWidth * 8;
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
- * When fontMode is set, fontSize from components is IGNORED for font selection.
283
- * The fontMode determines the global ESC/POS font (small/medium/large).
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 = mapTextAlign(textStyle.textAlign);
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.setBold(bold);
293
- // Font mode determines the fixed size — fontSize from components is ignored
294
- const size = this.getFontModeSize();
295
- this.setSize(size);
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
- * Get the ESC/POS character size based on the global fontMode.
299
- * - small: Font B 1x1 (64 cols)
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
- getFontModeSize() {
303
- return { width: 1, height: 1 };
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
- // Apply top border
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
- // Apply bottom border
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 buffer data to send to printer
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.toBuffer();
367
- // Remove leading line feeds (0x0A)
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) -> Buffer
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
@@ -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;AAGtE,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,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,QAAQ,GACT,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,mBAAmB,SAAS,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) -> Buffer
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
- * Maps fontSize to ESC/POS font selection + character size multipliers
23
+ * The three visual sizes an ESC ! command can reach on the printers we support.
24
24
  *
25
- * Three visual levels using standard ESC/POS fonts:
26
- * - Condensada: Font B 1x1 (9x17, 64 cols) — fontSize <= 10
27
- * - Normal: Font A 1x1 (12x24, 48 cols) — fontSize 11-19
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
- * Note: ESC ! command only supports up to 2x2 character size.
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 declare function mapFontSizeToESCPOS(fontSize?: number | string): ESCPOSFontSize;
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
- * Calculates spacing (margin/padding) in lines
39
- * Approximates pixels to line feeds
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 function calculateSpacing(value?: number): number;
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
  */
@@ -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,CAoBtD;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAOhD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAiB9E;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAI5E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,MAAM,CAGzE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,CAEjD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAClC,UAAU,EAAE,MAAM,GACjB,MAAM,CAaR;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GACjC,MAAM,CA8BR;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAyB9D"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"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"}