Estrella
A print server for the StarPRNT protocol
Install / Use
/learn @eljojo/EstrellaREADME
⭐️ estrella
A Rust library for Star Micronics TSP650II thermal receipt printers over Bluetooth. Implements the StarPRNT protocol with a React-inspired component system, an optimizing compiler, and a web UI for photo printing.
Photo Printing
The web UI supports printing photos with real-time dithered preview:
- Formats: JPEG, PNG, GIF, WEBP, HEIC (iPhone photos — requires Nix build;
.debusers convert to JPEG first) - Adjustments: Rotation, brightness, contrast
- Dithering: Choose algorithm for best results
- Auto-resize to 576px printer width
Image Downloads
Documents can include images from URLs. When a document is submitted via the JSON API, images are automatically downloaded, cached, resized, and dithered for printing.
{"type": "image", "url": "https://example.com/photo.jpg"}
- Auto-resize: Images are scaled to the printer's full width (576 dots) preserving aspect ratio
- Max height: Optional
heightfield acts as a cap — if the resized image is taller, it shrinks to fit - Alignment: Images narrower than paper width are centered by default (
"align": "center"). Also accepts"left"or"right" - Dithering: Defaults to Floyd-Steinberg. Set
"dither"to"bayer","atkinson","jarvis", or"none" - Caching: Downloaded images are cached in memory and shared with photo sessions (30-min TTL), so previewing a document multiple times won't re-download
{
"type": "image",
"url": "https://example.com/photo.jpg",
"width": 400,
"height": 300,
"align": "center",
"dither": "atkinson"
}
The Document System
Instead of manually constructing printer escape sequences, Estrella provides a declarative Document model. The same types work for both Rust construction and JSON deserialization — one set of types, zero conversion layer.
use estrella::document::*;
let doc = Document {
document: vec![
Component::Banner(Banner::new("CHURRA MART")),
Component::Text(Text { content: "2026-01-20 12:00:00".into(), center: true, ..Default::default() }),
Component::Spacer(Spacer::mm(3.0)),
Component::Banner(Banner { content: "TODAY ONLY: 50% OFF".into(), border: BorderStyle::Double, size: 2, ..Default::default() }),
Component::Divider(Divider::default()),
Component::LineItem(LineItem::new("Espresso", 4.50)),
Component::LineItem(LineItem::new("Croissant", 3.25)),
Component::Divider(Divider::default()),
Component::Total(Total { amount: 7.75, bold: Some(true), double_width: true, ..Default::default() }),
Component::Spacer(Spacer::mm(3.0)),
Component::QrCode(QrCode { data: "https://example.com/rewards".into(), cell_size: Some(6), ..Default::default() }),
Component::Text(Text { content: "Thank you!".into(), center: true, bold: true, ..Default::default() }),
],
cut: true,
..Default::default()
};
let bytes = doc.build(); // StarPRNT bytes, ready to send
let json = serde_json::to_string(&doc)?; // Same type serializes to JSON

Components
| Component | Description |
|-----------|-------------|
| Text | Styled text (bold, center, invert, size 0–3, optional font: "ibm") |
| Header | Pre-styled centered bold header |
| Banner | Framed text with box-drawing borders, auto-sizing (optional font: "ibm") |
| LineItem | Left name + right price (e.g., "Coffee" ... "$4.50") |
| Total | Right-aligned total line |
| Divider | Horizontal line (dashed, solid, double, equals) |
| Spacer | Vertical space in mm, lines, or raw units |
| Columns | Two-column layout (left + right) |
| Table | Table with box-drawing borders, headers, per-column alignment |
| Markdown | Rich text from Markdown (headings, bold, lists) |
| Image | Image from URL (downloaded, cached, dithered, auto-centered) |
| Pattern | Generative art pattern with params |
| Canvas | Absolute-positioned raster compositing with blend modes |
| QrCode, Pdf417, Barcode | 1D and 2D barcodes |
| NvLogo | Logo from printer's flash memory |
Dithering Algorithms
Thermal printers are binary (black or white), so grayscale images need dithering. Estrella implements four algorithms:
| Algorithm | Characteristics | |-----------|-----------------| | Floyd-Steinberg | Classic error diffusion. Smooth gradients, organic look. Default for photos. | | Atkinson | Bill Atkinson's Mac algorithm. Higher contrast, loses 25% of error intentionally. | | Jarvis | Spreads error over 12 neighbors. Smoothest gradients, slightly slower. | | Bayer | Ordered 8x8 matrix. Fast, deterministic, halftone pattern. Best for patterns. |
| Floyd-Steinberg | Atkinson | Jarvis | Bayer |
|-----------------|----------|--------|-------|
|
|
|
|
|
Pattern Generation
Procedural patterns for artistic prints and printer calibration. Each pattern has randomizable parameters.
|
|
|
|
|:--:|:--:|:--:|
| Ripple | Waves | Plasma |
Op Art
|
|
|
|
|
|:--:|:--:|:--:|:--:|
| Riley | Vasarely | Scintillate | Moire |
Organic
|
|
|
|
|
|:--:|:--:|:--:|:--:|
| Topography | Rings | Flowfield | Mycelium |
|
|
|
|
|
|:--:|:--:|:--:|:--:|
| Erosion | Crystal | Reaction Diffusion | Voronoi |
Glitch
|
|
|
|
|
|:--:|:--:|:--:|:--:|
| Glitch | Corrupt Barcode | Databend | Scanline Tear |
Generative
|
|
|
|
|:--:|:--:|:--:|
| Attractor | Automata | Estrella |
Textures
|
|
|
|
|
|:--:|:--:|:--:|:--:|
| Crosshatch | Stipple | Woodgrain | Weave |
Calibration
|
|
|
|
|
|:--:|:--:|:--:|:--:|
| Calibration | Microfeed | Density | Jitter |
|
|
|:--:|
| Overburn |
Pattern Weaving
Blend multiple patterns with DJ-style crossfade transitions:
estrella weave ripple plasma waves --length 200mm --crossfade 30mm

JSON API
The JSON API uses the same Document type as the Rust API — the component structs are all Serialize + Deserialize, so JSON documents map directly to Rust types with zero conversion. Useful for automations (e.g. Home Assistant daily briefings).
curl -X POST http://localhost:8080/api/json/print \
-H 'Content-Type: application/json' \
-d '{
"document": [
{"type": "banner", "content": "GOOD MORNING"},
{"type": "text", "content": "Monday, January 27", "center": true, "size": 0},
{"type": "divider", "style": "double"},
{"type": "text", "content": " WEATHER ", "bold": true, "invert": true},
{"type": "columns", "left": "Now", "right": "6°C Cloudy"},
{"type": "columns", "left": "High / Low", "right": "11°C / 3°C"},
{"type": "divider"},
{"type": "text", "content": " CALENDAR ", "bold": true, "invert": true},
{"type": "columns", "left": "9:00", "right": "Standup"},
{"type": "columns", "left": "11:30", "right": "Dentist"},
{"type": "divider"},
{"type": "qr_code", "data": "https://calendar.google.com"}
],
"cut": true
}'
The web UI includes a JSON API tab with a live preview editor and a sample daily briefing template.
Canvas components support absolute-positioned compositing with blend modes:
{
"type": "canvas",
"height": 100,
"elements": [
{"type": "pattern", "name": "estrella", "height": 80, "position": {"x": -43, "y": 0}, "blend_mode": "add"},
{"type": "text", "content": "Hello World", "center": true, "position": {"x": 7, "y": 16}, "blend_mode": "add"},
{"type": "total", "amount": 0, "position": {"x": 0, "y": 34}, "blend_mode": "add"}
]
}
Elements without position stack top-to-bottom (flow mode). Dithering defaults to "auto" — Atkinson when continuous-tone content is detected, none otherwise.
Endpoints:
POST /api/json/preview— returns a PNG previewPOST /api/json/print— sends to printer
Each component in the "document" array has a "type" f
