Skip to content

Commit e301239

Browse files
authored
feat: add PNG/raster output support (#86)
* feat(#3): add PNG/raster output support Pure-JS PNG encoder (zero dependencies) that rasterizes directly from encoder data. Palette-based PNG with stored DEFLATE compression. API: barcodePNG, qrcodePNG, datamatrixPNG, pdf417PNG, aztecPNG, gs1datamatrixPNG + *PNGDataURI variants. New subpath: etiket/png. Extracts encodeBars() from _barcode.ts for shared use between SVG and PNG rendering paths.
1 parent 447f907 commit e301239

23 files changed

Lines changed: 1367 additions & 88 deletions

.github/assets/cover.png

356 KB
Loading

.github/assets/cover.svg

Lines changed: 1 addition & 1 deletion
Loading

AGENTS.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# etiket
22

3-
Zero-dependency barcode & QR code SVG generator. 40+ formats, styled QR codes, tree-shakeable. Pure TypeScript.
3+
Zero-dependency barcode & QR code generator — SVG & PNG output. 40+ formats, styled QR codes, tree-shakeable. Pure TypeScript.
44

55
> [!IMPORTANT]
66
> Keep `AGENTS.md` updated with project status.
@@ -15,6 +15,7 @@ src/
1515
datamatrix.ts # Sub-path: etiket/datamatrix
1616
pdf417.ts # Sub-path: etiket/pdf417
1717
aztec.ts # Sub-path: etiket/aztec
18+
png.ts # Sub-path: etiket/png
1819
cli.ts # CLI entry (citty + consola)
1920
errors.ts # Custom error classes
2021
env.d.ts # Runtime type declarations
@@ -78,14 +79,21 @@ src/
7879
optimize.ts # SVG optimization
7980
types.ts # All rendering types
8081
utils.ts # escapeAttr utility
82+
png/
83+
types.ts # BarcodePNGOptions, MatrixPNGOptions
84+
crc32.ts # CRC32 for PNG chunk checksums
85+
adler32.ts # Adler32 for zlib wrapper
86+
deflate.ts # Stored DEFLATE + zlib compression
87+
png-encoder.ts # PNG chunk assembly (palette-based)
88+
rasterize.ts # bars/matrix → pixel rows → PNG
8189
text.ts # Terminal output (Unicode blocks)
8290
data-uri.ts # SVG → Data URI / Base64
8391
validators/
8492
index.ts # Re-exports
8593
barcode.ts # Per-format validation
8694
qr.ts # QR validation with metadata
8795
test/
88-
*.test.ts # 63 test files, 834+ tests
96+
*.test.ts # 67 test files, 905+ tests
8997
qr-roundtrip.test.ts # QR encode→decode via jsQR (all versions, EC, masks)
9098
barcode-roundtrip.test.ts # 1D barcode structural validation
9199
docs/
@@ -94,10 +102,12 @@ docs/
94102

95103
## Public API
96104

97-
Single entry: `etiket` (everything). Sub-paths: `etiket/barcode`, `etiket/qr`, `etiket/datamatrix`, `etiket/pdf417`, `etiket/aztec`.
105+
Single entry: `etiket` (everything). Sub-paths: `etiket/barcode`, `etiket/qr`, `etiket/datamatrix`, `etiket/pdf417`, `etiket/aztec`, `etiket/png`.
98106

99107
Key functions: `barcode()`, `qrcode()`, `datamatrix()`, `pdf417()`, `aztec()`, `gs1datamatrix()`, `swissQR()`, `gs1DigitalLink()`, `wifi()`, `vcard()`, `mecard()`, `event()`, `phone()`, `email()`, `sms()`, `geo()`, `encode()`, `optimizeSVG()`.
100108

109+
PNG functions: `barcodePNG()`, `qrcodePNG()`, `datamatrixPNG()`, `pdf417PNG()`, `aztecPNG()`, `gs1datamatrixPNG()` + `*PNGDataURI()` variants. Low-level: `renderBarcodePNG()`, `renderMatrixPNG()`.
110+
101111
## Build & Scripts
102112

103113
```bash

README.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<p align="center">
22
<br>
3-
<img src=".github/assets/cover.png" alt="etiket — Zero-dependency barcode & QR code SVG generator" width="100%">
3+
<img src=".github/assets/cover.png" alt="etiket — Zero-dependency barcode & QR code generator (SVG & PNG)" width="100%">
44
<br><br>
55
<b style="font-size: 2em;">etiket</b>
66
<br><br>
7-
Zero-dependency barcode & QR code SVG generator.
7+
Zero-dependency barcode & QR code generator — SVG & PNG output.
88
<br>
99
40+ formats, styled QR codes, tree-shakeable. Pure TypeScript, works everywhere.
1010
<br><br>
@@ -57,6 +57,7 @@ import { qrcode } from "etiket/qr"; // QR codes only
5757
import { datamatrix } from "etiket/datamatrix";
5858
import { pdf417 } from "etiket/pdf417";
5959
import { aztec } from "etiket/aztec";
60+
import { barcodePNG, qrcodePNG } from "etiket/png"; // PNG output
6061
```
6162

6263
## Supported Formats
@@ -235,12 +236,22 @@ import {
235236
barcodeBase64,
236237
qrcodeBase64,
237238
qrcodeTerminal,
239+
barcodePNG,
240+
qrcodePNG,
241+
barcodePNGDataURI,
242+
qrcodePNGDataURI,
238243
} from "etiket";
239244

245+
// SVG
240246
const svg = qrcode("Hello"); // SVG string
241247
const uri = qrcodeDataURI("Hello"); // data:image/svg+xml,...
242248
const b64 = qrcodeBase64("Hello"); // data:image/svg+xml;base64,...
243249
const term = qrcodeTerminal("Hello"); // Terminal (UTF-8 blocks)
250+
251+
// PNG (zero-dependency raster output)
252+
const png = qrcodePNG("Hello"); // Uint8Array
253+
const pngUri = qrcodePNGDataURI("Hello"); // data:image/png;base64,...
254+
const barPng = barcodePNG("12345", { type: "code128" }); // Uint8Array
244255
```
245256

246257
## Convenience Helpers
@@ -318,14 +329,21 @@ import {
318329
renderBarcodeSVG,
319330
renderQRCodeSVG,
320331
renderMatrixSVG,
332+
renderBarcodePNG,
333+
renderMatrixPNG,
321334
} from "etiket";
322335

323336
const bars = encodeCode128("data"); // number[] (bar/space widths)
324337
const matrix = encodeQR("data"); // boolean[][] (QR matrix)
325338
const dm = encodeDataMatrix("data"); // boolean[][] (Data Matrix)
326339

340+
// SVG rendering
327341
const svg = renderBarcodeSVG(bars, { height: 100 });
328342
const qrSvg = renderQRCodeSVG(matrix, { size: 400, dotType: "dots" });
343+
344+
// PNG rendering
345+
const png = renderBarcodePNG(bars, { height: 100, scale: 2 });
346+
const qrPng = renderMatrixPNG(matrix, { moduleSize: 10, margin: 4 });
329347
```
330348

331349
## Industry Standards
@@ -399,6 +417,7 @@ barcode("HELLO", { color: "currentColor", background: "transparent" });
399417
- Tree-shakeable sub-path exports
400418
- CLI tool (`npx etiket`)
401419
- SVG string output (no DOM required) + `optimizeSVG()` for compact inline
420+
- PNG raster output (pure JS, zero dependencies) via `etiket/png`
402421
- SVG accessibility (`ariaLabel`, `role`, `title`, `desc`)
403422
- Measurement units (`px`, `mm`, `in`, `cm`, `pt`) for print use cases
404423
- CSS `currentColor` support for theme-aware barcodes
@@ -430,11 +449,11 @@ barcode("HELLO", { color: "currentColor", background: "transparent" });
430449
| Convenience helpers (WiFi, vCard...) | :white_check_mark: | :x: | :x: | :x: | :x: |
431450
| Input validation | :white_check_mark: | :x: | :x: | :x: | :x: |
432451
| SVG output | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
433-
| PNG/Canvas output | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
452+
| PNG output | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
434453
| Pure ESM | :white_check_mark: | :white_check_mark: | :x: (CJS) | :x: (CJS) | :x: (CJS) |
435454
| Bundle size (gzip) | ~24KB | ~12KB | ~160KB | ~15KB | ~30KB+deps |
436455

437-
**etiket is the only library that combines** 1D barcodes + 2D codes + styled QR codes + zero dependencies + tree-shaking in a single package.
456+
**etiket is the only library that combines** 1D barcodes + 2D codes + styled QR codes + SVG & PNG output + zero dependencies + tree-shaking in a single package.
438457

439458
## Inspiration & Credits
440459

@@ -459,7 +478,6 @@ Contributions are welcome! Here are some areas where help is especially apprecia
459478

460479
**Other contributions:**
461480

462-
- PNG/raster output support ([#3](https://github.com/productdevbook/etiket/issues/3))
463481
- Data Matrix DMRE rectangular sizes ([#71](https://github.com/productdevbook/etiket/issues/71))
464482
- GS1 DataBar stacked variants ([#61](https://github.com/productdevbook/etiket/issues/61))
465483
- Round-trip scan tests for experimental formats

build.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineBuildConfig({
1111
"./src/datamatrix.ts",
1212
"./src/pdf417.ts",
1313
"./src/aztec.ts",
14+
"./src/png.ts",
1415
"./src/cli.ts",
1516
],
1617
minify: true,

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# etiket
22

3-
Zero-dependency barcode & QR code SVG generator. 20+ formats, styled QR codes, tree-shakeable. Pure TypeScript, works everywhere.
3+
Zero-dependency barcode & QR code generator — SVG & PNG output. 40+ formats, styled QR codes, tree-shakeable. Pure TypeScript, works everywhere.
44

55
## Why etiket?
66

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "etiket",
33
"version": "0.8.1",
4-
"description": "Zero-dependency barcode & QR code SVG generator. 40+ formats, styled QR codes, tree-shakeable. Pure TypeScript, works everywhere.",
4+
"description": "Zero-dependency barcode & QR code generator — SVG & PNG output. 40+ formats, styled QR codes, tree-shakeable. Pure TypeScript, works everywhere.",
55
"keywords": [
66
"aztec",
77
"barcode",
@@ -21,9 +21,11 @@
2121
"msi",
2222
"pdf417",
2323
"pharmacode",
24+
"png",
2425
"qr",
2526
"qr-code",
2627
"qrcode",
28+
"raster",
2729
"svg",
2830
"tree-shakeable",
2931
"typescript",
@@ -74,6 +76,10 @@
7476
"./aztec": {
7577
"types": "./dist/aztec.d.mts",
7678
"default": "./dist/aztec.mjs"
79+
},
80+
"./png": {
81+
"types": "./dist/png.d.mts",
82+
"default": "./dist/png.mjs"
7783
}
7884
},
7985
"scripts": {

src/_barcode.ts

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,120 +24,109 @@ import { encodePOSTNET, encodePLANET } from "./encoders/postnet";
2424
import { encodePlessey } from "./encoders/plessey";
2525
import { renderBarcodeSVG } from "./renderers/svg/barcode";
2626
import { svgToDataURI, svgToBase64 } from "./renderers/data-uri";
27-
import type { BarcodeOptions } from "./_types";
27+
import type { BarcodeEncodingOptions, BarcodeOptions } from "./_types";
2828

2929
/**
30-
* Generate a barcode as SVG string
30+
* Encode barcode text to bar width pattern
3131
*/
32-
export function barcode(text: string, options: BarcodeOptions = {}): string {
32+
export function encodeBars(text: string, options: BarcodeEncodingOptions = {}): number[] {
3333
const {
3434
type = "code128",
3535
msiCheckDigit,
3636
code39CheckDigit,
3737
codabarStart,
3838
codabarStop,
3939
code128Charset,
40-
...svgOptions
4140
} = options;
4241

43-
let bars: number[];
44-
4542
switch (type) {
4643
case "code128":
47-
bars = encodeCode128(text, code128Charset ? { charset: code128Charset } : undefined);
48-
break;
44+
return encodeCode128(text, code128Charset ? { charset: code128Charset } : undefined);
4945
case "ean13":
50-
bars = encodeEAN13(text).bars;
51-
break;
46+
return encodeEAN13(text).bars;
5247
case "ean8":
53-
bars = encodeEAN8(text).bars;
54-
break;
48+
return encodeEAN8(text).bars;
5549
case "code39":
56-
bars = encodeCode39(text, { checkDigit: code39CheckDigit });
57-
break;
50+
return encodeCode39(text, { checkDigit: code39CheckDigit });
5851
case "code39ext":
59-
bars = encodeCode39Extended(text, { checkDigit: code39CheckDigit });
60-
break;
52+
return encodeCode39Extended(text, { checkDigit: code39CheckDigit });
6153
case "code93":
62-
bars = encodeCode93(text);
63-
break;
54+
return encodeCode93(text);
6455
case "code93ext":
65-
bars = encodeCode93Extended(text);
66-
break;
56+
return encodeCode93Extended(text);
6757
case "itf":
68-
bars = encodeITF(text);
69-
break;
58+
return encodeITF(text);
7059
case "itf14":
71-
bars = encodeITF14(text);
72-
break;
60+
return encodeITF14(text);
7361
case "upca":
74-
bars = encodeUPCA(text).bars;
75-
break;
62+
return encodeUPCA(text).bars;
7663
case "upce":
77-
bars = encodeUPCE(text).bars;
78-
break;
64+
return encodeUPCE(text).bars;
7965
case "ean2":
80-
bars = encodeEAN2(text);
81-
break;
66+
return encodeEAN2(text);
8267
case "ean5":
83-
bars = encodeEAN5(text);
84-
break;
68+
return encodeEAN5(text);
8569
case "codabar":
86-
bars = encodeCodabar(text, { start: codabarStart, stop: codabarStop });
87-
break;
70+
return encodeCodabar(text, { start: codabarStart, stop: codabarStop });
8871
case "msi":
89-
bars = encodeMSI(text, { checkDigit: msiCheckDigit });
90-
break;
72+
return encodeMSI(text, { checkDigit: msiCheckDigit });
9173
case "pharmacode":
92-
bars = encodePharmacode(Number(text));
93-
break;
74+
return encodePharmacode(Number(text));
9475
case "code11":
95-
bars = encodeCode11(text);
96-
break;
76+
return encodeCode11(text);
9777
case "gs1-128":
98-
bars = encodeGS1128(text);
99-
break;
78+
return encodeGS1128(text);
10079
case "identcode":
101-
bars = encodeIdentcode(text);
102-
break;
80+
return encodeIdentcode(text);
10381
case "leitcode":
104-
bars = encodeLeitcode(text);
105-
break;
82+
return encodeLeitcode(text);
10683
case "postnet": {
10784
const heights = encodePOSTNET(text);
108-
bars = [];
85+
const bars: number[] = [];
10986
for (const _h of heights) {
11087
bars.push(1);
11188
bars.push(1);
11289
}
11390
bars.pop();
114-
break;
91+
return bars;
11592
}
11693
case "planet": {
11794
const heights = encodePLANET(text);
118-
bars = [];
95+
const bars: number[] = [];
11996
for (const _h of heights) {
12097
bars.push(1);
12198
bars.push(1);
12299
}
123100
bars.pop();
124-
break;
101+
return bars;
125102
}
126103
case "plessey":
127-
bars = encodePlessey(text);
128-
break;
104+
return encodePlessey(text);
129105
case "gs1-databar":
130-
bars = encodeGS1DataBarOmni(text);
131-
break;
106+
return encodeGS1DataBarOmni(text);
132107
case "gs1-databar-limited":
133-
bars = encodeGS1DataBarLimited(text);
134-
break;
108+
return encodeGS1DataBarLimited(text);
135109
case "gs1-databar-expanded":
136-
bars = encodeGS1DataBarExpanded(text);
137-
break;
110+
return encodeGS1DataBarExpanded(text);
138111
default:
139112
throw new Error(`Unsupported barcode type: ${type}`);
140113
}
114+
}
115+
116+
/**
117+
* Generate a barcode as SVG string
118+
*/
119+
export function barcode(text: string, options: BarcodeOptions = {}): string {
120+
const {
121+
type: _type,
122+
msiCheckDigit: _msi,
123+
code39CheckDigit: _c39,
124+
codabarStart: _cbStart,
125+
codabarStop: _cbStop,
126+
code128Charset: _c128,
127+
...svgOptions
128+
} = options;
129+
const bars = encodeBars(text, options);
141130

142131
return renderBarcodeSVG(bars, {
143132
...svgOptions,

0 commit comments

Comments
 (0)