thermalkit 0.1.0 → 0.2.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/README.md CHANGED
@@ -74,9 +74,23 @@ Construct a vertical layout buffer.
74
74
  | `page.text(content, opts?)` | Text at the current baseline. Does **not** auto-advance. |
75
75
  | `page.icon(name, size?, opts?)` | Phosphor icon (must be pre-loaded). |
76
76
  | `page.rule(opts?)` | Horizontal line at the current Y. |
77
+ | `page.dot(x, opts?)` | Filled circle at (x, current Y). Handy for decorative ornaments. |
77
78
  | `page.image(pngBuffer, opts?)` | Embed a raster image (use `preparePoster` for halftone photos). |
78
79
  | `page.push(svgFragment)` | Append raw SVG (escape hatch for custom shapes). |
79
80
  | `page.row(fn, opts?)` | Run `fn` with the cursor preserved — multiple `text` / `icon` calls land on the same baseline. |
81
+ | `page.kv(label, value, opts?)` | Label on the left, value on the right, same baseline. |
82
+
83
+ #### Text alignment shorthand
84
+
85
+ Instead of computing `x = page.width / 2, anchor = 'middle'` for every centered piece of text, use `align`:
86
+
87
+ ```js
88
+ page.text('Centered', { align: 'center', size: 14 });
89
+ page.text('Right!', { align: 'right', weight: 700 });
90
+ page.text('At PAD', { align: 'left' }); // default
91
+ ```
92
+
93
+ Explicit `x` / `anchor` win over `align`.
80
94
 
81
95
  ### Higher-level helpers
82
96
 
package/dist/index.d.ts CHANGED
@@ -19,5 +19,5 @@ export { Page } from './page.js';
19
19
  export { preparePoster } from './poster.js';
20
20
  export { loadIcon, loadIcons } from './icons.js';
21
21
  export { escapeXml, approxWidth, wrapByWidth, } from './svg.js';
22
- export type { PageOptions, TextOptions, IconOptions, RuleOptions, RowOptions, ImageOptions, RenderOptions, PosterOptions, PreparedImage, DitherAlgorithm, FontFamily, Align, } from './types.js';
22
+ export type { PageOptions, TextOptions, IconOptions, RuleOptions, RowOptions, ImageOptions, RenderOptions, PosterOptions, PreparedImage, DotOptions, KvOptions, DitherAlgorithm, FontFamily, Align, } from './types.js';
23
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,SAAS,EACT,WAAW,EACX,WAAW,GACZ,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,UAAU,EACV,KAAK,GACN,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,SAAS,EACT,WAAW,EACX,WAAW,GACZ,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,aAAa,EACb,UAAU,EACV,SAAS,EACT,eAAe,EACf,UAAU,EACV,KAAK,GACN,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -47,10 +47,37 @@ function wrapByWidth(text, maxWidth, size, opts = {}) {
47
47
  if (line) lines.push(line);
48
48
  return lines;
49
49
  }
50
+ /**
51
+ * Resolve the `align` shorthand to (x, anchor). Explicit `x` / `anchor` win.
52
+ */
53
+ function resolveAlign(o, page) {
54
+ let x = o.x;
55
+ let anchor = o.anchor;
56
+ if (x === void 0 || anchor === void 0) switch (o.align) {
57
+ case "center":
58
+ x ??= page.width / 2;
59
+ anchor ??= "middle";
60
+ break;
61
+ case "right":
62
+ x ??= page.width - page.padding;
63
+ anchor ??= "end";
64
+ break;
65
+ case "left":
66
+ x ??= page.padding;
67
+ anchor ??= "start";
68
+ break;
69
+ }
70
+ if (x === void 0) x = page.padding;
71
+ return {
72
+ x,
73
+ anchor
74
+ };
75
+ }
50
76
  function buildTextFragment(s, y, defaultFamily, o = { defaultX: 0 }) {
51
77
  const x = o.x ?? o.defaultX;
52
78
  const yy = o.y ?? y;
53
- const anchor = o.anchor ? ` text-anchor="${o.anchor}"` : "";
79
+ const anchorVal = o.anchor ?? o.defaultAnchor;
80
+ const anchor = anchorVal ? ` text-anchor="${anchorVal}"` : "";
54
81
  const family = resolveFontFamily(o.family, defaultFamily);
55
82
  const size = o.size ?? 14;
56
83
  const weight = o.weight ?? 400;
@@ -109,11 +136,16 @@ var Page = class {
109
136
  /**
110
137
  * Draw text at the current Y baseline (or at `opts.y` if given).
111
138
  * Does NOT advance the cursor — the caller controls vertical rhythm.
139
+ *
140
+ * `opts.align` is a shorthand for the (x, anchor) pair (see TextOptions).
141
+ * Explicit `x` / `anchor` win over `align`.
112
142
  */
113
143
  text(content, opts = {}) {
144
+ const { x, anchor } = resolveAlign(opts, this);
114
145
  this.parts.push(buildTextFragment(content, this.y, this.defaultFontFamily, {
115
146
  ...opts,
116
- defaultX: opts.x ?? this.padding
147
+ defaultX: x,
148
+ defaultAnchor: anchor
117
149
  }));
118
150
  return this;
119
151
  }
@@ -160,6 +192,21 @@ var Page = class {
160
192
  return this;
161
193
  }
162
194
  /**
195
+ * Draw a filled circle (dot) at (x, y) with the given radius.
196
+ * Common use: decorative ornaments under a heading.
197
+ *
198
+ * page.dot(W/2 - 28, { r: 1.5 });
199
+ * page.dot(W/2, { r: 2.5 });
200
+ * page.dot(W/2 + 28, { r: 1.5 });
201
+ */
202
+ dot(x, opts = {}) {
203
+ const cy = opts.y ?? this.y;
204
+ const r = opts.r ?? 2;
205
+ const fill = opts.fill ?? "#000";
206
+ this.parts.push(`<circle cx="${x}" cy="${cy}" r="${r}" fill="${fill}"/>`);
207
+ return this;
208
+ }
209
+ /**
163
210
  * Run `fn` in a "row context": the cursor is preserved on exit, so any
164
211
  * `text()` / `icon()` calls inside land on the same baseline. Optionally
165
212
  * advance the cursor after.
@@ -171,14 +218,34 @@ var Page = class {
171
218
  return this;
172
219
  }
173
220
  /**
221
+ * Key/value row — label on the left, value on the right, same baseline.
222
+ * Cursor doesn't advance (matches the rest of the primitive API).
223
+ *
224
+ * page.kv('Coucher', '21:14');
225
+ * page.kv('Vent', '15 km/h SW', {
226
+ * labelOpts: { weight: 500 },
227
+ * valueOpts: { family: 'georgia', size: 18 },
228
+ * });
229
+ */
230
+ kv(label, value, opts = {}) {
231
+ this.text(label, {
232
+ align: "left",
233
+ ...opts.labelOpts ?? {}
234
+ });
235
+ this.text(value, {
236
+ align: "right",
237
+ ...opts.valueOpts ?? {}
238
+ });
239
+ return this;
240
+ }
241
+ /**
174
242
  * Big centered title with optional italic subtitle below.
175
243
  * Advances the cursor past the block.
176
244
  */
177
245
  title(text, opts = {}) {
178
246
  const size = opts.size ?? 50;
179
247
  this.text(text, {
180
- x: this.width / 2,
181
- anchor: "middle",
248
+ align: "center",
182
249
  family: opts.family ?? "georgia",
183
250
  size,
184
251
  weight: 700,
@@ -187,8 +254,7 @@ var Page = class {
187
254
  if (opts.subtitle) {
188
255
  this.y += Math.round(size * .5);
189
256
  this.text(opts.subtitle, {
190
- x: this.width / 2,
191
- anchor: "middle",
257
+ align: "center",
192
258
  size: 14,
193
259
  style: "italic",
194
260
  spacing: 3
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["s: unknown","family: string | undefined","fallback: string","map: Record<string, string>","text: string","size: number","opts: { family?: string; weight?: number }","maxWidth: number","lines: string[]","s: string","y: number","defaultFamily: string","o: TextOptions & { defaultX: number }","contentX1: number","contentX2: number","o: RuleOptions","content: string","x: number","opts: PageOptions","delta: number","content: string","opts: TextOptions","name: string","opts: IconOptions","opts: RuleOptions","fragment: string","pngBuffer: Buffer","opts: ImageOptions","fn: () => void","opts: RowOptions","text: string","opts: { subtitle?: string; size?: number; family?: string; spacing?: number }","label: string","opts: { icon?: string; iconSize?: number; size?: number; spacing?: number }","size: number","maxWidth: number","opts: RenderOptions","images: Array<{ data: Buffer; x: number; y: number }>","url: string","opts: PosterOptions"],"sources":["../src/svg.ts","../src/page.ts","../src/poster.ts"],"sourcesContent":["/**\n * SVG-fragment helpers used internally by Page. Exposed for callers who want\n * to inject custom shapes via `page.push(rawSvg)`.\n */\nimport type { TextOptions, RuleOptions } from './types.js';\n\nexport const escapeXml = (s: unknown): string =>\n String(s ?? '').replace(/[<>&\"']/g, (c) =>\n ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '\"': '&quot;', \"'\": '&apos;' }[c] as string));\n\n/**\n * Resolve a shorthand `family` value (e.g. `'georgia'`) to a CSS font-family stack.\n * Pass-through anything that looks like a real stack already.\n */\nexport function resolveFontFamily(family: string | undefined, fallback: string): string {\n if (!family) return fallback;\n if (family.includes(',')) return family;\n const map: Record<string, string> = {\n georgia: \"Georgia, 'Times New Roman', serif\",\n serif: \"Georgia, 'Times New Roman', serif\",\n helvetica: 'Helvetica, Arial, sans-serif',\n sans: 'Helvetica, Arial, sans-serif',\n mono: \"Menlo, Consolas, 'Courier New', monospace\",\n };\n return map[family.toLowerCase()] ?? family;\n}\n\n/** Naive proportional-font width estimate, good enough for wrap decisions at 10–24 px. */\nexport function approxWidth(\n text: string,\n size: number,\n opts: { family?: string; weight?: number } = {},\n): number {\n const bold = (opts.weight ?? 400) >= 600;\n const serif = /serif|Georgia/i.test(opts.family ?? '');\n const factor = bold ? 0.56 : (serif ? 0.50 : 0.52);\n return text.length * size * factor;\n}\n\n/** Word-wrap text to fit within a given pixel width. */\nexport function wrapByWidth(\n text: string,\n maxWidth: number,\n size: number,\n opts: { family?: string; weight?: number } = {},\n): string[] {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const lines: string[] = [];\n let line = '';\n for (const w of words) {\n const test = line ? line + ' ' + w : w;\n if (approxWidth(test, size, opts) > maxWidth && line) {\n lines.push(line);\n line = w;\n } else {\n line = test;\n }\n }\n if (line) lines.push(line);\n return lines;\n}\n\nexport function buildTextFragment(\n s: string,\n y: number,\n defaultFamily: string,\n o: TextOptions & { defaultX: number } = { defaultX: 0 },\n): string {\n const x = o.x ?? o.defaultX;\n const yy = o.y ?? y;\n const anchor = o.anchor ? ` text-anchor=\"${o.anchor}\"` : '';\n const family = resolveFontFamily(o.family, defaultFamily);\n const size = o.size ?? 14;\n const weight = o.weight ?? 400;\n const style = o.style ? ` font-style=\"${o.style}\"` : '';\n const sp = o.spacing ? ` letter-spacing=\"${o.spacing}\"` : '';\n const fill = o.fill ?? '#000';\n return `<text x=\"${x}\" y=\"${yy}\"${anchor} font-family=\"${family}\" font-size=\"${size}\" font-weight=\"${weight}\"${style}${sp} fill=\"${fill}\">${escapeXml(s)}</text>`;\n}\n\nexport function buildRuleFragment(\n y: number,\n contentX1: number,\n contentX2: number,\n o: RuleOptions = {},\n): string {\n const x1 = o.x1 ?? contentX1;\n const x2 = o.x2 ?? contentX2;\n const stroke = o.stroke ?? 1;\n const dash = o.dasharray ? ` stroke-dasharray=\"${o.dasharray}\"` : '';\n return `<line x1=\"${x1}\" y1=\"${y}\" x2=\"${x2}\" y2=\"${y}\" stroke=\"#000\" stroke-width=\"${stroke}\"${dash}/>`;\n}\n\nexport function buildIconFragment(\n content: string,\n x: number,\n y: number,\n size: number,\n fill = '#000',\n): string {\n const scale = size / 256; // Phosphor uses a 256x256 viewBox\n return `<g transform=\"translate(${x}, ${y}) scale(${scale.toFixed(4)})\" fill=\"${fill}\">${content}</g>`;\n}\n","/**\n * The Page class — a thermal-receipt-oriented vertical layout builder.\n *\n * Conceptual model: a strip of paper (default 504 px wide for the TM-T88VI)\n * with a Y cursor you push down as you append content. Most calls advance\n * the cursor automatically; reach for `advance()` / `spacer()` when you need\n * manual control.\n */\nimport { loadIcons as loadPhosphorIcons } from './icons.js';\nimport {\n approxWidth,\n wrapByWidth,\n resolveFontFamily,\n escapeXml,\n buildTextFragment,\n buildRuleFragment,\n buildIconFragment,\n} from './svg.js';\nimport type {\n PageOptions,\n TextOptions,\n IconOptions,\n RuleOptions,\n RowOptions,\n ImageOptions,\n RenderOptions,\n} from './types.js';\n\nconst DEFAULT_FONT = 'Helvetica, Arial, sans-serif';\n\nexport class Page {\n /** Output width in pixels. */\n readonly width: number;\n /** Horizontal padding on left/right. */\n readonly padding: number;\n /** Width available for content (width - 2*padding). */\n readonly contentWidth: number;\n /** Current vertical cursor (mutable). */\n y = 0;\n\n private readonly parts: string[] = [];\n private readonly icons: Record<string, string>;\n private readonly defaultFontFamily: string;\n\n constructor(opts: PageOptions = {}) {\n this.width = opts.width ?? 504;\n if (this.width % 8 !== 0) {\n // ESC/POS GS v 0 requires the bitmap width to be a multiple of 8.\n throw new Error(`Page width must be a multiple of 8 (got ${this.width})`);\n }\n this.padding = opts.padding ?? 22;\n this.contentWidth = this.width - 2 * this.padding;\n this.defaultFontFamily = opts.defaultFontFamily ?? DEFAULT_FONT;\n\n if (Array.isArray(opts.icons)) {\n this.icons = loadPhosphorIcons(opts.icons);\n } else if (opts.icons && typeof opts.icons === 'object') {\n this.icons = { ...opts.icons };\n } else {\n this.icons = {};\n }\n }\n\n // ------------------------------------------------------------------\n // Cursor control\n // ------------------------------------------------------------------\n\n /** Move the Y cursor down by `delta` pixels (negative values move up). */\n advance(delta: number): this {\n this.y += delta;\n return this;\n }\n\n /** Alias for `advance` with a documented default of 12 px. */\n spacer(amount = 12): this {\n this.y += amount;\n return this;\n }\n\n // ------------------------------------------------------------------\n // Primitives\n // ------------------------------------------------------------------\n\n /**\n * Draw text at the current Y baseline (or at `opts.y` if given).\n * Does NOT advance the cursor — the caller controls vertical rhythm.\n */\n text(content: string, opts: TextOptions = {}): this {\n this.parts.push(\n buildTextFragment(content, this.y, this.defaultFontFamily, {\n ...opts,\n defaultX: opts.x ?? this.padding,\n }),\n );\n return this;\n }\n\n /**\n * Draw a Phosphor icon (must be pre-loaded into the page's icon map).\n * Icon top-left is placed at (x, y + dy). Common pattern when placing an\n * icon next to text is `dy: -size * 0.8` to visually align with the text baseline.\n */\n icon(name: string, size = 16, opts: IconOptions = {}): this {\n const content = this.icons[name];\n if (!content) return this; // graceful no-op\n const x = opts.x ?? this.padding;\n const dy = opts.dy ?? 0;\n this.parts.push(buildIconFragment(content, x, this.y + dy, size, opts.fill ?? '#000'));\n return this;\n }\n\n /** Horizontal rule at the current Y. */\n rule(opts: RuleOptions = {}): this {\n this.parts.push(\n buildRuleFragment(this.y, this.padding, this.width - this.padding, opts),\n );\n return this;\n }\n\n /** Append a raw SVG fragment at the current Y (escape hatch). */\n push(fragment: string): this {\n this.parts.push(fragment);\n return this;\n }\n\n /**\n * Embed a PNG buffer at (x, y) sized to (width, height). The PNG is\n * encoded as a base64 data URI inside an `<image>` element.\n *\n * NOTE: at render time sharp downsamples the SVG, which would smear an\n * already-dithered raster. For halftone posters use the dedicated\n * `poster()` helper (which composites onto the final raster) — `image()`\n * is for sharp text / vector content already binarised.\n */\n image(pngBuffer: Buffer, opts: ImageOptions = {}): this {\n const x = opts.x ?? this.padding;\n const y = opts.y ?? this.y;\n const w = opts.width;\n const h = opts.height;\n const sizeAttrs = [\n w != null ? `width=\"${w}\"` : '',\n h != null ? `height=\"${h}\"` : '',\n ].filter(Boolean).join(' ');\n const b64 = pngBuffer.toString('base64');\n this.parts.push(\n `<image x=\"${x}\" y=\"${y}\" ${sizeAttrs} preserveAspectRatio=\"xMidYMid meet\" href=\"data:image/png;base64,${b64}\"/>`,\n );\n return this;\n }\n\n // ------------------------------------------------------------------\n // Composition helpers\n // ------------------------------------------------------------------\n\n /**\n * Run `fn` in a \"row context\": the cursor is preserved on exit, so any\n * `text()` / `icon()` calls inside land on the same baseline. Optionally\n * advance the cursor after.\n */\n row(fn: () => void, opts: RowOptions = {}): this {\n const startY = this.y;\n fn();\n this.y = startY + (opts.advance ?? 0);\n return this;\n }\n\n // ------------------------------------------------------------------\n // Higher-level patterns\n // ------------------------------------------------------------------\n\n /**\n * Big centered title with optional italic subtitle below.\n * Advances the cursor past the block.\n */\n title(text: string, opts: { subtitle?: string; size?: number; family?: string; spacing?: number } = {}): this {\n const size = opts.size ?? 50;\n this.text(text, {\n x: this.width / 2,\n anchor: 'middle',\n family: opts.family ?? 'georgia',\n size,\n weight: 700,\n spacing: opts.spacing ?? 10,\n });\n if (opts.subtitle) {\n this.y += Math.round(size * 0.5);\n this.text(opts.subtitle, {\n x: this.width / 2,\n anchor: 'middle',\n size: 14,\n style: 'italic',\n spacing: 3,\n });\n }\n this.y += Math.round(size * 0.5);\n return this;\n }\n\n /**\n * Section header: optional icon to the left + caps-tracked label.\n * Advances the cursor past the block.\n */\n section(\n label: string,\n opts: { icon?: string; iconSize?: number; size?: number; spacing?: number } = {},\n ): this {\n const size = opts.size ?? 14;\n const iconSize = opts.iconSize ?? Math.round(size * 1.8);\n let textX = this.padding;\n if (opts.icon && this.icons[opts.icon]) {\n this.icon(opts.icon, iconSize, { dy: -Math.round(iconSize * 0.75) });\n textX = this.padding + iconSize + 8;\n }\n this.text(label, {\n x: textX,\n size,\n weight: 700,\n spacing: 3,\n });\n this.y += Math.round(size * 1.6);\n return this;\n }\n\n // ------------------------------------------------------------------\n // Text-measurement helpers (proxied to the standalone fns)\n // ------------------------------------------------------------------\n\n approxWidth(text: string, size: number, opts: TextOptions = {}): number {\n return approxWidth(text, size, { family: resolveFontFamily(opts.family, this.defaultFontFamily), weight: opts.weight });\n }\n\n wrapByWidth(text: string, maxWidth: number, size: number, opts: TextOptions = {}): string[] {\n return wrapByWidth(text, maxWidth, size, { family: resolveFontFamily(opts.family, this.defaultFontFamily), weight: opts.weight });\n }\n\n // Re-exposed for users assembling raw fragments via `page.push(...)`.\n escapeXml = escapeXml;\n\n // ------------------------------------------------------------------\n // Serialization\n // ------------------------------------------------------------------\n\n /** Serialise to a complete SVG document at the current Y as height. */\n toSvg(): string {\n const h = this.y;\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${this.width}\" height=\"${h}\" viewBox=\"0 0 ${this.width} ${h}\">\n<rect width=\"${this.width}\" height=\"${h}\" fill=\"#fff\"/>\n${this.parts.join('\\n')}\n</svg>`;\n }\n\n /**\n * Rasterise the page to a 1-bit-style PNG, ready for the thermal printer.\n * Renders at high density (so anti-aliasing produces good edges), downsamples\n * to the final width, then thresholds to pure B&W.\n */\n async toPng(opts: RenderOptions = {}): Promise<Buffer> {\n const { default: sharp } = await import('sharp');\n return sharp(Buffer.from(this.toSvg()), { density: opts.density ?? 240 })\n .resize({ width: opts.width ?? this.width, fit: 'inside' })\n .flatten({ background: '#ffffff' })\n .greyscale()\n .threshold(opts.threshold ?? 140)\n .png()\n .toBuffer();\n }\n\n /**\n * Like `toPng`, but composites a list of already-dithered raster images\n * onto the thresholded base at exact pixel positions. Use for halftone\n * posters that would otherwise be smeared by the master rasterisation.\n */\n async toPngWithImages(\n images: Array<{ data: Buffer; x: number; y: number }>,\n opts: RenderOptions = {},\n ): Promise<Buffer> {\n const { default: sharp } = await import('sharp');\n const base = await this.toPng(opts);\n if (!images.length) return base;\n return sharp(base)\n .composite(images.map(im => ({\n input: im.data,\n left: Math.round(im.x),\n top: Math.round(im.y),\n })))\n .png()\n .toBuffer();\n }\n\n /** Inspect the accumulated SVG fragments (for testing/debugging). */\n get fragments(): readonly string[] {\n return this.parts;\n }\n}\n","/**\n * Halftone poster pipeline: URL → 1-bit dithered PNG, ready to composite onto\n * a rasterised page.\n *\n * Steps:\n * 1. Download bytes (with optional User-Agent).\n * 2. Resize to the exact target display size (no scaling during render).\n * 3. CLAHE for local contrast (rescues dark posters), then normalize.\n * 4. Dither (Atkinson by default).\n * 5. Re-encode as a clean greyscale PNG with exactly 0 / 255 values.\n *\n * The output PNG is at the EXACT pixel dimensions you'll display it at on\n * the page — no further scaling. That's the trick that keeps the dot\n * pattern crisp through the master rasterisation step (use `page.toPngWithImages`).\n */\nimport { dither as runDither } from './dither.js';\nimport type { PosterOptions, PreparedImage } from './types.js';\n\nconst DEFAULT_USER_AGENT = 'thermalkit/0.1';\n\nexport async function preparePoster(\n url: string,\n opts: PosterOptions = {},\n): Promise<PreparedImage> {\n const { default: sharp } = await import('sharp');\n\n const res = await fetch(url, {\n headers: { 'User-Agent': opts.userAgent ?? DEFAULT_USER_AGENT },\n });\n if (!res.ok) throw new Error(`Poster HTTP ${res.status} (${url})`);\n const buf = Buffer.from(await res.arrayBuffer());\n\n // Compute the exact target H/W from the source aspect ratio.\n const meta = await sharp(buf).metadata();\n const ratio = (meta.height ?? 1) / (meta.width ?? 1);\n const displayW = opts.width ?? 100;\n const displayH = Math.round(displayW * ratio);\n\n let pipeline = sharp(buf)\n .resize({ width: displayW, height: displayH, fit: 'fill' })\n .greyscale();\n\n const contrast = opts.contrast ?? 'clahe';\n if (contrast === 'clahe') {\n pipeline = pipeline.clahe({ width: 8, height: 8, maxSlope: 3 }).normalize();\n } else if (contrast === 'normalize') {\n pipeline = pipeline.normalize();\n }\n\n const { data: grey, info } = await pipeline.raw().toBuffer({ resolveWithObject: true });\n\n const algo = opts.dither ?? 'atkinson';\n const dithered = runDither(algo, grey, info.width, info.height);\n\n const { data, info: outInfo } = await sharp(Buffer.from(dithered.buffer), {\n raw: { width: info.width, height: info.height, channels: 1 },\n })\n .png({ compressionLevel: 9 })\n .toBuffer({ resolveWithObject: true });\n\n return { data, width: outInfo.width, height: outInfo.height };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["s: unknown","family: string | undefined","fallback: string","map: Record<string, string>","text: string","size: number","opts: { family?: string; weight?: number }","maxWidth: number","lines: string[]","o: TextOptions","page: { width: number; padding: number }","s: string","y: number","defaultFamily: string","o: TextOptions & { defaultX: number; defaultAnchor?: 'start' | 'middle' | 'end' }","contentX1: number","contentX2: number","o: RuleOptions","content: string","x: number","opts: PageOptions","delta: number","content: string","opts: TextOptions","name: string","opts: IconOptions","opts: RuleOptions","fragment: string","pngBuffer: Buffer","opts: ImageOptions","x: number","opts: DotOptions","fn: () => void","opts: RowOptions","label: string","value: string","opts: KvOptions","text: string","opts: { subtitle?: string; size?: number; family?: string; spacing?: number }","opts: { icon?: string; iconSize?: number; size?: number; spacing?: number }","size: number","maxWidth: number","opts: RenderOptions","images: Array<{ data: Buffer; x: number; y: number }>","url: string","opts: PosterOptions"],"sources":["../src/svg.ts","../src/page.ts","../src/poster.ts"],"sourcesContent":["/**\n * SVG-fragment helpers used internally by Page. Exposed for callers who want\n * to inject custom shapes via `page.push(rawSvg)`.\n */\nimport type { TextOptions, RuleOptions } from './types.js';\n\nexport const escapeXml = (s: unknown): string =>\n String(s ?? '').replace(/[<>&\"']/g, (c) =>\n ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '\"': '&quot;', \"'\": '&apos;' }[c] as string));\n\n/**\n * Resolve a shorthand `family` value (e.g. `'georgia'`) to a CSS font-family stack.\n * Pass-through anything that looks like a real stack already.\n */\nexport function resolveFontFamily(family: string | undefined, fallback: string): string {\n if (!family) return fallback;\n if (family.includes(',')) return family;\n const map: Record<string, string> = {\n georgia: \"Georgia, 'Times New Roman', serif\",\n serif: \"Georgia, 'Times New Roman', serif\",\n helvetica: 'Helvetica, Arial, sans-serif',\n sans: 'Helvetica, Arial, sans-serif',\n mono: \"Menlo, Consolas, 'Courier New', monospace\",\n };\n return map[family.toLowerCase()] ?? family;\n}\n\n/** Naive proportional-font width estimate, good enough for wrap decisions at 10–24 px. */\nexport function approxWidth(\n text: string,\n size: number,\n opts: { family?: string; weight?: number } = {},\n): number {\n const bold = (opts.weight ?? 400) >= 600;\n const serif = /serif|Georgia/i.test(opts.family ?? '');\n const factor = bold ? 0.56 : (serif ? 0.50 : 0.52);\n return text.length * size * factor;\n}\n\n/** Word-wrap text to fit within a given pixel width. */\nexport function wrapByWidth(\n text: string,\n maxWidth: number,\n size: number,\n opts: { family?: string; weight?: number } = {},\n): string[] {\n const words = String(text).split(/\\s+/).filter(Boolean);\n const lines: string[] = [];\n let line = '';\n for (const w of words) {\n const test = line ? line + ' ' + w : w;\n if (approxWidth(test, size, opts) > maxWidth && line) {\n lines.push(line);\n line = w;\n } else {\n line = test;\n }\n }\n if (line) lines.push(line);\n return lines;\n}\n\n/**\n * Resolve the `align` shorthand to (x, anchor). Explicit `x` / `anchor` win.\n */\nexport function resolveAlign(\n o: TextOptions,\n page: { width: number; padding: number },\n): { x: number; anchor: 'start' | 'middle' | 'end' | undefined } {\n let x = o.x;\n let anchor = o.anchor;\n if (x === undefined || anchor === undefined) {\n switch (o.align) {\n case 'center':\n x ??= page.width / 2;\n anchor ??= 'middle';\n break;\n case 'right':\n x ??= page.width - page.padding;\n anchor ??= 'end';\n break;\n case 'left':\n x ??= page.padding;\n anchor ??= 'start';\n break;\n }\n }\n if (x === undefined) x = page.padding;\n return { x, anchor };\n}\n\nexport function buildTextFragment(\n s: string,\n y: number,\n defaultFamily: string,\n o: TextOptions & { defaultX: number; defaultAnchor?: 'start' | 'middle' | 'end' } = { defaultX: 0 },\n): string {\n const x = o.x ?? o.defaultX;\n const yy = o.y ?? y;\n const anchorVal = o.anchor ?? o.defaultAnchor;\n const anchor = anchorVal ? ` text-anchor=\"${anchorVal}\"` : '';\n const family = resolveFontFamily(o.family, defaultFamily);\n const size = o.size ?? 14;\n const weight = o.weight ?? 400;\n const style = o.style ? ` font-style=\"${o.style}\"` : '';\n const sp = o.spacing ? ` letter-spacing=\"${o.spacing}\"` : '';\n const fill = o.fill ?? '#000';\n return `<text x=\"${x}\" y=\"${yy}\"${anchor} font-family=\"${family}\" font-size=\"${size}\" font-weight=\"${weight}\"${style}${sp} fill=\"${fill}\">${escapeXml(s)}</text>`;\n}\n\nexport function buildRuleFragment(\n y: number,\n contentX1: number,\n contentX2: number,\n o: RuleOptions = {},\n): string {\n const x1 = o.x1 ?? contentX1;\n const x2 = o.x2 ?? contentX2;\n const stroke = o.stroke ?? 1;\n const dash = o.dasharray ? ` stroke-dasharray=\"${o.dasharray}\"` : '';\n return `<line x1=\"${x1}\" y1=\"${y}\" x2=\"${x2}\" y2=\"${y}\" stroke=\"#000\" stroke-width=\"${stroke}\"${dash}/>`;\n}\n\nexport function buildIconFragment(\n content: string,\n x: number,\n y: number,\n size: number,\n fill = '#000',\n): string {\n const scale = size / 256; // Phosphor uses a 256x256 viewBox\n return `<g transform=\"translate(${x}, ${y}) scale(${scale.toFixed(4)})\" fill=\"${fill}\">${content}</g>`;\n}\n","/**\n * The Page class — a thermal-receipt-oriented vertical layout builder.\n *\n * Conceptual model: a strip of paper (default 504 px wide for the TM-T88VI)\n * with a Y cursor you push down as you append content. Most calls advance\n * the cursor automatically; reach for `advance()` / `spacer()` when you need\n * manual control.\n */\nimport { loadIcons as loadPhosphorIcons } from './icons.js';\nimport {\n approxWidth,\n wrapByWidth,\n resolveFontFamily,\n resolveAlign,\n escapeXml,\n buildTextFragment,\n buildRuleFragment,\n buildIconFragment,\n} from './svg.js';\nimport type {\n PageOptions,\n TextOptions,\n IconOptions,\n RuleOptions,\n RowOptions,\n ImageOptions,\n RenderOptions,\n DotOptions,\n KvOptions,\n} from './types.js';\n\nconst DEFAULT_FONT = 'Helvetica, Arial, sans-serif';\n\nexport class Page {\n /** Output width in pixels. */\n readonly width: number;\n /** Horizontal padding on left/right. */\n readonly padding: number;\n /** Width available for content (width - 2*padding). */\n readonly contentWidth: number;\n /** Current vertical cursor (mutable). */\n y = 0;\n\n private readonly parts: string[] = [];\n private readonly icons: Record<string, string>;\n private readonly defaultFontFamily: string;\n\n constructor(opts: PageOptions = {}) {\n this.width = opts.width ?? 504;\n if (this.width % 8 !== 0) {\n // ESC/POS GS v 0 requires the bitmap width to be a multiple of 8.\n throw new Error(`Page width must be a multiple of 8 (got ${this.width})`);\n }\n this.padding = opts.padding ?? 22;\n this.contentWidth = this.width - 2 * this.padding;\n this.defaultFontFamily = opts.defaultFontFamily ?? DEFAULT_FONT;\n\n if (Array.isArray(opts.icons)) {\n this.icons = loadPhosphorIcons(opts.icons);\n } else if (opts.icons && typeof opts.icons === 'object') {\n this.icons = { ...opts.icons };\n } else {\n this.icons = {};\n }\n }\n\n // ------------------------------------------------------------------\n // Cursor control\n // ------------------------------------------------------------------\n\n /** Move the Y cursor down by `delta` pixels (negative values move up). */\n advance(delta: number): this {\n this.y += delta;\n return this;\n }\n\n /** Alias for `advance` with a documented default of 12 px. */\n spacer(amount = 12): this {\n this.y += amount;\n return this;\n }\n\n // ------------------------------------------------------------------\n // Primitives\n // ------------------------------------------------------------------\n\n /**\n * Draw text at the current Y baseline (or at `opts.y` if given).\n * Does NOT advance the cursor — the caller controls vertical rhythm.\n *\n * `opts.align` is a shorthand for the (x, anchor) pair (see TextOptions).\n * Explicit `x` / `anchor` win over `align`.\n */\n text(content: string, opts: TextOptions = {}): this {\n const { x, anchor } = resolveAlign(opts, this);\n this.parts.push(\n buildTextFragment(content, this.y, this.defaultFontFamily, {\n ...opts,\n defaultX: x,\n defaultAnchor: anchor,\n }),\n );\n return this;\n }\n\n /**\n * Draw a Phosphor icon (must be pre-loaded into the page's icon map).\n * Icon top-left is placed at (x, y + dy). Common pattern when placing an\n * icon next to text is `dy: -size * 0.8` to visually align with the text baseline.\n */\n icon(name: string, size = 16, opts: IconOptions = {}): this {\n const content = this.icons[name];\n if (!content) return this; // graceful no-op\n const x = opts.x ?? this.padding;\n const dy = opts.dy ?? 0;\n this.parts.push(buildIconFragment(content, x, this.y + dy, size, opts.fill ?? '#000'));\n return this;\n }\n\n /** Horizontal rule at the current Y. */\n rule(opts: RuleOptions = {}): this {\n this.parts.push(\n buildRuleFragment(this.y, this.padding, this.width - this.padding, opts),\n );\n return this;\n }\n\n /** Append a raw SVG fragment at the current Y (escape hatch). */\n push(fragment: string): this {\n this.parts.push(fragment);\n return this;\n }\n\n /**\n * Embed a PNG buffer at (x, y) sized to (width, height). The PNG is\n * encoded as a base64 data URI inside an `<image>` element.\n *\n * NOTE: at render time sharp downsamples the SVG, which would smear an\n * already-dithered raster. For halftone posters use the dedicated\n * `poster()` helper (which composites onto the final raster) — `image()`\n * is for sharp text / vector content already binarised.\n */\n image(pngBuffer: Buffer, opts: ImageOptions = {}): this {\n const x = opts.x ?? this.padding;\n const y = opts.y ?? this.y;\n const w = opts.width;\n const h = opts.height;\n const sizeAttrs = [\n w != null ? `width=\"${w}\"` : '',\n h != null ? `height=\"${h}\"` : '',\n ].filter(Boolean).join(' ');\n const b64 = pngBuffer.toString('base64');\n this.parts.push(\n `<image x=\"${x}\" y=\"${y}\" ${sizeAttrs} preserveAspectRatio=\"xMidYMid meet\" href=\"data:image/png;base64,${b64}\"/>`,\n );\n return this;\n }\n\n /**\n * Draw a filled circle (dot) at (x, y) with the given radius.\n * Common use: decorative ornaments under a heading.\n *\n * page.dot(W/2 - 28, { r: 1.5 });\n * page.dot(W/2, { r: 2.5 });\n * page.dot(W/2 + 28, { r: 1.5 });\n */\n dot(x: number, opts: DotOptions = {}): this {\n const cy = opts.y ?? this.y;\n const r = opts.r ?? 2;\n const fill = opts.fill ?? '#000';\n this.parts.push(`<circle cx=\"${x}\" cy=\"${cy}\" r=\"${r}\" fill=\"${fill}\"/>`);\n return this;\n }\n\n // ------------------------------------------------------------------\n // Composition helpers\n // ------------------------------------------------------------------\n\n /**\n * Run `fn` in a \"row context\": the cursor is preserved on exit, so any\n * `text()` / `icon()` calls inside land on the same baseline. Optionally\n * advance the cursor after.\n */\n row(fn: () => void, opts: RowOptions = {}): this {\n const startY = this.y;\n fn();\n this.y = startY + (opts.advance ?? 0);\n return this;\n }\n\n /**\n * Key/value row — label on the left, value on the right, same baseline.\n * Cursor doesn't advance (matches the rest of the primitive API).\n *\n * page.kv('Coucher', '21:14');\n * page.kv('Vent', '15 km/h SW', {\n * labelOpts: { weight: 500 },\n * valueOpts: { family: 'georgia', size: 18 },\n * });\n */\n kv(label: string, value: string, opts: KvOptions = {}): this {\n this.text(label, { align: 'left', ...(opts.labelOpts ?? {}) });\n this.text(value, { align: 'right', ...(opts.valueOpts ?? {}) });\n return this;\n }\n\n // ------------------------------------------------------------------\n // Higher-level patterns\n // ------------------------------------------------------------------\n\n /**\n * Big centered title with optional italic subtitle below.\n * Advances the cursor past the block.\n */\n title(text: string, opts: { subtitle?: string; size?: number; family?: string; spacing?: number } = {}): this {\n const size = opts.size ?? 50;\n this.text(text, {\n align: 'center',\n family: opts.family ?? 'georgia',\n size,\n weight: 700,\n spacing: opts.spacing ?? 10,\n });\n if (opts.subtitle) {\n this.y += Math.round(size * 0.5);\n this.text(opts.subtitle, {\n align: 'center',\n size: 14,\n style: 'italic',\n spacing: 3,\n });\n }\n this.y += Math.round(size * 0.5);\n return this;\n }\n\n /**\n * Section header: optional icon to the left + caps-tracked label.\n * Advances the cursor past the block.\n */\n section(\n label: string,\n opts: { icon?: string; iconSize?: number; size?: number; spacing?: number } = {},\n ): this {\n const size = opts.size ?? 14;\n const iconSize = opts.iconSize ?? Math.round(size * 1.8);\n let textX = this.padding;\n if (opts.icon && this.icons[opts.icon]) {\n this.icon(opts.icon, iconSize, { dy: -Math.round(iconSize * 0.75) });\n textX = this.padding + iconSize + 8;\n }\n this.text(label, {\n x: textX,\n size,\n weight: 700,\n spacing: 3,\n });\n this.y += Math.round(size * 1.6);\n return this;\n }\n\n // ------------------------------------------------------------------\n // Text-measurement helpers (proxied to the standalone fns)\n // ------------------------------------------------------------------\n\n approxWidth(text: string, size: number, opts: TextOptions = {}): number {\n return approxWidth(text, size, { family: resolveFontFamily(opts.family, this.defaultFontFamily), weight: opts.weight });\n }\n\n wrapByWidth(text: string, maxWidth: number, size: number, opts: TextOptions = {}): string[] {\n return wrapByWidth(text, maxWidth, size, { family: resolveFontFamily(opts.family, this.defaultFontFamily), weight: opts.weight });\n }\n\n // Re-exposed for users assembling raw fragments via `page.push(...)`.\n escapeXml = escapeXml;\n\n // ------------------------------------------------------------------\n // Serialization\n // ------------------------------------------------------------------\n\n /** Serialise to a complete SVG document at the current Y as height. */\n toSvg(): string {\n const h = this.y;\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${this.width}\" height=\"${h}\" viewBox=\"0 0 ${this.width} ${h}\">\n<rect width=\"${this.width}\" height=\"${h}\" fill=\"#fff\"/>\n${this.parts.join('\\n')}\n</svg>`;\n }\n\n /**\n * Rasterise the page to a 1-bit-style PNG, ready for the thermal printer.\n * Renders at high density (so anti-aliasing produces good edges), downsamples\n * to the final width, then thresholds to pure B&W.\n */\n async toPng(opts: RenderOptions = {}): Promise<Buffer> {\n const { default: sharp } = await import('sharp');\n return sharp(Buffer.from(this.toSvg()), { density: opts.density ?? 240 })\n .resize({ width: opts.width ?? this.width, fit: 'inside' })\n .flatten({ background: '#ffffff' })\n .greyscale()\n .threshold(opts.threshold ?? 140)\n .png()\n .toBuffer();\n }\n\n /**\n * Like `toPng`, but composites a list of already-dithered raster images\n * onto the thresholded base at exact pixel positions. Use for halftone\n * posters that would otherwise be smeared by the master rasterisation.\n */\n async toPngWithImages(\n images: Array<{ data: Buffer; x: number; y: number }>,\n opts: RenderOptions = {},\n ): Promise<Buffer> {\n const { default: sharp } = await import('sharp');\n const base = await this.toPng(opts);\n if (!images.length) return base;\n return sharp(base)\n .composite(images.map(im => ({\n input: im.data,\n left: Math.round(im.x),\n top: Math.round(im.y),\n })))\n .png()\n .toBuffer();\n }\n\n /** Inspect the accumulated SVG fragments (for testing/debugging). */\n get fragments(): readonly string[] {\n return this.parts;\n }\n}\n","/**\n * Halftone poster pipeline: URL → 1-bit dithered PNG, ready to composite onto\n * a rasterised page.\n *\n * Steps:\n * 1. Download bytes (with optional User-Agent).\n * 2. Resize to the exact target display size (no scaling during render).\n * 3. CLAHE for local contrast (rescues dark posters), then normalize.\n * 4. Dither (Atkinson by default).\n * 5. Re-encode as a clean greyscale PNG with exactly 0 / 255 values.\n *\n * The output PNG is at the EXACT pixel dimensions you'll display it at on\n * the page — no further scaling. That's the trick that keeps the dot\n * pattern crisp through the master rasterisation step (use `page.toPngWithImages`).\n */\nimport { dither as runDither } from './dither.js';\nimport type { PosterOptions, PreparedImage } from './types.js';\n\nconst DEFAULT_USER_AGENT = 'thermalkit/0.1';\n\nexport async function preparePoster(\n url: string,\n opts: PosterOptions = {},\n): Promise<PreparedImage> {\n const { default: sharp } = await import('sharp');\n\n const res = await fetch(url, {\n headers: { 'User-Agent': opts.userAgent ?? DEFAULT_USER_AGENT },\n });\n if (!res.ok) throw new Error(`Poster HTTP ${res.status} (${url})`);\n const buf = Buffer.from(await res.arrayBuffer());\n\n // Compute the exact target H/W from the source aspect ratio.\n const meta = await sharp(buf).metadata();\n const ratio = (meta.height ?? 1) / (meta.width ?? 1);\n const displayW = opts.width ?? 100;\n const displayH = Math.round(displayW * ratio);\n\n let pipeline = sharp(buf)\n .resize({ width: displayW, height: displayH, fit: 'fill' })\n .greyscale();\n\n const contrast = opts.contrast ?? 'clahe';\n if (contrast === 'clahe') {\n pipeline = pipeline.clahe({ width: 8, height: 8, maxSlope: 3 }).normalize();\n } else if (contrast === 'normalize') {\n pipeline = pipeline.normalize();\n }\n\n const { data: grey, info } = await pipeline.raw().toBuffer({ resolveWithObject: true });\n\n const algo = opts.dither ?? 'atkinson';\n const dithered = runDither(algo, grey, info.width, info.height);\n\n const { data, info: outInfo } = await sharp(Buffer.from(dithered.buffer), {\n raw: { width: info.width, height: info.height, channels: 1 },\n })\n .png({ compressionLevel: 9 })\n .toBuffer({ resolveWithObject: true });\n\n return { data, width: outInfo.width, height: outInfo.height };\n}\n"],"mappings":"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"}
package/dist/page.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PageOptions, TextOptions, IconOptions, RuleOptions, RowOptions, ImageOptions, RenderOptions } from './types.js';
1
+ import type { PageOptions, TextOptions, IconOptions, RuleOptions, RowOptions, ImageOptions, RenderOptions, DotOptions, KvOptions } from './types.js';
2
2
  export declare class Page {
3
3
  /** Output width in pixels. */
4
4
  readonly width: number;
@@ -19,6 +19,9 @@ export declare class Page {
19
19
  /**
20
20
  * Draw text at the current Y baseline (or at `opts.y` if given).
21
21
  * Does NOT advance the cursor — the caller controls vertical rhythm.
22
+ *
23
+ * `opts.align` is a shorthand for the (x, anchor) pair (see TextOptions).
24
+ * Explicit `x` / `anchor` win over `align`.
22
25
  */
23
26
  text(content: string, opts?: TextOptions): this;
24
27
  /**
@@ -41,12 +44,32 @@ export declare class Page {
41
44
  * is for sharp text / vector content already binarised.
42
45
  */
43
46
  image(pngBuffer: Buffer, opts?: ImageOptions): this;
47
+ /**
48
+ * Draw a filled circle (dot) at (x, y) with the given radius.
49
+ * Common use: decorative ornaments under a heading.
50
+ *
51
+ * page.dot(W/2 - 28, { r: 1.5 });
52
+ * page.dot(W/2, { r: 2.5 });
53
+ * page.dot(W/2 + 28, { r: 1.5 });
54
+ */
55
+ dot(x: number, opts?: DotOptions): this;
44
56
  /**
45
57
  * Run `fn` in a "row context": the cursor is preserved on exit, so any
46
58
  * `text()` / `icon()` calls inside land on the same baseline. Optionally
47
59
  * advance the cursor after.
48
60
  */
49
61
  row(fn: () => void, opts?: RowOptions): this;
62
+ /**
63
+ * Key/value row — label on the left, value on the right, same baseline.
64
+ * Cursor doesn't advance (matches the rest of the primitive API).
65
+ *
66
+ * page.kv('Coucher', '21:14');
67
+ * page.kv('Vent', '15 km/h SW', {
68
+ * labelOpts: { weight: 500 },
69
+ * valueOpts: { family: 'georgia', size: 18 },
70
+ * });
71
+ */
72
+ kv(label: string, value: string, opts?: KvOptions): this;
50
73
  /**
51
74
  * Big centered title with optional italic subtitle below.
52
75
  * Advances the cursor past the block.
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAIpB,qBAAa,IAAI;IACf,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uDAAuD;IACvD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,yCAAyC;IACzC,CAAC,SAAK;IAEN,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAE/B,IAAI,GAAE,WAAgB;IAuBlC,0EAA0E;IAC1E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5B,8DAA8D;IAC9D,MAAM,CAAC,MAAM,SAAK,GAAG,IAAI;IASzB;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,IAAI;IAUnD;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAK,EAAE,IAAI,GAAE,WAAgB,GAAG,IAAI;IAS3D,wCAAwC;IACxC,IAAI,CAAC,IAAI,GAAE,WAAgB,GAAG,IAAI;IAOlC,iEAAiE;IACjE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK5B;;;;;;;;OAQG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,IAAI;IAoBvD;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,IAAI,GAAE,UAAe,GAAG,IAAI;IAWhD;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;IAwB7G;;;OAGG;IACH,OAAO,CACL,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/E,IAAI;IAsBP,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,MAAM;IAIvE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,MAAM,EAAE;IAK3F,SAAS,yBAAa;IAMtB,uEAAuE;IACvE,KAAK,IAAI,MAAM;IASf;;;;OAIG;IACG,KAAK,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAWtD;;;;OAIG;IACG,eAAe,CACnB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACrD,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,MAAM,CAAC;IAclB,qEAAqE;IACrE,IAAI,SAAS,IAAI,SAAS,MAAM,EAAE,CAEjC;CACF"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"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"}
package/dist/svg.d.ts CHANGED
@@ -19,8 +19,19 @@ export declare function wrapByWidth(text: string, maxWidth: number, size: number
19
19
  family?: string;
20
20
  weight?: number;
21
21
  }): string[];
22
+ /**
23
+ * Resolve the `align` shorthand to (x, anchor). Explicit `x` / `anchor` win.
24
+ */
25
+ export declare function resolveAlign(o: TextOptions, page: {
26
+ width: number;
27
+ padding: number;
28
+ }): {
29
+ x: number;
30
+ anchor: 'start' | 'middle' | 'end' | undefined;
31
+ };
22
32
  export declare function buildTextFragment(s: string, y: number, defaultFamily: string, o?: TextOptions & {
23
33
  defaultX: number;
34
+ defaultAnchor?: 'start' | 'middle' | 'end';
24
35
  }): string;
25
36
  export declare function buildRuleFragment(y: number, contentX1: number, contentX2: number, o?: RuleOptions): string;
26
37
  export declare function buildIconFragment(content: string, x: number, y: number, size: number, fill?: string): string;
package/dist/svg.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE3D,eAAO,MAAM,SAAS,GAAI,GAAG,OAAO,KAAG,MAEqD,CAAC;AAE7F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWtF;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,MAAM,CAKR;AAED,wDAAwD;AACxD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,MAAM,EAAE,CAeV;AAED,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,aAAa,EAAE,MAAM,EACrB,CAAC,GAAE,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAoB,GACtD,MAAM,CAWR;AAED,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,MAAM,EACT,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,CAAC,GAAE,WAAgB,GAClB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,IAAI,SAAS,GACZ,MAAM,CAGR"}
1
+ {"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE3D,eAAO,MAAM,SAAS,GAAI,GAAG,OAAO,KAAG,MAEqD,CAAC;AAE7F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWtF;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,MAAM,CAKR;AAED,wDAAwD;AACxD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,MAAM,EAAE,CAeV;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,CAAC,EAAE,WAAW,EACd,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAA;CAAE,CAqB/D;AAED,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,aAAa,EAAE,MAAM,EACrB,CAAC,GAAE,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;CAAoB,GAClG,MAAM,CAYR;AAED,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,MAAM,EACT,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,CAAC,GAAE,WAAgB,GAClB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,IAAI,SAAS,GACZ,MAAM,CAGR"}
package/dist/types.d.ts CHANGED
@@ -4,11 +4,19 @@
4
4
  export type FontFamily = string;
5
5
  export type Align = 'left' | 'center' | 'right';
6
6
  export interface TextOptions {
7
- /** X coordinate, or shorthand for alignment relative to the content area. */
7
+ /**
8
+ * Horizontal alignment shorthand. Sets a sensible (x, anchor) pair:
9
+ * - `'left'` → x = padding, anchor = 'start' (default)
10
+ * - `'center'` → x = width/2, anchor = 'middle'
11
+ * - `'right'` → x = width - padding, anchor = 'end'
12
+ * Overridden by an explicit `x` or `anchor`.
13
+ */
14
+ align?: Align;
15
+ /** X coordinate. Wins over `align`. */
8
16
  x?: number;
9
17
  /** Override Y (default: current cursor). */
10
18
  y?: number;
11
- /** Text anchor overrides the alignment derived from `x`. */
19
+ /** Text anchor. Wins over `align`. */
12
20
  anchor?: 'start' | 'middle' | 'end';
13
21
  /** Font family stack. Common shorthands: `'georgia'`, `'helvetica'`, `'mono'`. */
14
22
  family?: FontFamily;
@@ -23,6 +31,20 @@ export interface TextOptions {
23
31
  /** Fill colour. Defaults to black; usually no reason to change. */
24
32
  fill?: string;
25
33
  }
34
+ export interface DotOptions {
35
+ /** Y coordinate. Default: the page's current cursor. */
36
+ y?: number;
37
+ /** Radius in pixels. Default 2. */
38
+ r?: number;
39
+ /** Fill colour. Defaults to black. */
40
+ fill?: string;
41
+ }
42
+ export interface KvOptions {
43
+ /** Options applied to the label (left). */
44
+ labelOpts?: TextOptions;
45
+ /** Options applied to the value (right). */
46
+ valueOpts?: TextOptions;
47
+ }
26
48
  export interface IconOptions {
27
49
  /** Override X coordinate (default: page padding). */
28
50
  x?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thermalkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Compose nicely-typeset images for thermal receipt printers (Epson TM-T88VI and friends).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",