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.
69 lines
2.1 KiB
Python
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
|