Receipts, but delightful. Turn structured receipt JSON into beautiful, shareable, printable receipts β designed right in your browser.
English Β· ηΉι«δΈζ Β· ζ₯ζ¬θͺ
One receipt JSON in β SVG, HTML, and PNG out. Same data, many surfaces β with a delightful direct-manipulation editor on top. Built for artist-alley booths, doujin events, craft markets, pop-up stores, and local-first POS tools (e.g. OpenBooth).
A receipt doesn't have to be ugly. For a creator at a booth, the receipt is a brand touchpoint β
a tiny gift the customer keeps, scans, and shares. receipt-engine makes that easy while staying a
neutral, embeddable library: pass it a receipt JSON, get back PNG / SVG / HTML β or hand a merchant
the browser editor and let them design their own.
- ποΈ Direct-manipulation editor β tap text to restyle, drag stickers to scale/rotate, drag the card edges to resize, drag blocks to reorder. Runs entirely in the browser, on phones too.
- π One schema, many outputs β SVG (canonical) Β· HTML Β· PNG, all deterministic.
- π¨ Themes β
custom(colorful, change colors / fonts / stickers) andthermal(monospace, auto-grayscaled images), plus fully custom themes viamergeTheme. - ποΈ 8 ready-made starter templates β minimal Β· doujin booth Β· zine press Β· cafΓ© Β· craft market Β· pixel arcade Β· boutique Β· neon night β each with its own font, palette and personality. Pick one and tweak, or start blank.
- πͺ Any currency β pick a code or just type your own symbol (
NT$,Β₯, β¦); the editor renders it as-is. - βΆ Borderless vector stickers β a clean, solid-fill mark set (no emoji), scalable / rotatable on the canvas.
- π¨οΈ Thermal printing β ESC/POS raster (GS v 0) over Web Bluetooth, straight from the browser.
- π² Browser PNG & share β rasterize to PNG client-side (canvas) and share via Web Share β no server.
- π i18n β the editor UI ships in δΈζ / ζ₯ζ¬θͺ / English. One gap: in the Bluetooth print panel only the controls are translated β its runtime status line, error hints and diagnostics labels are Traditional Chinese only.
- π QR codes, π’ smart totals, π§± custom blocks, π§© React component + CLI + typed core.
- π‘οΈ Safe & deterministic β every user value is escaped; the receipt never leaves the browser for a server.
Or run it locally (pnpm monorepo β packages aren't published to npm yet):
pnpm install
pnpm build
pnpm test
# serve the static editor (any static file server works), then open the printed URL:
npx serve apps/playground/publicServe it rather than opening apps/playground/public/index.html from file://: font
embedding fetches the bundled faces same-origin, which a file:// page can't do, and Web
Bluetooth needs a secure context β localhost counts, file:// doesn't.
What you can do in the editor
- Start from a template β 8 ready-made styles (or blank), then make it yours.
- Tap any text β a contextual inspector to change its content, font, color, size and weight
(saved per-element in
styleOverrides); double-tap to edit text inline. - Tap a sticker β a Photoshop-style frame: corner handles scale, a top handle rotates, two-finger pinch on touch; drag to move with alignment snapping.
- Drag the card edges β change width / top / bottom padding.
- Drag a section (or use the layout-order β/β panel) β reorder blocks (
blockOrder). - Upload a logo / background (background is scalable, rotatable, transparent-able), pick
colors & fonts, toggle a transparent background / card / QR backing, switch
custom/thermal, save/restore a config, and download PNG with the fonts embedded so the export matches the preview.
The editor only mutates the receipt model β exports stay deterministic and editor-metadata-free.
| Package | What it does |
|---|---|
@receipt-engine/core |
Schema, validation, normalization, totals. |
@receipt-engine/themes |
Built-in themes + getTheme / mergeTheme. |
@receipt-engine/render-svg |
Receipt β SVG string (canonical). |
@receipt-engine/render-html |
Receipt β standalone HTML. |
@receipt-engine/render-png |
Receipt β PNG Buffer (resvg, server-side). |
@receipt-engine/bitmap |
1-bit dithering + bit-packing for thermal printers. |
@receipt-engine/escpos |
ESC/POS commands + raster output (GS v 0). |
@receipt-engine/connect |
Browser delivery: Web Bluetooth thermal print, canvas PNG, Web Share. API below. |
@receipt-engine/import |
POS / order β receipt adapters (incl. OpenBooth) + template overlay. |
@receipt-engine/react |
<ReceiptCard />. |
@receipt-engine/cli |
receipt-engine render β¦. |
Apps: apps/playground β the static in-browser editor (deployed above) Β·
apps/openbooth-bridge β the OpenBooth β receipt-engine integration bundle.
import { renderReceiptToSvg } from '@receipt-engine/render-svg'
import { renderReceiptToPng } from '@receipt-engine/render-png'
const svg = renderReceiptToSvg(receipt, { theme: 'custom', width: 720 })
const png = await renderReceiptToPng(receipt, { theme: 'custom', pixelRatio: 2 })import { ReceiptCard } from '@receipt-engine/react'
export const App = () => <ReceiptCard receipt={receipt} theme="custom" width={360} />CLI
# from the repo, the CLI runs via the dev script:
pnpm --filter @receipt-engine/cli dev render examples/cute-booth/receipt.json --theme custom --format svg --out receipt.svg
# once built, the bin is available:
receipt-engine render receipt.json --theme custom --format png --out receipt.pngOptions: --theme custom|thermal, --format svg|html|png, --out <path>, --width <number>,
--pretty. svg/html print to stdout when --out is omitted.
Theme customization
import { getTheme, mergeTheme } from '@receipt-engine/themes'
import { renderReceiptToSvg } from '@receipt-engine/render-svg'
const theme = mergeTheme(getTheme('custom'), {
palette: { primary: '#0b7285', accent: '#0b7285' },
})
const svg = renderReceiptToSvg(receipt, { theme })@receipt-engine/connect is the browser-side delivery package. Its faΓ§ade is two pieces β
a function that turns a receipt into something you can show and something you can send,
and a printer you can talk to β so a caller never has to know about GS v 0,
bytes-per-row, band limits, dithering or GATT characteristics.
import { renderReceipt, Printer, GPRINTER_BLE_80 } from '@receipt-engine/connect'
// preview + bytes + measurements, in one call
const { preview, escposBytes, metadata } = await renderReceipt(receipt, {
printer: GPRINTER_BLE_80, // default; also GENERIC_BLE_58 / GENERIC_BLE_80
dots: 576, // optional override of the profile's paper width
bitmap: { dither: 'hybrid', inkFloor: 145 }, // optional 1-bpp knobs
render: { cropToCard: true, hideCardBorder: true }, // optional RenderSvgOptions (minus `paper`)
job: { feedAfterPrintMm: 20 }, // optional feed / cut / blank-run elision
})
document.querySelector('#preview')!.innerHTML = preview // SVG string, safe to inject
console.log(metadata.estimatedLengthMm, metadata.estimatedReceiptsPerRoll)
const printer = new Printer({ profile: GPRINTER_BLE_80, onStateChange: showStatus })
if (Printer.supported) {
await printer.connect() // must be called from a user gesture
await printer.print(escposBytes) // resolves when fully written
printer.disconnect()
}renderReceipt() runs the real production path β native-width SVG β canvas raster β
1-bpp β ESC/POS β so the preview and the bytes can never disagree. It is browser-only
(rasterizing needs a canvas). Its defaults are the thermal theme, a transparent page
background (thermal paper is already white; a background would only burn ink), and the
hybrid conversion rather than error diffusion, which would stipple glyphs and roughly
triple the bytes sent. render overrides the first two and accepts the rest of
RenderSvgOptions; paper is the one thing it cannot override, since the preview and
the bytes have to describe the same sheet as the printer they were built for.
The bitmap knobs are @receipt-engine/bitmap's ToBitmapOptions: dither
('none' | 'floyd-steinberg' | 'atkinson' | 'hybrid' | 'halftone'), threshold β the
global luminance cutoff, default 128, used by the first three β and inkFloor /
paperCeil (defaults 96 / 250), the solid-ink and bare-paper clamps that hybrid and
halftone use instead, so type stays solid and paper stays clean while only the
mid-tones get screened. halftone additionally takes spot
(round | diamond | line | heart | star) and cellSize (default 8 dots, about 1mm at
203 dpi). ToBitmapOptions, DitherMode, SpotShape, PrintJobOptions,
PaperProfile and ReceiptMetadata are all re-exported from
@receipt-engine/connect, so a caller can type these options without reaching for the
underlying packages.
Printer is deliberately thin: connect(), print(bytes, opts?), disconnect(),
getState(), getDiagnostics() (device name, service/characteristic UUIDs, write mode,
byte counters), plus Printer.supported for "does this browser have Web Bluetooth at
all" β Android Chrome yes, iOS Safari no. Writes are queued internally, so concurrent
print() calls can't interleave into a garbled receipt, and a failed write rejects
rather than silently reporting success. raw exposes the underlying transport for tests
and custom flows.
Paper profiles (@receipt-engine/core) are the single source of truth for "how wide
is this receipt, in dots" β layout width, raster width and the GS v 0 header all have
to agree on that number:
| Profile | Paper | Printable | Bytes/row | Side padding | QR box | Max logo |
|---|---|---|---|---|---|---|
PAPER_58 |
58mm | 384 dots | 48 | 16 dots | 120 dots | 160 Γ 76 dots |
PAPER_80 |
80mm | 576 dots | 72 | 24 dots | 180 dots | 240 Γ 114 dots |
Both run at 203 dpi with a 12mm post-print feed and no outer margin β on a roll, the
printable width is the paper, so a margin is just wasted roll. Look one up by id with
getPaperProfile('80mm'). Pass a profile as renderReceiptToSvg's paper option and the
receipt is laid out at that dot width, so an 80mm receipt is a genuine 576-dot design
rather than a 384-dot one scaled up (which would smear every raster edge).
Printer profiles (@receipt-engine/connect) say how to talk to a given machine β
GATT service hints, default BLE pacing, cutter capability, post-print feed, and the
largest GATT write it accepts where that has been measured on hardware. Built in:
GPRINTER_BLE_80 ('gprinter-ble-80', 80mm, no cutter, 180-byte write ceiling),
GENERIC_BLE_58 and GENERIC_BLE_80; getPrinterProfile(id) looks them up.
The playground's Bluetooth print panel is built on all of this β image-processing modes,
threshold and artwork-density controls, feed distance, transfer pacing and a true 1-bit
preview are documented in
apps/playground/README.md.
Full field reference in docs/schema.md. Ready-made examples live in
examples/ (simple, cute-booth, openbooth-like).
The rendering paths are pure front-end JavaScript β SVG, HTML, and PNG (canvas, via
@receipt-engine/connect) all run directly in a mobile browser, no server required. That's how the
playground renders, exports PNG, and even thermal-prints over Web Bluetooth on a phone.
(@receipt-engine/render-png is a separate server-side path using a native module, for batch / Node.)
The simplest way to use it: open the deployed editor
on your phone. To embed rendering in your own app (React Native / WebView), import
@receipt-engine/render-svg or @receipt-engine/render-html directly.
Schema Β· Themes Β· Rendering Β· Roadmap
Shipped in-browser editor Β· browser PNG export Β· ESC/POS thermal print over Web Bluetooth Β·
58mm / 80mm paper presets + printer profiles Β· test-print & length estimate Β·
OpenBooth integration Β· δΈ/ζ₯/θ± i18n.
Next resvg-wasm PNG path Β· more templates Β· hosted receipt pages Β· coupon QR Β·
community themes Β· plugin system.
Full list in docs/roadmap.md.
MIT Β© mimito


{ "schemaVersion": "0.1", "currency": "TWD", "merchant": { "name": "Mimito Booth", "subtitle": "ζδ½ Γ ζη« Γ ε°θͺ", "logo": "./assets/logo.svg" }, "event": { "name": "Artist Alley", "boothNumber": "A12" }, "transaction": { "receiptNo": "AA-A12-018", "issuedAt": "2026-06-01T14:30:00+08:00" }, "items": [ { "name": "Sticker Set", "quantity": 2, "unitPrice": 120, "tags": ["ζ°ε"] }, { "name": "Mini Zine", "quantity": 1, "unitPrice": 180, "tags": ["ηΉε Έ"] } ], "discounts": [{ "label": "Set deal", "amount": 50 }], "payments": [{ "method": "Cash", "amount": 700 }], "qr": { "value": "https://instagram.com/mimito.art", "label": "θΏ½θΉ€ζε" }, "message": { "title": "Thank you!", "body": "ζθ¬ζ―ζζεηζ€δ½οΌ" } }