Files
Gracious 90137dd111 Add model table, PrinterError, and port upstream PRs #28/#12
Library changes on top of upstream 35c41a7:

- models.py: MODELS table (max width / density / needs_print_clear per
  model) and prepare_print(), replacing the duplicated width/density
  logic in the CLI. Adds d101 (192px head, tested on 12mm tape).
- _transceive raises PrinterError instead of returning None (callers
  used to crash with AttributeError on a silent printer).
- PR #28: send ALLOW_PRINT_CLEAR / SET_QUANTITY for D-series models
  (labels taller than ~210px failed on D11 without them) and pause 1ms
  between line packets.
- PR #12: replace the fixed sleep after END_PAGE_PRINT with
  get_print_status() polling (30s deadline, fallback for firmwares that
  do not answer). get_print_status returns PrintStatus(finished,
  progress, feed_progress, error).
- set_dimension parameter names match their use.

Verified on a D101 over Bluetooth.
2026-09-21 08:02:58 +01:00

69 lines
2.1 KiB
Python

import logging
from dataclasses import dataclass
from PIL import Image
class PrinterError(Exception):
"""Raised when the printer returns an error or does not respond."""
@dataclass(frozen=True)
class PrinterModel:
name: str
max_width_px: int
max_density: int
# D-series printers need ALLOW_PRINT_CLEAR / SET_QUANTITY before page data
# (labels taller than ~210px fail otherwise). B21 rejects these commands.
needs_print_clear: bool = False
MODELS = {
m.name: m
for m in (
PrinterModel("b1", max_width_px=384, max_density=5),
PrinterModel("b18", max_width_px=384, max_density=3),
PrinterModel("b21", max_width_px=384, max_density=5),
PrinterModel("d11", max_width_px=96, max_density=3, needs_print_clear=True),
# D101 takes up to 25mm tape (192px head); tested on 12mm tape
PrinterModel("d101", max_width_px=192, max_density=3, needs_print_clear=True),
PrinterModel("d110", max_width_px=96, max_density=3, needs_print_clear=True),
)
}
def get_model(name: str) -> PrinterModel:
try:
return MODELS[name.lower()]
except KeyError:
raise ValueError(f"Unknown printer model: {name!r}") from None
def prepare_print(
model: str | PrinterModel, image: Image.Image, density: int, rotate: int = 0
) -> tuple[Image.Image, int]:
"""Rotate/validate an image and clamp density for the given model.
Returns the (possibly rotated) image and the effective density.
"""
if isinstance(model, str):
model = get_model(model)
if density > model.max_density:
logging.warning(
f"{model.name.upper()} only supports density up to {model.max_density}"
)
density = model.max_density
rotate = int(rotate)
if rotate % 360 != 0:
# PIL library rotates counter clockwise, so we need to multiply by -1
image = image.rotate(-rotate, expand=True)
if image.width > model.max_width_px:
raise ValueError(
f"Image width {image.width}px too big for {model.name.upper()} "
f"(max {model.max_width_px}px)"
)
return image, density