thermalkit 0.1.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/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/chunks/dither-DyIl72tE.js +166 -0
- package/dist/chunks/dither-DyIl72tE.js.map +1 -0
- package/dist/chunks/icons-C1OFHE6u.js +49 -0
- package/dist/chunks/icons-C1OFHE6u.js.map +1 -0
- package/dist/dither.d.ts +42 -0
- package/dist/dither.d.ts.map +1 -0
- package/dist/dither.js +3 -0
- package/dist/icons.d.ts +11 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +3 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +317 -0
- package/dist/index.js.map +1 -0
- package/dist/page.d.ts +94 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/poster.d.ts +3 -0
- package/dist/poster.d.ts.map +1 -0
- package/dist/printer.d.ts +49 -0
- package/dist/printer.d.ts.map +1 -0
- package/dist/printer.js +77 -0
- package/dist/printer.js.map +1 -0
- package/dist/svg.d.ts +27 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NTag
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# thermalkit
|
|
2
|
+
|
|
3
|
+
Compose nicely-typeset images for thermal receipt printers (Epson TM-T88VI and friends).
|
|
4
|
+
|
|
5
|
+
Designed for 80 mm thermal paper at 180 dpi — the canonical 504-pixel-wide vertical strip. Use it to build daily briefings, ticket lineups, schedules, weather forecasts, or anything that fits a single-column receipt layout.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install thermalkit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If you want to print (not just generate images), install the printer dependency along with it:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install thermalkit node-thermal-printer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { Page } from 'thermalkit';
|
|
23
|
+
import { EpsonPrinter } from 'thermalkit/printer';
|
|
24
|
+
|
|
25
|
+
const page = new Page({
|
|
26
|
+
width: 504, // px (TM-T88VI printable area)
|
|
27
|
+
icons: ['sun', 'wind', 'drop'], // Phosphor icons, pre-loaded
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
page.advance(60);
|
|
31
|
+
page.title('BERLIN', { subtitle: 'morning briefing' });
|
|
32
|
+
page.rule({ stroke: 1.5 });
|
|
33
|
+
|
|
34
|
+
page.advance(36);
|
|
35
|
+
page.section('MÉTÉO', { icon: 'sun' });
|
|
36
|
+
|
|
37
|
+
page.text('21° / 14°', { family: 'georgia', size: 32 });
|
|
38
|
+
page.advance(28);
|
|
39
|
+
|
|
40
|
+
page.row(() => {
|
|
41
|
+
page.icon('wind', 16, { dy: -12 });
|
|
42
|
+
page.text('15 km/h SW', { x: 38 });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const png = await page.toPng();
|
|
46
|
+
await new EpsonPrinter({ host: '192.168.0.225' }).print(png);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `new Page(options)`
|
|
52
|
+
|
|
53
|
+
Construct a vertical layout buffer.
|
|
54
|
+
|
|
55
|
+
| option | type | default | meaning |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `width` | `number` | `504` | Output width in pixels. Must be a multiple of 8 (ESC/POS bitmap requirement). |
|
|
58
|
+
| `padding` | `number` | `22` | Horizontal padding around the content area. |
|
|
59
|
+
| `icons` | `string[]` \| `Record<string,string>` | `{}` | Either a list of Phosphor icon names to pre-load, or a name→SVG-inner-content map. |
|
|
60
|
+
| `defaultFontFamily` | `string` | `Helvetica, Arial, sans-serif` | Used by `page.text()` when no family is given. |
|
|
61
|
+
|
|
62
|
+
### Cursor control
|
|
63
|
+
|
|
64
|
+
| method | what it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `page.y` | Read or write the current Y cursor (mutable property). |
|
|
67
|
+
| `page.advance(delta)` | Move the cursor down by `delta` pixels. |
|
|
68
|
+
| `page.spacer(amount = 12)` | Alias for `advance` with a documented default. |
|
|
69
|
+
|
|
70
|
+
### Primitives (all chainable)
|
|
71
|
+
|
|
72
|
+
| method | what it draws |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `page.text(content, opts?)` | Text at the current baseline. Does **not** auto-advance. |
|
|
75
|
+
| `page.icon(name, size?, opts?)` | Phosphor icon (must be pre-loaded). |
|
|
76
|
+
| `page.rule(opts?)` | Horizontal line at the current Y. |
|
|
77
|
+
| `page.image(pngBuffer, opts?)` | Embed a raster image (use `preparePoster` for halftone photos). |
|
|
78
|
+
| `page.push(svgFragment)` | Append raw SVG (escape hatch for custom shapes). |
|
|
79
|
+
| `page.row(fn, opts?)` | Run `fn` with the cursor preserved — multiple `text` / `icon` calls land on the same baseline. |
|
|
80
|
+
|
|
81
|
+
### Higher-level helpers
|
|
82
|
+
|
|
83
|
+
| method | what it draws |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `page.title(text, { subtitle?, size?, family? })` | Big centered title + optional italic subtitle. |
|
|
86
|
+
| `page.section(label, { icon?, size? })` | Caps-tracked section header, with optional icon on the left. |
|
|
87
|
+
|
|
88
|
+
### Text-measurement
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
page.approxWidth(text, size, opts?);
|
|
92
|
+
page.wrapByWidth(text, maxWidth, size, opts?);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Naive but cheap. Good enough for word-wrapping decisions in the 10-24 px font-size range.
|
|
96
|
+
|
|
97
|
+
### Output
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const svg = page.toSvg(); // raw SVG string
|
|
101
|
+
const png = await page.toPng(); // 1-bit-style PNG buffer
|
|
102
|
+
const png = await page.toPng({ // override raster params
|
|
103
|
+
density: 240, width: 504, threshold: 140,
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For halftone photos (posters), use `toPngWithImages` to composite dithered rasters onto the thresholded base — that avoids the dither pattern getting smeared during the master rasterisation step:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
import { Page, preparePoster } from 'thermalkit';
|
|
111
|
+
|
|
112
|
+
const page = new Page({ width: 504 });
|
|
113
|
+
// ...
|
|
114
|
+
const posterY = page.y;
|
|
115
|
+
page.advance(150); // reserve space
|
|
116
|
+
|
|
117
|
+
const poster = await preparePoster('https://example.com/poster.jpg', {
|
|
118
|
+
width: 100,
|
|
119
|
+
dither: 'atkinson',
|
|
120
|
+
});
|
|
121
|
+
const png = await page.toPngWithImages([
|
|
122
|
+
{ data: poster.data, x: 22, y: posterY },
|
|
123
|
+
]);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Printer
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import { EpsonPrinter } from 'thermalkit/printer';
|
|
130
|
+
|
|
131
|
+
const printer = new EpsonPrinter({ host: '192.168.0.225', port: 9100 });
|
|
132
|
+
|
|
133
|
+
await printer.print(png, {
|
|
134
|
+
density: 0, // -50..+50, persistent in printer memory
|
|
135
|
+
cut: true, // paper cut at end
|
|
136
|
+
align: 'center',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await printer.setDensity(20); // change density without printing
|
|
140
|
+
await printer.raw(Buffer.from([0x1b, 0x40])); // send arbitrary ESC/POS
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Dithering
|
|
144
|
+
|
|
145
|
+
The poster pipeline uses Atkinson dither by default — cleaner contrast than Floyd-Steinberg, less speckle than ordered Bayer. You can call the algorithms directly too:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { atkinson, floydSteinberg, bayer, threshold } from 'thermalkit/dither';
|
|
149
|
+
|
|
150
|
+
const out = atkinson(greyBytes, width, height); // Uint8Array (0 or 255)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| algorithm | character | best for |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `atkinson` | clean, high contrast, less speckle | posters, faces, illustrations |
|
|
156
|
+
| `floydSteinberg` | photographic, more detail, more noise | photographs at large sizes |
|
|
157
|
+
| `bayer` | regular cross-hatch pattern | when you want a "computer-print" aesthetic |
|
|
158
|
+
| `threshold` | hard B&W, no halftone | text, line art, icons |
|
|
159
|
+
|
|
160
|
+
## Icons
|
|
161
|
+
|
|
162
|
+
The library ships with Phosphor icon loading out of the box. Pass icon names to the `Page` constructor:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
const page = new Page({
|
|
166
|
+
icons: ['sun', 'cloud-rain', 'wind', 'calendar-heart'],
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
You can also load them yourself:
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { loadIcon, loadIcons } from 'thermalkit/icons';
|
|
174
|
+
|
|
175
|
+
const sun = loadIcon('sun'); // inner SVG content (string)
|
|
176
|
+
const map = loadIcons(['sun', 'wind']); // name → content
|
|
177
|
+
const map2 = loadIcons(['sun'], 'bold'); // different Phosphor weight
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If `@phosphor-icons/core` isn't installed, icon loading silently returns empty strings (and `page.icon()` becomes a no-op).
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//#region src/dither.ts
|
|
2
|
+
/**
|
|
3
|
+
* 1-bit dithering algorithms.
|
|
4
|
+
*
|
|
5
|
+
* All three take a single-channel greyscale `Uint8Array` (one byte per pixel,
|
|
6
|
+
* 0 = black, 255 = white) and return another `Uint8Array` of the same size
|
|
7
|
+
* containing exclusively 0 or 255 values.
|
|
8
|
+
*
|
|
9
|
+
* Pick by use case:
|
|
10
|
+
* - **atkinson** — default; cleaner / higher contrast / less speckle. Loses
|
|
11
|
+
* detail in extreme dark/light. Best for posters, faces, illustrations.
|
|
12
|
+
* - **fs** (Floyd-Steinberg) — preserves the most photographic detail but
|
|
13
|
+
* produces visible noise at small sizes.
|
|
14
|
+
* - **bayer** — fastest, regular cross-hatch pattern. Good when you want a
|
|
15
|
+
* deliberate "computer-print" look.
|
|
16
|
+
*
|
|
17
|
+
* See the README for a side-by-side comparison.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Floyd-Steinberg error-diffusion dither.
|
|
21
|
+
* Right 7/16, down-left 3/16, down 5/16, down-right 1/16.
|
|
22
|
+
*/
|
|
23
|
+
function floydSteinberg(grey, width, height) {
|
|
24
|
+
const work = new Float32Array(grey.length);
|
|
25
|
+
for (let i = 0; i < grey.length; i++) work[i] = grey[i];
|
|
26
|
+
const out = new Uint8Array(grey.length);
|
|
27
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
28
|
+
const i = y * width + x;
|
|
29
|
+
const v = work[i];
|
|
30
|
+
const nv = v < 128 ? 0 : 255;
|
|
31
|
+
out[i] = nv;
|
|
32
|
+
const err = v - nv;
|
|
33
|
+
if (x + 1 < width) work[i + 1] += err * (7 / 16);
|
|
34
|
+
if (x > 0 && y + 1 < height) work[i + width - 1] += err * (3 / 16);
|
|
35
|
+
if (y + 1 < height) work[i + width] += err * (5 / 16);
|
|
36
|
+
if (x + 1 < width && y + 1 < height) work[i + width + 1] += err * (1 / 16);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Atkinson dither — used by Apple's original Macintosh. Propagates only 6/8
|
|
42
|
+
* of the error across 6 neighbours (the rest is discarded), giving higher
|
|
43
|
+
* contrast and less speckle than Floyd-Steinberg.
|
|
44
|
+
*/
|
|
45
|
+
function atkinson(grey, width, height) {
|
|
46
|
+
const work = new Float32Array(grey.length);
|
|
47
|
+
for (let i = 0; i < grey.length; i++) work[i] = grey[i];
|
|
48
|
+
const out = new Uint8Array(grey.length);
|
|
49
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
50
|
+
const i = y * width + x;
|
|
51
|
+
const v = work[i];
|
|
52
|
+
const nv = v < 128 ? 0 : 255;
|
|
53
|
+
out[i] = nv;
|
|
54
|
+
const err = (v - nv) / 8;
|
|
55
|
+
if (x + 1 < width) work[i + 1] += err;
|
|
56
|
+
if (x + 2 < width) work[i + 2] += err;
|
|
57
|
+
if (y + 1 < height) {
|
|
58
|
+
if (x > 0) work[i + width - 1] += err;
|
|
59
|
+
work[i + width] += err;
|
|
60
|
+
if (x + 1 < width) work[i + width + 1] += err;
|
|
61
|
+
}
|
|
62
|
+
if (y + 2 < height) work[i + 2 * width] += err;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
const BAYER_8 = new Uint8Array([
|
|
67
|
+
0,
|
|
68
|
+
32,
|
|
69
|
+
8,
|
|
70
|
+
40,
|
|
71
|
+
2,
|
|
72
|
+
34,
|
|
73
|
+
10,
|
|
74
|
+
42,
|
|
75
|
+
48,
|
|
76
|
+
16,
|
|
77
|
+
56,
|
|
78
|
+
24,
|
|
79
|
+
50,
|
|
80
|
+
18,
|
|
81
|
+
58,
|
|
82
|
+
26,
|
|
83
|
+
12,
|
|
84
|
+
44,
|
|
85
|
+
4,
|
|
86
|
+
36,
|
|
87
|
+
14,
|
|
88
|
+
46,
|
|
89
|
+
6,
|
|
90
|
+
38,
|
|
91
|
+
60,
|
|
92
|
+
28,
|
|
93
|
+
52,
|
|
94
|
+
20,
|
|
95
|
+
62,
|
|
96
|
+
30,
|
|
97
|
+
54,
|
|
98
|
+
22,
|
|
99
|
+
3,
|
|
100
|
+
35,
|
|
101
|
+
11,
|
|
102
|
+
43,
|
|
103
|
+
1,
|
|
104
|
+
33,
|
|
105
|
+
9,
|
|
106
|
+
41,
|
|
107
|
+
51,
|
|
108
|
+
19,
|
|
109
|
+
59,
|
|
110
|
+
27,
|
|
111
|
+
49,
|
|
112
|
+
17,
|
|
113
|
+
57,
|
|
114
|
+
25,
|
|
115
|
+
15,
|
|
116
|
+
47,
|
|
117
|
+
7,
|
|
118
|
+
39,
|
|
119
|
+
13,
|
|
120
|
+
45,
|
|
121
|
+
5,
|
|
122
|
+
37,
|
|
123
|
+
63,
|
|
124
|
+
31,
|
|
125
|
+
55,
|
|
126
|
+
23,
|
|
127
|
+
61,
|
|
128
|
+
29,
|
|
129
|
+
53,
|
|
130
|
+
21
|
|
131
|
+
]);
|
|
132
|
+
/**
|
|
133
|
+
* 8×8 Bayer ordered dither. Deterministic, fast, and produces a regular
|
|
134
|
+
* cross-hatch pattern at low resolutions.
|
|
135
|
+
*/
|
|
136
|
+
function bayer(grey, width, height) {
|
|
137
|
+
const out = new Uint8Array(grey.length);
|
|
138
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
139
|
+
const t = (BAYER_8[(y & 7) * 8 + (x & 7)] + .5) / 64 * 255;
|
|
140
|
+
out[y * width + x] = grey[y * width + x] < t ? 0 : 255;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Threshold ("hard binarization") — no halftone, every pixel is either 0 or 255
|
|
146
|
+
* based on whether it's above or below `threshold`. Use for line art / text,
|
|
147
|
+
* not for photographs.
|
|
148
|
+
*/
|
|
149
|
+
function threshold(grey, width, height, cutoff = 128) {
|
|
150
|
+
const out = new Uint8Array(grey.length);
|
|
151
|
+
for (let i = 0; i < grey.length; i++) out[i] = grey[i] < cutoff ? 0 : 255;
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function dither(algo, grey, width, height) {
|
|
155
|
+
switch (algo) {
|
|
156
|
+
case "fs": return floydSteinberg(grey, width, height);
|
|
157
|
+
case "bayer": return bayer(grey, width, height);
|
|
158
|
+
case "none": return threshold(grey, width, height);
|
|
159
|
+
case "atkinson":
|
|
160
|
+
default: return atkinson(grey, width, height);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { atkinson, bayer, dither, floydSteinberg, threshold };
|
|
166
|
+
//# sourceMappingURL=dither-DyIl72tE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dither-DyIl72tE.js","names":["grey: Uint8Array","width: number","height: number","algo: DitherAlgorithm"],"sources":["../../src/dither.ts"],"sourcesContent":["/**\n * 1-bit dithering algorithms.\n *\n * All three take a single-channel greyscale `Uint8Array` (one byte per pixel,\n * 0 = black, 255 = white) and return another `Uint8Array` of the same size\n * containing exclusively 0 or 255 values.\n *\n * Pick by use case:\n * - **atkinson** — default; cleaner / higher contrast / less speckle. Loses\n * detail in extreme dark/light. Best for posters, faces, illustrations.\n * - **fs** (Floyd-Steinberg) — preserves the most photographic detail but\n * produces visible noise at small sizes.\n * - **bayer** — fastest, regular cross-hatch pattern. Good when you want a\n * deliberate \"computer-print\" look.\n *\n * See the README for a side-by-side comparison.\n */\n\n/**\n * Floyd-Steinberg error-diffusion dither.\n * Right 7/16, down-left 3/16, down 5/16, down-right 1/16.\n */\nexport function floydSteinberg(\n grey: Uint8Array,\n width: number,\n height: number,\n): Uint8Array {\n const work = new Float32Array(grey.length);\n for (let i = 0; i < grey.length; i++) work[i] = grey[i];\n const out = new Uint8Array(grey.length);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const i = y * width + x;\n const v = work[i];\n const nv = v < 128 ? 0 : 255;\n out[i] = nv;\n const err = v - nv;\n if (x + 1 < width) work[i + 1] += err * (7 / 16);\n if (x > 0 && y + 1 < height) work[i + width - 1] += err * (3 / 16);\n if (y + 1 < height) work[i + width] += err * (5 / 16);\n if (x + 1 < width && y + 1 < height) work[i + width + 1] += err * (1 / 16);\n }\n }\n return out;\n}\n\n/**\n * Atkinson dither — used by Apple's original Macintosh. Propagates only 6/8\n * of the error across 6 neighbours (the rest is discarded), giving higher\n * contrast and less speckle than Floyd-Steinberg.\n */\nexport function atkinson(\n grey: Uint8Array,\n width: number,\n height: number,\n): Uint8Array {\n const work = new Float32Array(grey.length);\n for (let i = 0; i < grey.length; i++) work[i] = grey[i];\n const out = new Uint8Array(grey.length);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const i = y * width + x;\n const v = work[i];\n const nv = v < 128 ? 0 : 255;\n out[i] = nv;\n const err = (v - nv) / 8;\n if (x + 1 < width) work[i + 1] += err;\n if (x + 2 < width) work[i + 2] += err;\n if (y + 1 < height) {\n if (x > 0) work[i + width - 1] += err;\n work[i + width] += err;\n if (x + 1 < width) work[i + width + 1] += err;\n }\n if (y + 2 < height) work[i + 2 * width] += err;\n }\n }\n return out;\n}\n\nconst BAYER_8 = new Uint8Array([\n 0, 32, 8, 40, 2, 34, 10, 42,\n 48, 16, 56, 24, 50, 18, 58, 26,\n 12, 44, 4, 36, 14, 46, 6, 38,\n 60, 28, 52, 20, 62, 30, 54, 22,\n 3, 35, 11, 43, 1, 33, 9, 41,\n 51, 19, 59, 27, 49, 17, 57, 25,\n 15, 47, 7, 39, 13, 45, 5, 37,\n 63, 31, 55, 23, 61, 29, 53, 21,\n]);\n\n/**\n * 8×8 Bayer ordered dither. Deterministic, fast, and produces a regular\n * cross-hatch pattern at low resolutions.\n */\nexport function bayer(\n grey: Uint8Array,\n width: number,\n height: number,\n): Uint8Array {\n const out = new Uint8Array(grey.length);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const t = ((BAYER_8[(y & 7) * 8 + (x & 7)] + 0.5) / 64) * 255;\n out[y * width + x] = grey[y * width + x] < t ? 0 : 255;\n }\n }\n return out;\n}\n\n/**\n * Threshold (\"hard binarization\") — no halftone, every pixel is either 0 or 255\n * based on whether it's above or below `threshold`. Use for line art / text,\n * not for photographs.\n */\nexport function threshold(\n grey: Uint8Array,\n width: number,\n height: number,\n cutoff = 128,\n): Uint8Array {\n const out = new Uint8Array(grey.length);\n for (let i = 0; i < grey.length; i++) out[i] = grey[i] < cutoff ? 0 : 255;\n return out;\n}\n\nimport type { DitherAlgorithm } from './types.js';\n\nexport function dither(\n algo: DitherAlgorithm,\n grey: Uint8Array,\n width: number,\n height: number,\n): Uint8Array {\n switch (algo) {\n case 'fs': return floydSteinberg(grey, width, height);\n case 'bayer': return bayer(grey, width, height);\n case 'none': return threshold(grey, width, height);\n case 'atkinson':\n default: return atkinson(grey, width, height);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/icons.ts
|
|
6
|
+
const require_ = createRequire(import.meta.url);
|
|
7
|
+
let phosphorAssetsDir = null;
|
|
8
|
+
try {
|
|
9
|
+
const pkgPath = require_.resolve("@phosphor-icons/core/package.json");
|
|
10
|
+
phosphorAssetsDir = path.join(path.dirname(pkgPath), "assets");
|
|
11
|
+
} catch {
|
|
12
|
+
phosphorAssetsDir = null;
|
|
13
|
+
}
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
function readInner(filePath) {
|
|
16
|
+
const raw = readFileSync(filePath, "utf8");
|
|
17
|
+
const m = raw.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
|
|
18
|
+
return m ? m[1].trim() : "";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load a single Phosphor icon's inner SVG.
|
|
22
|
+
* Returns `''` if the icon (or Phosphor itself) is unavailable.
|
|
23
|
+
*/
|
|
24
|
+
function loadIcon(name, weight = "regular") {
|
|
25
|
+
const key = `${weight}/${name}`;
|
|
26
|
+
const cached = cache.get(key);
|
|
27
|
+
if (cached !== void 0) return cached;
|
|
28
|
+
let content = "";
|
|
29
|
+
if (phosphorAssetsDir) {
|
|
30
|
+
const file = path.join(phosphorAssetsDir, weight, `${name}.svg`);
|
|
31
|
+
if (existsSync(file)) try {
|
|
32
|
+
content = readInner(file);
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
cache.set(key, content);
|
|
36
|
+
return content;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load multiple icons into a `name → content` map suitable for `new Page({ icons })`.
|
|
40
|
+
*/
|
|
41
|
+
function loadIcons(names, weight = "regular") {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const n of names) out[n] = loadIcon(n, weight);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { loadIcon, loadIcons };
|
|
49
|
+
//# sourceMappingURL=icons-C1OFHE6u.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons-C1OFHE6u.js","names":["phosphorAssetsDir: string | null","filePath: string","name: string","weight: PhosphorWeight","names: string[]","out: Record<string, string>"],"sources":["../../src/icons.ts"],"sourcesContent":["/**\n * Thin loader for Phosphor icons (`@phosphor-icons/core`).\n *\n * The package ships per-icon `.svg` files at:\n * assets/<weight>/<name>.svg\n *\n * We read each file once, extract the inner SVG content (everything between\n * `<svg …>` and `</svg>`), and cache it so subsequent loads are free.\n *\n * The Page builder embeds this content via:\n * <g transform=\"translate(x, y) scale(s)\" fill=\"#000\">{content}</g>\n */\nimport { readFileSync, existsSync } from 'node:fs';\nimport path from 'node:path';\nimport { createRequire } from 'node:module';\n\nconst require_ = createRequire(import.meta.url);\n\nexport type PhosphorWeight =\n | 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';\n\n// Resolve the package once. Fall back gracefully if Phosphor isn't installed.\nlet phosphorAssetsDir: string | null = null;\ntry {\n const pkgPath = require_.resolve('@phosphor-icons/core/package.json');\n phosphorAssetsDir = path.join(path.dirname(pkgPath), 'assets');\n} catch {\n phosphorAssetsDir = null;\n}\n\nconst cache = new Map<string, string>();\n\nfunction readInner(filePath: string): string {\n const raw = readFileSync(filePath, 'utf8');\n const m = raw.match(/<svg[^>]*>([\\s\\S]*?)<\\/svg>/);\n return m ? m[1].trim() : '';\n}\n\n/**\n * Load a single Phosphor icon's inner SVG.\n * Returns `''` if the icon (or Phosphor itself) is unavailable.\n */\nexport function loadIcon(name: string, weight: PhosphorWeight = 'regular'): string {\n const key = `${weight}/${name}`;\n const cached = cache.get(key);\n if (cached !== undefined) return cached;\n let content = '';\n if (phosphorAssetsDir) {\n const file = path.join(phosphorAssetsDir, weight, `${name}.svg`);\n if (existsSync(file)) {\n try { content = readInner(file); } catch { /* swallow */ }\n }\n }\n cache.set(key, content);\n return content;\n}\n\n/**\n * Load multiple icons into a `name → content` map suitable for `new Page({ icons })`.\n */\nexport function loadIcons(\n names: string[],\n weight: PhosphorWeight = 'regular',\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const n of names) out[n] = loadIcon(n, weight);\n return out;\n}\n"],"mappings":";;;;;AAgBA,MAAM,WAAW,cAAc,OAAO,KAAK,IAAI;AAM/C,IAAIA,oBAAmC;AACvC,IAAI;CACF,MAAM,UAAU,SAAS,QAAQ,oCAAoC;AACrE,qBAAoB,KAAK,KAAK,KAAK,QAAQ,QAAQ,EAAE,SAAS;AAC/D,QAAO;AACN,qBAAoB;AACrB;AAED,MAAM,QAAQ,IAAI;AAElB,SAAS,UAAUC,UAA0B;CAC3C,MAAM,MAAM,aAAa,UAAU,OAAO;CAC1C,MAAM,IAAI,IAAI,MAAM,8BAA8B;AAClD,QAAO,IAAI,EAAE,GAAG,MAAM,GAAG;AAC1B;;;;;AAMD,SAAgB,SAASC,MAAcC,SAAyB,WAAmB;CACjF,MAAM,OAAO,EAAE,OAAO,GAAG,KAAK;CAC9B,MAAM,SAAS,MAAM,IAAI,IAAI;AAC7B,KAAI,kBAAsB,QAAO;CACjC,IAAI,UAAU;AACd,KAAI,mBAAmB;EACrB,MAAM,OAAO,KAAK,KAAK,mBAAmB,SAAS,EAAE,KAAK,MAAM;AAChE,MAAI,WAAW,KAAK,CAClB,KAAI;AAAE,aAAU,UAAU,KAAK;EAAG,QAAO,CAAiB;CAE7D;AACD,OAAM,IAAI,KAAK,QAAQ;AACvB,QAAO;AACR;;;;AAKD,SAAgB,UACdC,OACAD,SAAyB,WACD;CACxB,MAAME,MAA8B,CAAE;AACtC,MAAK,MAAM,KAAK,MAAO,KAAI,KAAK,SAAS,GAAG,OAAO;AACnD,QAAO;AACR"}
|
package/dist/dither.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1-bit dithering algorithms.
|
|
3
|
+
*
|
|
4
|
+
* All three take a single-channel greyscale `Uint8Array` (one byte per pixel,
|
|
5
|
+
* 0 = black, 255 = white) and return another `Uint8Array` of the same size
|
|
6
|
+
* containing exclusively 0 or 255 values.
|
|
7
|
+
*
|
|
8
|
+
* Pick by use case:
|
|
9
|
+
* - **atkinson** — default; cleaner / higher contrast / less speckle. Loses
|
|
10
|
+
* detail in extreme dark/light. Best for posters, faces, illustrations.
|
|
11
|
+
* - **fs** (Floyd-Steinberg) — preserves the most photographic detail but
|
|
12
|
+
* produces visible noise at small sizes.
|
|
13
|
+
* - **bayer** — fastest, regular cross-hatch pattern. Good when you want a
|
|
14
|
+
* deliberate "computer-print" look.
|
|
15
|
+
*
|
|
16
|
+
* See the README for a side-by-side comparison.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Floyd-Steinberg error-diffusion dither.
|
|
20
|
+
* Right 7/16, down-left 3/16, down 5/16, down-right 1/16.
|
|
21
|
+
*/
|
|
22
|
+
export declare function floydSteinberg(grey: Uint8Array, width: number, height: number): Uint8Array;
|
|
23
|
+
/**
|
|
24
|
+
* Atkinson dither — used by Apple's original Macintosh. Propagates only 6/8
|
|
25
|
+
* of the error across 6 neighbours (the rest is discarded), giving higher
|
|
26
|
+
* contrast and less speckle than Floyd-Steinberg.
|
|
27
|
+
*/
|
|
28
|
+
export declare function atkinson(grey: Uint8Array, width: number, height: number): Uint8Array;
|
|
29
|
+
/**
|
|
30
|
+
* 8×8 Bayer ordered dither. Deterministic, fast, and produces a regular
|
|
31
|
+
* cross-hatch pattern at low resolutions.
|
|
32
|
+
*/
|
|
33
|
+
export declare function bayer(grey: Uint8Array, width: number, height: number): Uint8Array;
|
|
34
|
+
/**
|
|
35
|
+
* Threshold ("hard binarization") — no halftone, every pixel is either 0 or 255
|
|
36
|
+
* based on whether it's above or below `threshold`. Use for line art / text,
|
|
37
|
+
* not for photographs.
|
|
38
|
+
*/
|
|
39
|
+
export declare function threshold(grey: Uint8Array, width: number, height: number, cutoff?: number): Uint8Array;
|
|
40
|
+
import type { DitherAlgorithm } from './types.js';
|
|
41
|
+
export declare function dither(algo: DitherAlgorithm, grey: Uint8Array, width: number, height: number): Uint8Array;
|
|
42
|
+
//# sourceMappingURL=dither.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dither.d.ts","sourceRoot":"","sources":["../src/dither.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,UAAU,CAkBZ;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,UAAU,CAsBZ;AAaD;;;GAGG;AACH,wBAAgB,KAAK,CACnB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,UAAU,CASZ;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,SAAM,GACX,UAAU,CAIZ;AAED,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,wBAAgB,MAAM,CACpB,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,UAAU,CAQZ"}
|
package/dist/dither.js
ADDED
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type PhosphorWeight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
|
|
2
|
+
/**
|
|
3
|
+
* Load a single Phosphor icon's inner SVG.
|
|
4
|
+
* Returns `''` if the icon (or Phosphor itself) is unavailable.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadIcon(name: string, weight?: PhosphorWeight): string;
|
|
7
|
+
/**
|
|
8
|
+
* Load multiple icons into a `name → content` map suitable for `new Page({ icons })`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadIcons(names: string[], weight?: PhosphorWeight): Record<string, string>;
|
|
11
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,cAAc,GACtB,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAmB/D;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,cAA0B,GAAG,MAAM,CAajF;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,GAAE,cAA0B,GACjC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIxB"}
|
package/dist/icons.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thermalkit — compose 1-bit images for thermal receipt printers.
|
|
3
|
+
*
|
|
4
|
+
* Quick start:
|
|
5
|
+
*
|
|
6
|
+
* import { Page } from 'thermalkit';
|
|
7
|
+
* import { EpsonPrinter } from 'thermalkit/printer';
|
|
8
|
+
*
|
|
9
|
+
* const page = new Page({ width: 504, icons: ['sun', 'wind'] });
|
|
10
|
+
* page.title('BERLIN', { subtitle: 'morning briefing' });
|
|
11
|
+
* page.rule();
|
|
12
|
+
* page.section('MÉTÉO', { icon: 'sun' });
|
|
13
|
+
* page.text('21° / 14°', { size: 32, family: 'georgia' });
|
|
14
|
+
*
|
|
15
|
+
* const png = await page.toPng();
|
|
16
|
+
* await new EpsonPrinter({ host: '192.168.0.225' }).print(png);
|
|
17
|
+
*/
|
|
18
|
+
export { Page } from './page.js';
|
|
19
|
+
export { preparePoster } from './poster.js';
|
|
20
|
+
export { loadIcon, loadIcons } from './icons.js';
|
|
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';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|