Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 266 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

### Setup
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install -r requirements-dev.txt # For development
```

### Running the dev server
```bash
make dev # Runs on 127.0.0.1:8000
make dev HOST=0.0.0.0 PORT=8008 # Custom host/port
```

The dev server uses uvicorn with auto-reload enabled.

### Testing
```bash
pytest # Run all tests
pytest tests/test_app.py # Run specific test file
pytest tests/test_app.py::test_name # Run single test
pytest --cov=. --cov-report=xml # With coverage report
```

### Formatting
```bash
make format # Runs black formatter
```

### Image processing script
```bash
python notebooks/image_processing.py # Auto-picks image
python notebooks/image_processing.py -i path/to/image.png # Specific image
python notebooks/image_processing.py -w 463 # Custom width
```

## Architecture Overview

Ditherbooth is a FastAPI service that converts photos to 1-bit dithered prints for Zebra LP2844 label printers.

### Core modules

**`ditherbooth/app.py`** (main FastAPI application)
- `/print` endpoint: accepts image upload, media type, and printer language
- `/preview` endpoint: returns processed 1-bit PNG without printing
- Dev settings API (`/api/dev/settings`) with password protection
- Public config API (`/api/public-config`) — includes `media_dimensions` for canvas sizing
- Template CRUD API (`/api/templates`) — GET list, POST create, GET by ID, DELETE

**`ditherbooth/imaging/process.py`**
- `to_1bit()`: Core image processing function
- Reads image bytes, applies EXIF orientation
- Converts to grayscale
- Resizes to fit target width and optional max height (preserves aspect ratio)
- Pastes centered on white canvas
- Converts to 1-bit using Floyd-Steinberg dithering

**`ditherbooth/printer/epl.py`** and **`ditherbooth/printer/zpl.py`**
- `img_to_epl_gw()`: Converts 1-bit PIL image to EPL2 GW command
- `img_to_zpl_gf()`: Converts 1-bit PIL image to ZPL GF command
- Both encode pixel data as raw bytes (white pixels set bit high)

**`ditherbooth/printer/cups.py`**
- `spool_raw()`: Writes payload to temp file and sends to CUPS via `lpr -o raw`

### Image processing pipeline

1. **Upload** → FastAPI receives image file + media preset + language
2. **Dither** → `to_1bit()` resizes and converts to 1-bit B/W
3. **Trim** (continuous media only) → `trim_bottom_white()` removes trailing blank rows
4. **Encode** → EPL or ZPL encoder converts to printer language bytes
5. **Print** → `spool_raw()` sends to CUPS printer queue

### Media presets and dimensions

Defined in `MEDIA_DIMENSIONS` dict in `app.py`:
- `continuous58`: 463 dots wide, no fixed height
- `continuous80`: 640 dots wide, no fixed height (default)
- `label100x150`: 800×1200 dots (fixed label)
- `label55x30`: 440×240 dots (fixed label)
- `label50x30`: 400×240 dots (fixed label)

**Continuous media behavior:**
- Trailing white rows are trimmed via `trim_bottom_white()` to avoid excessive feed
- Label height is set to a small value (Q=16 ≈ 2mm post-print spacing)
- Gap is set to 0

**Fixed label behavior:**
- Image is resized to fit within fixed width×height (contain mode, no cropping)
- Label height matches media height
- Gap is omitted (printer uses calibrated gap detection)

### Configuration system

**Config file location:**
- `DITHERBOOTH_CONFIG_PATH` env var (if set)
- Otherwise: `$XDG_CONFIG_HOME/ditherbooth/config.json`
- Otherwise: `~/.config/ditherbooth/config.json`

**Config keys:**
- `test_mode` (bool): Skip actual printing, return test response
- `default_media` (str): Default media preset
- `default_lang` (str): "EPL" or "ZPL"
- `lock_controls` (bool): Hide media/lang selectors in UI (kiosk mode)
- `printer_name` (str): Override CUPS queue name
- `test_mode_delay_ms` (int): Simulate print delay in test mode
- `epl_darkness` (int, 0-15): EPL darkness setting (D command)
- `epl_speed` (int, 1-6): EPL speed setting (S command)

**Dev settings API:**
- Password required via `X-Dev-Password` header
- Password from `DITHERBOOTH_DEV_PASSWORD` env (default: "dev")

### Environment variables

- `DITHERBOOTH_PRINTER`: CUPS queue name (default: "Zebra_LP2844")
- `DITHERBOOTH_DEV_PASSWORD`: Password for settings API (default: "dev")
- `DITHERBOOTH_CONFIG_PATH`: Custom config file location

### Printer setup

**macOS:**

macOS no longer supports raw CUPS queues. Instead, use the EPL2 driver:

```bash
# Find the USB device
lpinfo -v | grep -i zebra

# Create printer with EPL2 driver (update serial number to match your printer)
sudo lpadmin -p Zebra_LP2844 -E \
-v "usb://Zebra/LP2844?serial=42J103400046" \
-m drv:///sample.drv/zebraep2.ppd

# Enable and accept jobs
cupsenable Zebra_LP2844
cupsaccept Zebra_LP2844
```

**Linux:**

Create a raw CUPS queue:
```bash
sudo lpadmin -p Zebra_LP2844 -E -v usb://Zebra/LP2844 -m raw
```

**Test printing:**
```bash
echo -e "N\nq400\nQ200,24\nA50,50,0,3,1,1,N,\"Hello\"\nP1" | lpr -P Zebra_LP2844
```

**Troubleshooting:**

If printer shows as paused or disabled:
```bash
cupsenable Zebra_LP2844 # Enable the printer
cupsaccept Zebra_LP2844 # Accept jobs
lpstat -p Zebra_LP2844 # Check status
```

If jobs are stuck:
```bash
cancel -a Zebra_LP2844 # Cancel all jobs
```

### Designer v2 (Fabric.js label editor)

**`ditherbooth/static/designer-v2.js`** — Fabric.js-based label designer
- Loaded as a tab alongside the photo flow in `index.html`
- Canvas sized to media dimensions from `/api/public-config`
- CSS transform scaling to fit viewport
- Text tool: adds `fabric.IText` objects, font size control, inline editing
- Preview: exports canvas to PNG, POSTs to `/preview` for 1-bit dithered preview
- Print: exports canvas to PNG, POSTs to `/print`
- Templates: save/load/delete via `/api/templates` endpoints
- Multi-label queue: add labels to queue, reorder via drag-and-drop, print all sequentially

**Template storage:**
- Templates stored as JSON files in `{config_dir}/templates/` directory
- Each template: `{ id, name, created_at, canvas_json }` (Fabric.js JSON serialization)
- Template ID = UUID, filename = `{id}.json`

**Multi-label queue (frontend-only):**
- Queue is an in-memory array of `{ id, name, thumbnailDataURL, canvasJSON }` items
- "Add to Queue" captures current canvas state + generates thumbnail via off-screen Fabric.js canvas
- Horizontal scrollable thumbnail strip below the designer canvas
- Drag-to-reorder using HTML5 drag and drop
- Click thumbnail to load canvasJSON back onto main canvas for editing
- "Print Queue" iterates items sequentially, POSTing each to `/print` with progress indicator
- Stops on first error and reports which label failed

**Frontend tab system:**
- Tab bar in `index.html`: Photo | Designer
- Tab switching via JS show/hide in `app.js` (no page reload)
- `app.js` exposes `window.getPublicConfig()` for `designer-v2.js` to access config
- `window.initDesignerV2()` called after DOMContentLoaded + config loaded
- `window.onDesignerTabVisible()` called when designer tab becomes visible (for canvas resize)

**Image elements:**
- "Add Image" button triggers hidden file input, reads file as dataURL, creates `fabric.Image`
- Image auto-scaled to fit within 80% of canvas bounds, centered
- Images are regular Fabric.js objects — movable, scalable, deletable via Delete button

**Smart snapping (PowerPoint-style guidelines):**
- On `object:moving`, collects snap targets from canvas edges, canvas center, and other objects' edges/centers
- Snaps within 8px threshold, draws dashed blue guide lines at snap points
- Falls back to grid snapping when no smart snap hits
- Guide lines are temporary (`_isSnapGuide`), cleared on mouse release
- `canvasToCleanJSON()` strips grid lines from serialization (templates and queue)

**Shapes tool:**
- Toolbar dropdown: Rectangle, Circle, Line
- Shapes created with stroke only (no fill), using current fill color
- Standard Fabric.js objects — movable, scalable, deletable

**Font picker and fill color:**
- Font dropdown: Arial, Courier, Georgia, Impact, Times, Comic Sans
- Fill toggle button switches between black and white (`currentFill` state)
- Applies to new objects and updates selected object on toggle

**Object layering:**
- "Bring Forward" (↑) and "Send Backward" (↓) buttons in toolbar
- Send backward prevents objects from going behind grid lines

**Queue duplication:**
- Duplicate button (⧉) on each queue thumbnail, next to remove (×)
- Deep-copies canvasJSON and reuses thumbnail, inserts after original

**Touch optimization:**
- Larger Fabric.js selection handles on touch devices (`cornerSize: 20`, `touchCornerSize: 40`)
- Increased snap threshold (14px vs 8px) on touch for easier snapping

## Key implementation details

### Image resizing strategy (ditherbooth/imaging/process.py:5-39)

The `to_1bit()` function uses **contain** resizing:
- Calculates scale factor to fit both width constraint AND optional height constraint
- Uses `min(sx, sy)` to ensure image fits within bounds
- No cropping or padding to fill the entire label height
- Centers horizontally on white canvas (unless `center_x=False`)

This differs from "cover" (would crop) or "fill" (would pad/distort).

### EPL encoding details (ditherbooth/printer/epl.py)

EPL format uses the GW (Graphics Write) command:
- Pixels are packed into bytes (8 pixels per byte)
- White pixels → bit set to 1, black pixels → bit set to 0
- Commands: `N` (clear), `D` (darkness), `S` (speed), `q` (width), `Q` (height, gap)
- For continuous media: small Q value (16 dots) creates minimal post-print spacing
- For fixed labels: Q matches label height, gap detection automatic

### Test mode

When `test_mode` is enabled in config:
- `/print` endpoint processes the image but skips `spool_raw()`
- Returns `{"status": "ok", "mode": "test", "bytes": ..., "media": ..., "lang": ...}`
- Optional `test_mode_delay_ms` simulates print latency
- Useful for UI testing without a physical printer
98 changes: 88 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Ditherbooth is a small FastAPI service and single-page app that turns uploaded p

- [Features](#features)
- [Quick Start](#quick-start)
- [Troubleshooting](#troubleshooting)
- [Developer Settings and Test Mode](#developer-settings-and-test-mode)
- [API Test with cURL](#api-test-with-curl)
- [Media Presets](#media-presets)
Expand All @@ -47,11 +48,31 @@ Ditherbooth is a small FastAPI service and single-page app that turns uploaded p
### Prerequisites

* Python 3.9+
* [CUPS](https://www.cups.org/) with a **raw** queue named `Zebra_LP2844`
* [CUPS](https://www.cups.org/) with a printer queue named `Zebra_LP2844`
* Zebra LP2844 or LP2844‑Z printer connected via USB
* `DITHERBOOTH_PRINTER` environment variable (optional) to override the CUPS queue name; defaults to `Zebra_LP2844`

#### Create a raw queue
#### Create a printer queue

**macOS:**

macOS no longer supports raw CUPS queues. Use the EPL2 driver instead:

```bash
# Find your printer's USB device (note the serial number)
lpinfo -v | grep -i zebra

# Create printer with EPL2 driver (update serial number to match yours)
sudo lpadmin -p Zebra_LP2844 -E \
-v "usb://Zebra/LP2844?serial=YOUR_SERIAL_NUMBER" \
-m drv:///sample.drv/zebraep2.ppd

# Enable and accept jobs
cupsenable Zebra_LP2844
cupsaccept Zebra_LP2844
```

**Linux:**

```bash
sudo lpadmin -p Zebra_LP2844 -E -v usb://Zebra/LP2844 -m raw
Expand All @@ -68,15 +89,11 @@ lpstat -p Zebra_LP2844
Print a simple label to confirm the queue works:

```bash
python - <<'PY'
from ditherbooth.printer.cups import spool_raw
payload = (
'N\nq400\nQ200,24\nA50,50,0,3,1,1,N,"Hello"\nP1\n'
)
spool_raw('Zebra_LP2844', payload)
PY
echo -e "N\nq400\nQ200,24\nA50,50,0,3,1,1,N,\"Hello\"\nP1" | lpr -P Zebra_LP2844
```

If successful, the printer should print a small label with "Hello" text.

### Installation

```bash
Expand Down Expand Up @@ -110,7 +127,68 @@ Find your IP:

Open `http://<your-ip>:8000` on your phone. Use the gear icon (Dev Settings) to enable Test Mode so no printer is required.

### Developer settings and test mode
## Troubleshooting

### Printer shows as paused or disabled

If the printer appears paused in System Settings or `lpstat` shows it as disabled:

```bash
cupsenable Zebra_LP2844 # Enable the printer
cupsaccept Zebra_LP2844 # Accept jobs
lpstat -p Zebra_LP2844 # Verify status
```

### Print jobs are stuck in queue

```bash
cancel -a Zebra_LP2844 # Cancel all pending jobs
lpstat -o # Check queue is empty
```

### "Unable to send data to printer" error

This usually means the printer lost communication. Try:

1. Check USB cable is connected
2. Verify printer is powered on and shows a green ready light
3. Check the printer appears in USB devices:
```bash
lpinfo -v | grep -i zebra
```
4. If the device URI is wrong or shows `/dev/null`, reconfigure:
```bash
# Find the correct USB device
lpinfo -v | grep -i zebra

# Update the printer (use your serial number)
sudo lpadmin -p Zebra_LP2844 -v "usb://Zebra/LP2844?serial=YOUR_SERIAL"

# Re-enable
cupsenable Zebra_LP2844
cupsaccept Zebra_LP2844
```

### Web interface returns "Printer error" (502)

The app returns HTTP 502 when CUPS cannot communicate with the printer. Check:

1. Printer status: `lpstat -p Zebra_LP2844`
2. Recent CUPS errors: `tail -20 /var/log/cups/error_log`
3. Follow steps above to enable printer and clear stuck jobs

### Development without a printer

Use test mode to develop without a physical printer:

```bash
curl -X PUT -H "X-Dev-Password: dev" -H "Content-Type: application/json" \
-d '{"test_mode": true}' http://localhost:8000/api/dev/settings
```

Or enable it through the web UI using the gear icon (Dev Settings) with password `dev`.

## Developer settings and test mode

The app exposes a small password-protected settings API to make the frontend configurable without exposing options to end users. These settings are persisted to a JSON file (`ditherbooth_config.json` by default) and are used both by the UI and the print endpoint.

Expand Down
Loading