MCPcopy Index your code
hub / github.com/eljojo/estrella

github.com/eljojo/estrella @v0.1.2

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.1.2 ↗ · + Follow
1,803 symbols 4,079 edges 127 files 414 documented · 23% updated 7d agov0.1.2 · 2026-02-12★ 104
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

⭐️ 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.

My Desk

Photo Printing

The web UI supports printing photos with real-time dithered preview:

  • Formats: JPEG, PNG, GIF, WEBP, HEIC (iPhone photos — requires Nix build; .deb users convert to JPEG first)
  • Adjustments: Rotation, brightness, contrast
  • Dithering: Choose algorithm for best results
  • Auto-resize to 576px printer width

Screenshot 2026-01-23 at 18 19 25

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 height field 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

Demo Receipt

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
Floyd-Steinberg Atkinson Jarvis Bayer

Pattern Generation

The web ui allows to preview patterns

Procedural patterns for artistic prints and printer calibration. Each pattern has randomizable parameters.

Ripple Waves Plasma
Ripple Waves Plasma

More patterns

Op Art

Riley Vasarely Scintillate Moire
Riley Vasarely Scintillate Moire

Organic

Topography Rings Flowfield Mycelium
Topography Rings Flowfield Mycelium
Erosion Crystal Reaction Diffusion Voronoi
Erosion Crystal Reaction Diffusion Voronoi

Glitch

Glitch Corrupt Barcode Databend Scanline Tear
Glitch Corrupt Barcode Databend Scanline Tear

Generative

Attractor Automata Estrella
Attractor Automata Estrella

Textures

Crosshatch Stipple Woodgrain Weave
Crosshatch Stipple Woodgrain Weave

Calibration

Calibration Microfeed Density Jitter
Calibration Microfeed Density Jitter
Overburn
Overburn

Pattern Weaving

Blend multiple patterns with DJ-style crossfade transitions:

estrella weave ripple plasma waves --length 200mm --crossfade 30mm

Weave Crossfade

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 preview - POST /api/json/print — sends to printer

Full component reference

Each component in the "document" array has a "type" field and type-specific properties:

Type Required Optional (defaults)
text content bold, underline, upperline, invert, upside_down, reduced (false); smoothing (null/auto); align ("left"), center, right (false); size (1, default Font A — 0=Font B, 2=double, 3=triple, or [h,w]); scale (null); double_width, double_height (false); inline (false); font (null — set "ibm" for IBM Plex Sans)
header content variant: "normal" (2x2 centered bold) or "small" (1x1)
banner content size (3, max expansion 0–3, auto-cascades width); border: "single"/"double"/"heavy"/"shade"/"shadow"; bold (true); padding (1); font (null — set "ibm" for IBM Plex Sans)
line_item name, price width (48)
total amount label ("TOTAL:"), bold (true), double_width (false), align ("right")
divider style: "dashed" / "solid" / "double" / "equals"; width (48)
spacer one of: mm, lines, units
blank_line
columns left, right width (48), bold, underline, invert (false)
table rows headers (null), border: "single"/"double"/"mixed"/"heavy"/"shade" (default: "single"); align ([] — per-column: "left"/"center"/"right"); row_separator (false); width (48)
markdown content show_urls (false)
qr_code data cell_size (4), error_level ("M"), align ("center")
pdf417 data module_width (3), ecc_level (2), align ("center")
barcode format, data height (80); format: "code128" / "code39" / "ean13" / "upca" / "itf"
image url dither ("floyd-steinberg"), width (576), height (null), align ("center" — also "left", "right"; only affects images narrower than paper)
pattern name height (500), params ({}), dither ("bayer")
canvas elements height (auto), width (576), dither ("auto" — detects continuous-tone content); each element: position ({x, y}), blend_mode ("normal"), opacity (1.0) + any component fields
nv_logo key center (false), scale (1), scale_x (1), scale_y (1)

Text size controls both font selection and character expansion using a 1-indexed model:

Size Font Expansion Chars/line Description
0 B (9×24) none 64 Small text
1 A (12×24) none 48 Normal (default)
2 A 24 Double
3 A 16 Triple

Pass a single number for uniform scaling ("size": 2 = double height and width), or an [h, w] array for independent control ("size": [3, 1] = triple height, normal width, 48 chars/line).

The banner component uses the same sizing model but auto-selects the largest width that fits. Given "size": 3, it tries widths 3→2→1→Font B until the content fits inside the box-drawing frame.

cut at the top level defaults to true. Set to false to suppress the paper cut.

How It Works: The Compilation Pipeline

Document components emit an intermediate representation (IR), which gets optimized before generating StarPRNT bytes:

``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ COMPILATION PIPELINE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────┐ │ │ │ Document │ │ IR │ │ Optimizer │ │ Bytes │ │ │ │ │ │ │ │ │ │ │ │ │ │ Text │ │ Op::Init │ │ 4 passes │ │ ESC @ │ │ │ │ LineItem │ ───► │ Op::Text │ ───► │ that remove│ ───► │ ... │ │ │ │ QrCode │ emit │

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Function 1,055
Method 551
Class 151
Enum 33
Interface 13

Languages

Rust93%
TypeScript7%

Modules by API surface

src/protocol/text.rs62 symbols
src/document/mod.rs54 symbols
src/render/chart.rs47 symbols
src/protocol/barcode.rs46 symbols
src/document/layout.rs46 symbols
src/document/types.rs45 symbols
src/ir/optimize.rs42 symbols
src/render/dither.rs34 symbols
frontend/src/components/ComponentEditor.tsx34 symbols
tests/golden_tests.rs32 symbols
src/document/canvas.rs30 symbols
src/preview/mod.rs29 symbols

For agents

$ claude mcp add estrella \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page