@thermal-print/escpos 0.3.1-beta.6 → 0.3.1-beta.7

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/styles.js CHANGED
@@ -32,6 +32,7 @@ export function extractViewStyle(style) {
32
32
  borderBottom: style?.borderBottom,
33
33
  borderTop: style?.borderTop,
34
34
  width: style?.width,
35
+ height: style?.height,
35
36
  };
36
37
  }
37
38
  /**
@@ -43,32 +44,67 @@ export function isBold(style) {
43
44
  style.fontWeight === "Helvetica-Bold" ||
44
45
  style.fontFamily === "Helvetica-Bold");
45
46
  }
47
+ export const FONT_LEVEL_SIZES = {
48
+ 0: { font: 1, width: 1, height: 1 },
49
+ 1: { font: 0, width: 1, height: 1 },
50
+ 2: { font: 0, width: 2, height: 2 },
51
+ };
46
52
  /**
47
- * Maps fontSize to ESC/POS font selection + character size multipliers
48
- *
49
- * Three visual levels using standard ESC/POS fonts:
50
- * - Condensada: Font B 1x1 (9x17, 64 cols) — fontSize <= 10
51
- * - Normal: Font A 1x1 (12x24, 48 cols) — fontSize 11-19
52
- * - Expandida: Font A 2x2 (12x24, 24 cols) — fontSize >= 20
53
+ * How many Font A 1x1 columns one character of each level occupies.
54
+ * Font B fits 4/3 of the characters Font A does (56 vs 42 on the MP-4200 TH),
55
+ * so one Font B character is 3/4 of a column.
56
+ */
57
+ export const FONT_LEVEL_COLUMN_UNITS = {
58
+ 0: 0.75,
59
+ 1: 1,
60
+ 2: 2,
61
+ };
62
+ /** The level a document sits at before any per-element fontSize is applied. */
63
+ export function baseFontLevel(fontMode) {
64
+ return fontMode === "small" ? 0 : 1;
65
+ }
66
+ /**
67
+ * How many levels a fontSize moves relative to the document's base level.
53
68
  *
54
- * Note: ESC ! command only supports up to 2x2 character size.
69
+ * fontSize is RELATIVE, not absolute: the same `fontSize: 22` means "one step
70
+ * bigger than this document's body text", so a receipt printed in `fontMode:
71
+ * "small"` does not suddenly jump to double size.
55
72
  */
56
- export function mapFontSizeToESCPOS(fontSize) {
57
- // Default: Font A normal (48 cols)
58
- if (!fontSize)
59
- return { font: 0, width: 1, height: 1 };
60
- // Parse fontSize if it's a string (e.g., "8.28px")
73
+ export function fontSizeLevelDelta(fontSize) {
74
+ if (fontSize === undefined || fontSize === null || fontSize === "")
75
+ return 0;
61
76
  const size = typeof fontSize === "string" ? parseFloat(fontSize) : fontSize;
62
- if (isNaN(size))
63
- return { font: 0, width: 1, height: 1 };
64
- // Condensada: Font B (64 cols) — fontSize <= 10
77
+ if (typeof size !== "number" || Number.isNaN(size))
78
+ return 0;
65
79
  if (size <= 10)
66
- return { font: 1, width: 1, height: 1 };
67
- // Normal: Font A (48 cols) — fontSize 11-19
80
+ return -1;
68
81
  if (size <= 19)
69
- return { font: 0, width: 1, height: 1 };
70
- // Expandida: Font A 2x2 (24 cols) — fontSize >= 20
71
- return { font: 0, width: 2, height: 2 };
82
+ return 0;
83
+ return 1;
84
+ }
85
+ /** Clamps base level + fontSize delta into the three levels ESC ! can express. */
86
+ export function resolveFontLevel(baseLevel, fontSize) {
87
+ const level = baseLevel + fontSizeLevelDelta(fontSize);
88
+ return Math.max(0, Math.min(2, level));
89
+ }
90
+ /**
91
+ * Maps fontSize to the ESC/POS font + character size multipliers of its level.
92
+ *
93
+ * @param fontSize - fontSize from the component style (relative to baseLevel)
94
+ * @param baseLevel - the document's base level (see baseFontLevel)
95
+ */
96
+ export function mapFontSizeToESCPOS(fontSize, baseLevel = 1) {
97
+ return FONT_LEVEL_SIZES[resolveFontLevel(baseLevel, fontSize)];
98
+ }
99
+ /**
100
+ * Characters that fit on one line at `level`, on a paper that fits
101
+ * `baseColumns` characters at `baseLevel`.
102
+ *
103
+ * @param reservedColumns - columns taken by horizontal padding, in Font A units
104
+ */
105
+ export function columnsForLevel(baseColumns, baseLevel, level, reservedColumns = 0) {
106
+ const fontAColumns = baseColumns * FONT_LEVEL_COLUMN_UNITS[baseLevel] - reservedColumns;
107
+ return Math.max(1, Math.floor(fontAColumns / FONT_LEVEL_COLUMN_UNITS[level]));
72
108
  }
73
109
  /**
74
110
  * Maps textAlign to ESC/POS alignment
@@ -81,14 +117,51 @@ export function mapTextAlign(textAlign) {
81
117
  return "left";
82
118
  }
83
119
  /**
84
- * Calculates spacing (margin/padding) in lines
85
- * Approximates pixels to line feeds
120
+ * Points per printed line, and per character column.
121
+ *
122
+ * margin/padding/height arrive in POINTS, the same unit @react-pdf/renderer
123
+ * and @thermal-print/pdf use — a 10pt body line is 12pt tall with the default
124
+ * 1.2 line height, and an 80mm page is 226pt wide for 42 Font A columns.
125
+ * Converting with those two constants is what makes a `marginTop: 12` produce
126
+ * the same visual gap in the PDF preview and on the thermal printer.
127
+ */
128
+ export const POINTS_PER_LINE = 12;
129
+ export const POINTS_PER_COLUMN = 226 / 42;
130
+ /** Parses a size that may arrive as a number or as "12px" / "12pt". */
131
+ export function parseSize(value) {
132
+ if (typeof value === "number")
133
+ return Number.isFinite(value) ? value : 0;
134
+ if (typeof value === "string") {
135
+ const parsed = parseFloat(value);
136
+ return Number.isNaN(parsed) ? 0 : parsed;
137
+ }
138
+ return 0;
139
+ }
140
+ /**
141
+ * Converts a vertical spacing (margin/padding/height) in points to line feeds.
142
+ * Capped at 6 lines so a stray `marginTop: 400` cannot eject a page of paper.
86
143
  */
87
144
  export function calculateSpacing(value) {
88
- if (!value)
145
+ const points = parseSize(value);
146
+ if (points <= 0)
89
147
  return 0;
90
- // Rough approximation: ~20 pixels = 1 line feed
91
- return Math.round(value / 20);
148
+ return Math.min(6, Math.round(points / POINTS_PER_LINE));
149
+ }
150
+ /** Converts a horizontal spacing (padding left/right) in points to columns. */
151
+ export function calculateHorizontalSpacing(value) {
152
+ const points = parseSize(value);
153
+ if (points <= 0)
154
+ return 0;
155
+ return Math.round(points / POINTS_PER_COLUMN);
156
+ }
157
+ /** Parses a percentage width ("30%") into a fraction (0.3). Returns undefined otherwise. */
158
+ export function parsePercentageWidth(width) {
159
+ if (typeof width === "string" && width.includes("%")) {
160
+ const percentage = parseFloat(width);
161
+ if (!Number.isNaN(percentage))
162
+ return percentage / 100;
163
+ }
164
+ return undefined;
92
165
  }
93
166
  /**
94
167
  * Determines if a border is dashed
@@ -126,6 +199,69 @@ export function parseWidth(width, totalWidth) {
126
199
  }
127
200
  return totalWidth;
128
201
  }
202
+ /**
203
+ * Splits the row width across its columns.
204
+ *
205
+ * Columns with an explicit width (fixed or percentage) take it; whatever is
206
+ * left over is shared evenly by the columns that declared none. Before
207
+ * DEV-2390 a column without a width got `parseWidth(undefined) === totalWidth`,
208
+ * so a three-column row asked for three full paper widths and printed each
209
+ * cell padded to the whole line.
210
+ */
211
+ export function distributeColumnWidths(widths, totalWidth) {
212
+ if (widths.length === 0)
213
+ return [];
214
+ const resolved = widths.map((width) => width === undefined ? undefined : Math.max(0, parseWidth(width, totalWidth)));
215
+ const explicitTotal = resolved.reduce((sum, width) => sum + (width ?? 0), 0);
216
+ const autoColumns = resolved.filter((width) => width === undefined).length;
217
+ // A row whose declared widths do not fit the paper — three cells of "50%", or
218
+ // a `width: "100%"` cell with a second cell beside it — cannot be honoured.
219
+ // Splitting it evenly is the only bounded answer: keeping the declared widths
220
+ // leaves the leftover columns at zero, their capacity floors at one character,
221
+ // and a product name then prints one letter per line, metres of paper for one
222
+ // row. Every column also needs at least one character, hence the +autoColumns.
223
+ if (explicitTotal + autoColumns > totalWidth) {
224
+ return evenColumnWidths(resolved.length, totalWidth);
225
+ }
226
+ if (autoColumns === 0)
227
+ return resolved;
228
+ const remaining = Math.max(autoColumns, totalWidth - explicitTotal);
229
+ const share = Math.floor(remaining / autoColumns);
230
+ let leftover = remaining - share * autoColumns;
231
+ return resolved.map((width) => {
232
+ if (width !== undefined)
233
+ return width;
234
+ const extra = leftover > 0 ? 1 : 0;
235
+ leftover -= extra;
236
+ return share + extra;
237
+ });
238
+ }
239
+ /** Splits a row width evenly, giving the remainder to the leftmost columns. */
240
+ function evenColumnWidths(columns, totalWidth) {
241
+ const share = Math.max(1, Math.floor(totalWidth / columns));
242
+ let leftover = Math.max(0, totalWidth - share * columns);
243
+ return Array.from({ length: columns }, () => {
244
+ const extra = leftover > 0 ? 1 : 0;
245
+ leftover -= extra;
246
+ return share + extra;
247
+ });
248
+ }
249
+ /**
250
+ * Splits the free space of a space-between row across its gaps.
251
+ * Every gap gets at least one space; the remainder goes to the leftmost gaps.
252
+ */
253
+ export function distributeGaps(contentWidth, totalWidth, gapCount) {
254
+ if (gapCount <= 0)
255
+ return [];
256
+ const free = Math.max(gapCount, totalWidth - contentWidth);
257
+ const share = Math.floor(free / gapCount);
258
+ let leftover = free - share * gapCount;
259
+ return Array.from({ length: gapCount }, () => {
260
+ const extra = leftover > 0 ? 1 : 0;
261
+ leftover -= extra;
262
+ return share + extra;
263
+ });
264
+ }
129
265
  /**
130
266
  * Aligns text within a column width (using CP860 byte length for accurate padding)
131
267
  */
@@ -162,25 +298,34 @@ export function alignTextInColumn(text, width, align) {
162
298
  * Splits text into multiple lines if it exceeds width
163
299
  */
164
300
  export function wrapText(text, width) {
301
+ if (width <= 0)
302
+ return [text];
165
303
  if (text.length <= width) {
166
304
  return [text];
167
305
  }
168
306
  const lines = [];
169
307
  let currentLine = "";
170
- const words = text.split(" ");
171
- for (const word of words) {
308
+ const pushCurrent = () => {
309
+ if (currentLine) {
310
+ lines.push(currentLine);
311
+ currentLine = "";
312
+ }
313
+ };
314
+ for (const word of text.split(" ")) {
172
315
  if ((currentLine + " " + word).trim().length <= width) {
173
316
  currentLine = (currentLine + " " + word).trim();
317
+ continue;
174
318
  }
175
- else {
176
- if (currentLine) {
177
- lines.push(currentLine);
178
- }
179
- currentLine = word;
319
+ pushCurrent();
320
+ // A single word wider than the column has to be broken, otherwise the
321
+ // printer wraps it wherever it likes and the column grid falls apart.
322
+ let rest = word;
323
+ while (rest.length > width) {
324
+ lines.push(rest.slice(0, width));
325
+ rest = rest.slice(width);
180
326
  }
327
+ currentLine = rest;
181
328
  }
182
- if (currentLine) {
183
- lines.push(currentLine);
184
- }
329
+ pushCurrent();
185
330
  return lines.length > 0 ? lines : [text.substring(0, width)];
186
331
  }
@@ -6,6 +6,14 @@ import { ESCPOSGenerator } from "./generator";
6
6
  */
7
7
  export declare class TreeTraverser {
8
8
  private generator;
9
+ /**
10
+ * Alignment inherited from an ancestor View's `alignItems`, mirroring
11
+ * PDFTraverser.alignmentContext. A Text or Image without its own textAlign
12
+ * follows it.
13
+ */
14
+ private alignmentContext;
15
+ /** Fraction of the line an ancestor View's percentage width allows (0.3 for "30%"). */
16
+ private widthFraction;
9
17
  constructor(generator: ESCPOSGenerator);
10
18
  /**
11
19
  * Traverse the entire tree starting from root
@@ -16,7 +24,11 @@ export declare class TreeTraverser {
16
24
  */
17
25
  private handleDocument;
18
26
  /**
19
- * Handle Page element
27
+ * Handle Page element.
28
+ *
29
+ * In "rico" mode the Page padding becomes the receipt margin, the same way
30
+ * PDFTraverser.handlePage turns it into page margins. Margins arrive in
31
+ * points; the generator clamps them so they can never eat the whole line.
20
32
  */
21
33
  private handlePage;
22
34
  /**
@@ -25,13 +37,18 @@ export declare class TreeTraverser {
25
37
  private handleView;
26
38
  /**
27
39
  * Handle column layout (stacked vertically)
28
- * Adds newlines between sibling elements for proper spacing
29
40
  */
30
41
  private handleColumnLayout;
31
42
  /**
32
43
  * Handle row layout (side-by-side columns)
33
44
  */
34
45
  private handleRowLayout;
46
+ /**
47
+ * Emits one line of a column-grid row, padding every cell to its capacity.
48
+ * With per-cell styling the cells are printed one by one so each can carry
49
+ * its own print mode; otherwise the whole line goes out as a single string.
50
+ */
51
+ private emitGridLine;
35
52
  /**
36
53
  * Find the first Text node in a tree
37
54
  */
@@ -1 +1 @@
1
- {"version":3,"file":"traverser.d.ts","sourceRoot":"","sources":["../src/traverser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAkB;gBAEvB,SAAS,EAAE,eAAe;IAItC;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BvD;;OAEG;YACW,cAAc;IAO5B;;OAEG;YACW,UAAU;IAIxB;;OAEG;YACW,UAAU;IAkBxB;;;OAGG;YACW,kBAAkB;IAchC;;OAEG;YACW,eAAe;IA4G7B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;OAGG;YACW,kBAAkB;IAyChC;;;OAGG;YACW,UAAU;IA6BxB;;OAEG;YACW,cAAc;IAM5B;;OAEG;YACW,WAAW;IA+BzB;;OAEG;YACW,gBAAgB;CAK/B"}
1
+ {"version":3,"file":"traverser.d.ts","sourceRoot":"","sources":["../src/traverser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AA6B9C;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAkB;IAEnC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB,CAAiB;IAEzC,uFAAuF;IACvF,OAAO,CAAC,aAAa,CAAqB;gBAE9B,SAAS,EAAE,eAAe;IAItC;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BvD;;OAEG;YACW,cAAc;IAO5B;;;;;;OAMG;YACW,UAAU;IAyBxB;;OAEG;YACW,UAAU;IAyDxB;;OAEG;YACW,kBAAkB;IAMhC;;OAEG;YACW,eAAe;IA+H7B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAkCpB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;OAGG;YACW,kBAAkB;IAyChC;;;OAGG;YACW,UAAU;IA+BxB;;OAEG;YACW,cAAc;IAM5B;;OAEG;YACW,WAAW;IAuCzB;;OAEG;YACW,gBAAgB;CAK/B"}