Fluent ESC/POS receipt builder for thermal printers — works natively in Rust and in any JavaScript runtime via WASM.
Built by Mamadou Sarr — battle-tested on real POS hardware in Dakar, Senegal.
Try the Receipt Builder → — design your receipt visually, then copy the JS code.
Features
- ✅ Fluent builder API — chain calls, get bytes
- ✅ 58mm, 80mm, and A4 paper widths
- ✅ Correct money arithmetic with
rust_decimal— no float rounding errors - ✅ CP858 encoding — French, Spanish, Portuguese accents + Euro sign
- ✅ CODE128 barcodes, EAN-13, QR codes
- ✅ Logo / image printing (raster, native feature)
- ✅ Cash drawer kick
- ✅ WASM/npm — same API in browser (WebUSB / WebSerial) and Node.js
- ✅ Zero unsafe code
- ✅ JSON template engine — define receipts as JSON, render to bytes
- ✅ Image dithering — Floyd-Steinberg dithering, works in WASM (logos from canvas)
- ✅ One-liner browser printing —
ThermoPrinterclass for WebSerial / WebUSB - ✅ PNG / PDF export — render receipts to images for email or archiving
- ✅ Tauri plugin —
tauri-plugin-thermoprintfor desktop POS apps - ✅ i18n — 6 languages (FR, EN, ES, PT, AR, WO)
Installation
Rust / Tauri
[]
= { = "0.1", = ["native"] } # default; includes image support
= { = "1", = ["macros"] } # for the dec!() macro
Minimal (no image support):
[]
= { = "0.1", = false }
= { = "1", = ["macros"] }
npm / JavaScript
Rust Usage
use ;
use *;
let bytes = new
.init
.shop_header
.divider
.item
.item
.divider
.subtotal_ht
.taxes
.total
.received
.change
.divider
.barcode_code128
.served_by
.thank_you
.feed
.cut
.build; // → Vec<u8>
// Send `bytes` to your printer however you like.
// thermoprint never touches the OS — that's your call.
With logo (native feature)
let bytes = new
.init
.align_center
.logo? // resized automatically
.shop_header
// ...
.build;
Cash drawer
let bytes = new
.init
.open_cash_drawer
.build;
JavaScript / TypeScript Usage
import init, { WasmReceiptBuilder } from 'thermoprint';
await init();
// All money amounts are strings — no floating-point surprises
const bytes: Uint8Array = new WasmReceiptBuilder("80mm")
.init()
.shopHeader("MA BOUTIQUE", "+221 77 000 00 00", "Dakar, Sénégal")
.divider("=")
.item("Polo shirt", 2, "15000", null)
.item("Jean Levis 501", 1, "25000", "2000") // with discount
.divider("-")
.subtotalHt("53000")
.total("62540")
.received("70000")
.change("7460")
.divider("=")
.barcodeCode128("ORD-2024-001")
.feed(3)
.cut()
.build(); // → Uint8Array
// Send to printer via WebUSB / WebSerial / Node.js serial port
WebUSB example
const device = await navigator.usb.requestDevice({ filters: [] });
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
await device.transferOut(1, bytes);
WebSerial example
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });
const writer = port.writable.getWriter();
await writer.write(bytes);
writer.releaseLock();
JSON Template Engine
Define receipts as JSON — no code required. Works in Rust, WASM, and the Tauri plugin.
Rust:
use render_json;
let bytes = render_json.unwrap;
JavaScript (WASM):
import init from 'thermoprint';
await ;
const bytes = ;
Supported element types: init, shop_header, text_line, centered, right, row, divider, blank, bold, double_size, double_height, normal_size, underline, align, item, subtotal, tax, discount, total, received, change, served_by, thank_you, barcode_code128, barcode_ean13, qr_code, feed, cut, cut_full, form_feed, open_cash_drawer.
One-Liner Browser Printing
ThermoPrinter handles WebSerial and WebUSB connections automatically.
import from 'thermoprint/printer';
// One-liner: connect → print → disconnect
await ;
// Or with more control
const printer = ;
await printer.; // prompts user to select device
await printer.;
await printer.;
Options: transport ('webserial' or 'webusb'), baudRate, usbFilters, usbEndpoint, chunkSize, chunkDelay.
Image Dithering (WASM + Native)
Convert any image to print-ready ESC/POS raster bytes with Floyd-Steinberg dithering. Pure Rust — works everywhere, no image crate needed.
JavaScript (from canvas):
import init from 'thermoprint';
await ;
const ctx = canvas.;
const imageData = ctx.;
const raster = ;
const receipt =
.....;
Rust:
use ;
let raster = dither_rgba;
PNG / PDF Export
Render receipts to images for email receipts, archiving, or previews. Uses the same JSON template format.
import from 'thermoprint/export';
const exporter = ;
const dataUrl = exporter.; // data:image/png;base64,...
const blob = await exporter.; // Blob
exporter.; // triggers download
exporter.; // triggers PDF download
const canvas = exporter.; // for embedding
Tauri Plugin
For desktop POS apps built with Tauri v2. See tauri-plugin-thermoprint/README.md.
// src-tauri/src/main.rs
import from '@tauri-apps/api/core';
const ports = await ;
await ;
API Reference
ReceiptBuilder::new(width: PrintWidth)
| Method | Description |
|---|---|
.init() |
Reset printer + set code page. Always call first. |
.currency(symbol) |
Override currency symbol (default: "FCFA") |
.align_left/center/right() |
Set text alignment |
.bold(bool) |
Toggle bold |
.double_size(bool) |
Toggle double width + height |
.double_height(bool) |
Toggle double height only |
.normal_size() |
Reset to normal size |
.underline(bool) |
Toggle underline |
.text(s) |
Append text (no newline) |
.text_line(s) |
Append text + newline |
.centered(s) |
Append centred text line |
.right(s) |
Append right-aligned text line |
.row(left, right) |
Two-column row (label + value) |
.divider(ch) |
Full-width divider line |
.blank() |
Blank line |
.feed(n) |
Feed n lines |
.cut() |
Partial cut |
.cut_full() |
Full cut |
.form_feed() |
Page eject (A4) |
.shop_header(name, phone, addr) |
Centred bold header block |
.item(name, qty, price, discount?) |
Line item with optional discount |
.subtotal_ht(amount) |
Subtotal excl. tax |
.discount(amount, coupon?) |
Discount line |
.taxes(entries) |
Multiple tax lines |
.total(amount) |
Grand total (bold, double height) |
.received(amount) |
Amount received |
.change(amount) |
Change to return |
.served_by(name) |
Cashier name footer |
.thank_you(shop_name) |
Thank you footer |
.barcode_code128(value) |
CODE128 barcode |
.barcode_ean13(value) |
EAN-13 barcode |
.qr_code(data, size) |
QR code |
.open_cash_drawer() |
Cash drawer kick |
.logo(path) (native) |
Logo from file |
.logo_raw(bytes) |
Pre-rasterised logo bytes |
.build() |
Finalise → Vec<u8> / Uint8Array |
Building
# Native
# WASM (browser)
# WASM (Node.js)
# Tests
# Publish to crates.io
# Publish to npm
License
MIT © Mamadou Sarr