diff --git a/src/dymoprint/cli/cli.py b/src/dymoprint/cli/cli.py index ea00227..f11a58c 100755 --- a/src/dymoprint/cli/cli.py +++ b/src/dymoprint/cli/cli.py @@ -18,7 +18,6 @@ DEFAULT_MARGIN_PX, PIXELS_PER_MM, USE_QR, - VERTICAL_PREVIEW_MARGIN_PX, e_qrcode, ) from dymoprint.lib.dymo_labeler import DymoLabeler @@ -29,6 +28,8 @@ BarcodeWithTextRenderEngine, HorizontallyCombinedRenderEngine, PictureRenderEngine, + PrintPayloadRenderEngine, + PrintPreviewRenderEngine, QrRenderEngine, RenderContext, TestPatternRenderEngine, @@ -294,42 +295,36 @@ def run(): else None ) - render = HorizontallyCombinedRenderEngine( - render_engines, - min_payload_len_px=min_payload_len_px, - max_payload_len_px=max_payload_len_px, + render_engine = HorizontallyCombinedRenderEngine(render_engines) + render_kwargs = dict( + render_engine=render_engine, justify=args.justify, + visible_horizontal_margin_px=margin_px, + labeler_margin_px=DymoLabeler.get_labeler_margin_px(), + max_width_px=max_payload_len_px, + min_width_px=min_payload_len_px, ) - dymo_labeler = DymoLabeler( - margin_px=margin_px, - tape_size_mm=args.tape_size_mm, - ) + dymo_labeler = DymoLabeler(tape_size_mm=args.tape_size_mm) render_context = RenderContext(height_px=dymo_labeler.height_px) - bitmap = render.render(render_context) # print or show the label if args.preview or args.preview_inverted or args.imagemagick or args.browser: + render = PrintPreviewRenderEngine(**render_kwargs) + bitmap = render.render(render_context) LOG.debug("Demo mode: showing label..") - # fix size, adding print borders - expanded_bitmap = Image.new( - "1", - ( - bitmap.width + margin_px * 2, - bitmap.height + VERTICAL_PREVIEW_MARGIN_PX * 2, - ), - ) - expanded_bitmap.paste(bitmap, (margin_px, VERTICAL_PREVIEW_MARGIN_PX)) if args.preview or args.preview_inverted: - label_rotated = expanded_bitmap.transpose(Image.ROTATE_270) + label_rotated = bitmap.transpose(Image.ROTATE_270) print(image_to_unicode(label_rotated, invert=args.preview_inverted)) if args.imagemagick: - ImageOps.invert(expanded_bitmap).show() + ImageOps.invert(bitmap).show() if args.browser: with NamedTemporaryFile(suffix=".png", delete=False) as fp: - ImageOps.invert(expanded_bitmap).save(fp) + ImageOps.invert(bitmap).save(fp) webbrowser.open(f"file://{fp.name}") else: + render = PrintPayloadRenderEngine(**render_kwargs) + bitmap = render.render(render_context) dymo_labeler.print(bitmap) diff --git a/src/dymoprint/gui/gui.py b/src/dymoprint/gui/gui.py index 011b872..4e50392 100644 --- a/src/dymoprint/gui/gui.py +++ b/src/dymoprint/gui/gui.py @@ -20,7 +20,7 @@ ) from dymoprint.gui.common import crash_msg_box -from dymoprint.lib.constants import DEFAULT_MARGIN_PX, ICON_DIR +from dymoprint.lib.constants import ICON_DIR from dymoprint.lib.dymo_labeler import ( DymoLabeler, DymoLabelerDetectError, @@ -39,12 +39,12 @@ class DymoPrintWindow(QWidget): SUPPORTED_TAPE_SIZE_MM = (19, 12, 9, 6) DEFAULT_TAPE_SIZE_MM_INDEX = 1 - label_bitmap: Optional[Image.Image] + print_label_bitmap: Optional[Image.Image] dymo_labeler: DymoLabeler def __init__(self): super().__init__() - self.label_bitmap = None + self.print_label_bitmap = None self.detected_device = None self.window_layout = QVBoxLayout() @@ -54,11 +54,11 @@ def __init__(self): self.label_render = QLabel() self.error_label = QLabel() self.print_button = QPushButton() - self.margin_px = QSpinBox() + self.horizontal_margin_mm = QSpinBox() self.tape_size_mm = QComboBox() self.foreground_color = QComboBox() self.background_color = QComboBox() - self.min_label_len_mm = QSpinBox() + self.min_label_width_mm = QSpinBox() self.justify = QComboBox() self.info_label = QLabel() self.last_error = None @@ -84,14 +84,15 @@ def init_elements(self): shadow.setBlurRadius(15) self.label_render.setGraphicsEffect(shadow) - self.margin_px.setMinimum(20) - self.margin_px.setMaximum(1000) - self.margin_px.setValue(DEFAULT_MARGIN_PX) + h_margins_mm = round(DymoLabeler.LABELER_HORIZONTAL_MARGIN_MM) + self.horizontal_margin_mm.setMinimum(h_margins_mm) + self.horizontal_margin_mm.setMaximum(100) + self.horizontal_margin_mm.setValue(h_margins_mm) for tape_size_mm in self.SUPPORTED_TAPE_SIZE_MM: self.tape_size_mm.addItem(str(tape_size_mm), tape_size_mm) self.tape_size_mm.setCurrentIndex(self.DEFAULT_TAPE_SIZE_MM_INDEX) - self.min_label_len_mm.setMinimum(0) - self.min_label_len_mm.setMaximum(1000) + self.min_label_width_mm.setMinimum(h_margins_mm * 2) + self.min_label_width_mm.setMaximum(300) self.justify.addItems(["center", "left", "right"]) self.foreground_color.addItems( @@ -109,30 +110,31 @@ def init_timers(self): self.check_status() self.status_time = QTimer() self.status_time.timeout.connect(self.check_status) - self.status_time.setInterval(2000) - self.status_time.start(2000) + self.status_time.setInterval(20000) + self.status_time.start(20000) def init_connections(self): - self.margin_px.valueChanged.connect(self.label_list.render_label) - self.margin_px.valueChanged.connect(self.update_params) + self.horizontal_margin_mm.valueChanged.connect(self.label_list.render_label) + self.horizontal_margin_mm.valueChanged.connect(self.update_params) self.tape_size_mm.currentTextChanged.connect(self.update_params) - self.min_label_len_mm.valueChanged.connect(self.update_params) + self.min_label_width_mm.valueChanged.connect(self.update_params) self.justify.currentTextChanged.connect(self.update_params) self.foreground_color.currentTextChanged.connect(self.label_list.render_label) self.background_color.currentTextChanged.connect(self.label_list.render_label) - self.label_list.renderSignal.connect(self.update_label_render) + self.label_list.renderPrintPreviewSignal.connect(self.update_preview_render) + self.label_list.renderPrintPayloadSignal.connect(self.update_print_render) self.print_button.clicked.connect(self.print_label) def init_layout(self): settings_widget = QToolBar(self) - settings_widget.addWidget(QLabel("Margin:")) - settings_widget.addWidget(self.margin_px) + settings_widget.addWidget(QLabel("Margin [mm]:")) + settings_widget.addWidget(self.horizontal_margin_mm) settings_widget.addSeparator() - settings_widget.addWidget(QLabel("Tape Size:")) + settings_widget.addWidget(QLabel("Tape Size [mm]:")) settings_widget.addWidget(self.tape_size_mm) settings_widget.addSeparator() - settings_widget.addWidget(QLabel("Min Label Len [mm]:")) - settings_widget.addWidget(self.min_label_len_mm) + settings_widget.addWidget(QLabel("Min Label Length [mm]:")) + settings_widget.addWidget(self.min_label_width_mm) settings_widget.addSeparator() settings_widget.addWidget(QLabel("Justify:")) settings_widget.addWidget(self.justify) @@ -175,28 +177,24 @@ def init_layout(self): def update_params(self): justify: str = self.justify.currentText() - margin_px: int = self.margin_px.value() - min_label_mm_len: int = self.min_label_len_mm.value() - tape_size_mm: int = self.tape_size_mm.currentData() + horizontal_margin_mm: float = self.horizontal_margin_mm.value() + min_label_width_mm: float = self.min_label_width_mm.value() + tape_size_mm: float = self.tape_size_mm.currentData() - self.dymo_labeler.margin_px = margin_px self.dymo_labeler.tape_size_mm = tape_size_mm self.render_context.height_px = self.dymo_labeler.height_px - min_payload_len_px = max(0, (min_label_mm_len * 7) - margin_px * 2) - self.label_list.update_params(self.render_context, min_payload_len_px, justify) - - def update_label_render(self, label_bitmap): - self.label_bitmap = label_bitmap - label_image = Image.new( - "L", - ( - self.margin_px.value() + label_bitmap.width + self.margin_px.value(), - label_bitmap.height, - ), + + self.label_list.update_params( + h_margin_mm=horizontal_margin_mm, + min_label_width_mm=min_label_width_mm, + render_context=self.render_context, + justify=justify, ) - label_image.paste(label_bitmap, (self.margin_px.value(), 0)) - label_image_inv = ImageOps.invert(label_image).copy() - qim = ImageQt.ImageQt(label_image_inv) + + def update_preview_render(self, label_bitmap): + preview_label_image = label_bitmap.convert("L") + preview_label_image_inv = ImageOps.invert(preview_label_image).copy() + qim = ImageQt.ImageQt(preview_label_image_inv) q_image = QPixmap.fromImage(qim) mask = q_image.createMaskFromColor( @@ -210,15 +208,16 @@ def update_label_render(self, label_bitmap): self.label_render.setPixmap(q_image) self.label_render.adjustSize() - self.info_label.setText(f"← {px_to_mm(label_image.size[0])} mm →") + self.info_label.setText(f"← {px_to_mm(label_bitmap.size[0])} mm →") + + def update_print_render(self, print_label_bitmap): + self.print_label_bitmap = print_label_bitmap def print_label(self): try: - if self.label_bitmap is None: + if self.print_label_bitmap is None: raise RuntimeError("No label to print! Call update_label_render first.") - self.dymo_labeler.print( - self.label_bitmap, - ) + self.dymo_labeler.print(self.print_label_bitmap) except DymoLabelerPrintError as err: crash_msg_box(self, "Printing Failed!", err) diff --git a/src/dymoprint/gui/q_dymo_labels_list.py b/src/dymoprint/gui/q_dymo_labels_list.py index 51b9582..bb3c2e5 100644 --- a/src/dymoprint/gui/q_dymo_labels_list.py +++ b/src/dymoprint/gui/q_dymo_labels_list.py @@ -14,7 +14,15 @@ QrDymoLabelWidget, TextDymoLabelWidget, ) -from dymoprint.lib.render_engines import HorizontallyCombinedRenderEngine, RenderContext +from dymoprint.lib.dymo_labeler import DymoLabeler +from dymoprint.lib.render_engines import ( + HorizontallyCombinedRenderEngine, + MarginsMode, + PrintPayloadRenderEngine, + PrintPreviewRenderEngine, + RenderContext, +) +from dymoprint.lib.utils import mm_to_px LOG = logging.getLogger(__name__) @@ -30,7 +38,10 @@ class QDymoLabelList(QListWidget): Attributes: ---------- - renderSignal (QtCore.pyqtSignal): A signal emitted when the label is rendered. + renderPrintPreviewSignal (QtCore.pyqtSignal): A signal emitted when the preview + is rendered. + renderPrintPayloadSignal (QtCore.pyqtSignal): A signal emitted when the print + payload is rendered. render_context (RenderContext): The render context used for rendering the label. Methods: @@ -41,20 +52,33 @@ class QDymoLabelList(QListWidget): the label rendering. update_render_engine(self, render_engine): Updates the render context used for rendering the label. - render_label(self): Renders the label using the current render context and - emits the renderSignal. + render_preview(self): Renders the payload using the current render context and + emits the renderPrintPreviewSignal. + render_print(self): Renders the print payload using the current render context + and emits the renderPrintPayloadSignal. + render_label(self): Renders the both preview and print payloads using the + current render context and emits the corresponding signals. contextMenuEvent(self, event): Overrides the default context menu event to add or delete label widgets. """ - renderSignal = QtCore.pyqtSignal(Image.Image, name="renderSignal") + renderPrintPreviewSignal = QtCore.pyqtSignal( + Image.Image, name="renderPrintPreviewSignal" + ) + renderPrintSrenderPrintPayloadSignalignal = QtCore.pyqtSignal( + Image.Image, name="renderPrintPayloadSignal" + ) render_context: Optional[RenderContext] itemWidget: TextDymoLabelWidget + h_margin_mm: float + min_label_width_mm: Optional[float] + justify: str - def __init__(self, min_payload_len_px=0, justify="center", parent=None): + def __init__(self, parent=None): super().__init__(parent) - self.min_payload_len_px = min_payload_len_px - self.justify = justify + self.margin_px = None + self.min_label_width_mm = None + self.justify = "center" self.render_context = None self.setAlternatingRowColors(True) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) @@ -79,19 +103,22 @@ def dropEvent(self, e) -> None: def update_params( self, + h_margin_mm: float, + min_label_width_mm: float, render_context: RenderContext, - min_payload_len_px: int, - justify="center", + justify: str = "center", ): """Update the render context used for rendering the label. Args: ---- - justify: justification [center,left,right] - min_payload_len_px: minimum payload size in pixels + h_margin_mm: horizontal margin [mm] + min_label_width_mm: minimum label width [mm] render_context (RenderContext): The new render context to use. + justify: justification [center,left,right] """ - self.min_payload_len_px = min_payload_len_px + self.h_margin_mm = h_margin_mm + self.min_label_width_mm = min_label_width_mm self.justify = justify self.render_context = render_context for i in range(self.count()): @@ -100,31 +127,71 @@ def update_params( self.render_label() @property - def render_engines(self): - engines = [] + def _payload_render_engine(self): + render_engines = [] for i in range(self.count()): item = self.item(i) item_widget = self.itemWidget(self.item(i)) if item_widget and item: item.setSizeHint(item_widget.sizeHint()) - engines.append(item_widget.render_engine) - return engines + render_engines.append(item_widget.render_engine) + return HorizontallyCombinedRenderEngine(render_engines=render_engines) - def render_label(self): + def _render_with_margins(self, mode: MarginsMode, signal: QtCore.pyqtSignal): """Render the label using the current render context and emit renderSignal.""" - render_engine = HorizontallyCombinedRenderEngine( - render_engines=self.render_engines, - min_payload_len_px=self.min_payload_len_px, - max_payload_len_px=None, + render_engine = PrintPreviewRenderEngine( + render_engine=self._payload_render_engine, justify=self.justify, + visible_horizontal_margin_px=mm_to_px(self.h_margin_mm), + labeler_margin_px=DymoLabeler.get_labeler_margin_px(), + max_width_px=None, + min_width_px=mm_to_px(self.min_label_width_mm), ) try: - label_bitmap = render_engine.render(self.render_context) + bitmap = render_engine.render(self.render_context) except BaseException as err: # noqa: BLE001 crash_msg_box(self, "Render Engine Failed!", err) - label_bitmap = EmptyRenderEngine().render(self.render_context) + bitmap = EmptyRenderEngine().render(self.render_context) + + signal.emit(bitmap) - self.renderSignal.emit(label_bitmap) + def render_preview(self): + render_engine = PrintPreviewRenderEngine( + render_engine=self._payload_render_engine, + justify=self.justify, + visible_horizontal_margin_px=mm_to_px(self.h_margin_mm), + labeler_margin_px=DymoLabeler.get_labeler_margin_px(), + max_width_px=None, + min_width_px=mm_to_px(self.min_label_width_mm), + ) + try: + bitmap = render_engine.render(self.render_context) + except BaseException as err: # noqa: BLE001 + crash_msg_box(self, "Render Engine Failed!", err) + bitmap = EmptyRenderEngine().render(self.render_context) + + self.renderPrintPreviewSignal.emit(bitmap) + + def render_print(self): + render_engine = PrintPayloadRenderEngine( + render_engine=self._payload_render_engine, + justify=self.justify, + visible_horizontal_margin_px=mm_to_px(self.h_margin_mm), + labeler_margin_px=DymoLabeler.get_labeler_margin_px(), + max_width_px=None, + min_width_px=mm_to_px(self.min_label_width_mm), + ) + try: + bitmap = render_engine.render(self.render_context) + except BaseException as err: # noqa: BLE001 + crash_msg_box(self, "Render Engine Failed!", err) + bitmap = EmptyRenderEngine().render(self.render_context) + + self.renderPrintPayloadSignal.emit(bitmap) + + def render_label(self): + self.render_preview() + self.render_print() def contextMenuEvent(self, event): """Override the default context menu event to add or delete label widgets. diff --git a/src/dymoprint/lib/dymo_labeler.py b/src/dymoprint/lib/dymo_labeler.py index bca5718..ffff8c1 100755 --- a/src/dymoprint/lib/dymo_labeler.py +++ b/src/dymoprint/lib/dymo_labeler.py @@ -15,8 +15,9 @@ from PIL import Image from usb.core import NoBackendError, USBError -from dymoprint.lib.constants import DEFAULT_MARGIN_PX, ESC, SYN +from dymoprint.lib.constants import ESC, SYN from dymoprint.lib.detect import DetectedDevice, DymoUSBError, detect_device +from dymoprint.lib.utils import mm_to_px LOG = logging.getLogger(__name__) DEFAULT_TAPE_SIZE_MM = 12 @@ -219,24 +220,22 @@ def _get_status(self): self._status_request() return self._send_command() - def print_label(self, lines: list[list[int]], margin_px=DEFAULT_MARGIN_PX): + def print_label(self, lines: list[list[int]]): """Print the label described by lines. Automatically split the label if it's larger than maxLines. """ while len(lines) > self._maxLines + 1: - self._raw_print_label(lines[0 : self._maxLines], margin_px=0) + self._raw_print_label(lines[0 : self._maxLines]) del lines[0 : self._maxLines] - self._raw_print_label(lines, margin_px=margin_px) + self._raw_print_label(lines) - def _raw_print_label(self, lines: list[list[int]], margin_px=DEFAULT_MARGIN_PX): + def _raw_print_label(self, lines: list[list[int]]): """Print the label described by lines (HLF).""" # Here used to be a matrix optimization code that caused problems in issue #87 self._tape_color(0) for line in lines: self._line(line) - if margin_px > 0: - self._skip_lines(margin_px * 2) self._status_request() status = self._get_status() LOG.debug(f"Post-send response: {status}") @@ -244,15 +243,15 @@ def _raw_print_label(self, lines: list[list[int]], margin_px=DEFAULT_MARGIN_PX): class DymoLabeler: device: DetectedDevice - margin_px: int tape_size_mm: int + LABELER_HORIZONTAL_MARGIN_MM = 8.1 + LABELER_VERTICAL_MARGIN_MM = 1.9 + def __init__( self, - margin_px: int = DEFAULT_MARGIN_PX, tape_size_mm: int = DEFAULT_TAPE_SIZE_MM, ): - self.margin_px = margin_px self.tape_size_mm = tape_size_mm self.device = None @@ -272,6 +271,13 @@ def _functions(self): tape_size_mm=self.tape_size_mm, ) + @classmethod + def get_labeler_margin_px(cls) -> tuple[float, float]: + return ( + mm_to_px(cls.LABELER_HORIZONTAL_MARGIN_MM), + mm_to_px(cls.LABELER_VERTICAL_MARGIN_MM), + ) + def detect(self): try: self.device = detect_device() @@ -315,7 +321,7 @@ def print( try: LOG.debug("Printing label..") - self._functions.print_label(label_matrix, margin_px=self.margin_px) + self._functions.print_label(label_matrix) LOG.debug("Done printing.") usb.util.dispose_resources(self.device.dev) LOG.debug("Cleaned up.") diff --git a/src/dymoprint/lib/render_engines/__init__.py b/src/dymoprint/lib/render_engines/__init__.py index 8056aed..c7daac9 100644 --- a/src/dymoprint/lib/render_engines/__init__.py +++ b/src/dymoprint/lib/render_engines/__init__.py @@ -4,7 +4,10 @@ from dymoprint.lib.render_engines.horizontally_combined import ( HorizontallyCombinedRenderEngine, ) +from dymoprint.lib.render_engines.margins import MarginsMode, MarginsRenderEngine from dymoprint.lib.render_engines.picture import NoPictureFilePath, PictureRenderEngine +from dymoprint.lib.render_engines.print_payload import PrintPayloadRenderEngine +from dymoprint.lib.render_engines.print_preview import PrintPreviewRenderEngine from dymoprint.lib.render_engines.qr import NoContentError, QrRenderEngine from dymoprint.lib.render_engines.render_context import RenderContext from dymoprint.lib.render_engines.render_engine import RenderEngine @@ -16,9 +19,13 @@ BarcodeWithTextRenderEngine, EmptyRenderEngine, HorizontallyCombinedRenderEngine, + MarginsMode, + MarginsRenderEngine, NoContentError, NoPictureFilePath, PictureRenderEngine, + PrintPayloadRenderEngine, + PrintPreviewRenderEngine, QrRenderEngine, RenderContext, RenderEngine, diff --git a/src/dymoprint/lib/render_engines/horizontally_combined.py b/src/dymoprint/lib/render_engines/horizontally_combined.py index c6919e8..931b98b 100644 --- a/src/dymoprint/lib/render_engines/horizontally_combined.py +++ b/src/dymoprint/lib/render_engines/horizontally_combined.py @@ -7,27 +7,15 @@ from dymoprint.lib.render_engines.render_engine import RenderEngine -class BitmapTooBigError(ValueError): - def __init__(self, width_px, max_width_px): - msg = f"width_px: {width_px}, max_width_px: {max_width_px}" - super().__init__(msg) - - class HorizontallyCombinedRenderEngine(RenderEngine): PADDING = 4 def __init__( self, render_engines: list[RenderEngine], - min_payload_len_px: int = 0, - max_payload_len_px: int | None = None, - justify: str = "center", ): super().__init__() self.render_engines = render_engines - self.min_payload_len_px = min_payload_len_px - self.max_payload_len_px = max_payload_len_px - self.justify = justify def render(self, context: RenderContext) -> Image.Image: render_engines = self.render_engines or [EmptyRenderEngine()] @@ -50,28 +38,4 @@ def render(self, context: RenderContext) -> Image.Image: merged_bitmap.paste(bitmap, box=(x_offset, y_offset)) x_offset += bitmap.width + self.PADDING - if ( - self.max_payload_len_px is not None - and merged_bitmap.width > self.max_payload_len_px - ): - raise BitmapTooBigError(merged_bitmap.width, self.max_payload_len_px) - - if self.min_payload_len_px > merged_bitmap.width: - offset = 0 - if self.justify == "center": - offset = max( - 0, int((self.min_payload_len_px - merged_bitmap.width) / 2) - ) - if self.justify == "right": - offset = max(0, int(self.min_payload_len_px - merged_bitmap.width)) - expanded_merged_bitmap = Image.new( - "1", - ( - self.min_payload_len_px, - merged_bitmap.height, - ), - ) - expanded_merged_bitmap.paste(merged_bitmap, box=(offset, 0)) - return expanded_merged_bitmap - return merged_bitmap diff --git a/src/dymoprint/lib/render_engines/margins.py b/src/dymoprint/lib/render_engines/margins.py new file mode 100644 index 0000000..8c8c542 --- /dev/null +++ b/src/dymoprint/lib/render_engines/margins.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from enum import Enum + +from PIL import Image + +from dymoprint.lib.render_engines.render_context import RenderContext +from dymoprint.lib.render_engines.render_engine import RenderEngine + + +class BitmapTooBigError(ValueError): + def __init__(self, width_px, max_width_px): + msg = f"width_px: {width_px}, max_width_px: {max_width_px}" + super().__init__(msg) + + +class MarginsMode(Enum): + PRINT = 1 + PREVIEW = 2 + + +class MarginsRenderEngine(RenderEngine): + def __init__( + self, + render_engine: RenderEngine, + mode: MarginsMode, + justify: str = "center", + visible_horizontal_margin_px: float = 0, + labeler_margin_px: tuple[float, float] = (0, 0), + max_width_px: float | None = None, + min_width_px: float = 0, + ): + super().__init__() + labeler_horizontal_margin_px, labeler_vertical_margin_px = labeler_margin_px + assert visible_horizontal_margin_px >= 0 + assert labeler_horizontal_margin_px >= 0 + assert labeler_vertical_margin_px >= 0 + assert not max_width_px or max_width_px >= 0 + assert min_width_px >= 0 + self.mode = mode + self.justify = justify + self.visible_horizontal_margin_px = visible_horizontal_margin_px + self.labeler_horizontal_margin_px = labeler_horizontal_margin_px + self.labeler_vertical_margin_px = labeler_vertical_margin_px + self.max_width_px = max_width_px + self.min_width_px = min_width_px + self.render_engine = render_engine + + def calculate_visible_width(self, payload_width_px: int) -> float: + minimal_label_width_px = ( + payload_width_px + self.visible_horizontal_margin_px * 2 + ) + if self.max_width_px is not None and minimal_label_width_px > self.max_width_px: + raise BitmapTooBigError(minimal_label_width_px, self.max_width_px) + + if self.min_width_px > minimal_label_width_px: + label_width_px = self.min_width_px + else: + label_width_px = minimal_label_width_px + return label_width_px + + def render(self, context: RenderContext) -> Image.Image: + payload_bitmap = self.render_engine.render(context) + payload_width_px = payload_bitmap.width + label_width_px = self.calculate_visible_width(payload_width_px) + padding_px = label_width_px - payload_width_px # sum of margins from both sides + + if self.justify == "left": + horizontal_offset_px = self.visible_horizontal_margin_px + elif self.justify == "center": + horizontal_offset_px = padding_px / 2 + elif self.justify == "right": + horizontal_offset_px = padding_px - self.visible_horizontal_margin_px + assert horizontal_offset_px >= self.visible_horizontal_margin_px + + # In print mode: + # ============== + # There is a gap between the printer head and the cutter (for the sake of this + # example, let us say it is DX pixels wide). + # We assume the printing starts when the print head is in offset DX from the + # label's edge (just under the cutter). + # After we print the payload, we need to offset the label DX pixels, in order + # to move the edge of the printed payload past the cutter, othewise the cutter + # will cut inside the printed payload. + # Afterwards, we need to offset another DX pixels, so that the cut will have + # some margin from the payload edge. The reason we move DX pixels this time, is + # in order to have simmetry with the initial margin between label edge and start + # of printed payload. + # + # There's also some vertical margin between printed area and the label edge + + vertial_offset_px: float = 0 + if self.mode == MarginsMode.PRINT: + # print head is already in offset from label's edge under the cutter + horizontal_offset_px -= self.labeler_horizontal_margin_px + # no need to add vertical margins to bitmap + bitmap_height = payload_bitmap.height + elif self.mode == MarginsMode.PREVIEW: + # add vertical margins to bitmap + bitmap_height = payload_bitmap.height + self.labeler_vertical_margin_px * 2 + vertial_offset_px = self.labeler_vertical_margin_px + + bitmap = Image.new("1", (label_width_px, bitmap_height)) + bitmap.paste( + payload_bitmap, box=(round(horizontal_offset_px), round(vertial_offset_px)) + ) + return bitmap diff --git a/src/dymoprint/lib/render_engines/print_payload.py b/src/dymoprint/lib/render_engines/print_payload.py new file mode 100644 index 0000000..131b6cc --- /dev/null +++ b/src/dymoprint/lib/render_engines/print_payload.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from PIL import Image + +from dymoprint.lib.render_engines.margins import MarginsMode, MarginsRenderEngine +from dymoprint.lib.render_engines.render_context import RenderContext +from dymoprint.lib.render_engines.render_engine import RenderEngine + + +class PrintPayloadRenderEngine(RenderEngine): + def __init__( + self, + render_engine: RenderEngine, + justify: str = "center", + visible_horizontal_margin_px: float = 0, + labeler_margin_px: tuple[float, float] = (0, 0), + max_width_px: float | None = None, + min_width_px: float = 0, + ): + super().__init__() + self.render_engine = MarginsRenderEngine( + render_engine=render_engine, + mode=MarginsMode.PRINT, + justify=justify, + visible_horizontal_margin_px=visible_horizontal_margin_px, + labeler_margin_px=labeler_margin_px, + max_width_px=max_width_px, + min_width_px=min_width_px, + ) + + def render(self, context: RenderContext) -> Image.Image: + return self.render_engine.render(context) diff --git a/src/dymoprint/lib/render_engines/print_preview.py b/src/dymoprint/lib/render_engines/print_preview.py new file mode 100644 index 0000000..55d911e --- /dev/null +++ b/src/dymoprint/lib/render_engines/print_preview.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from PIL import Image + +from dymoprint.lib.render_engines.margins import MarginsMode, MarginsRenderEngine +from dymoprint.lib.render_engines.render_context import RenderContext +from dymoprint.lib.render_engines.render_engine import RenderEngine + + +class PrintPreviewRenderEngine(RenderEngine): + def __init__( + self, + render_engine: RenderEngine, + justify: str = "center", + visible_horizontal_margin_px: float = 0, + labeler_margin_px: tuple[float, float] = (0, 0), + max_width_px: float | None = None, + min_width_px: float = 0, + ): + super().__init__() + self.render_engine = MarginsRenderEngine( + render_engine=render_engine, + mode=MarginsMode.PREVIEW, + justify=justify, + visible_horizontal_margin_px=visible_horizontal_margin_px, + labeler_margin_px=labeler_margin_px, + max_width_px=max_width_px, + min_width_px=min_width_px, + ) + + def render(self, context: RenderContext) -> Image.Image: + return self.render_engine.render(context) diff --git a/src/dymoprint/lib/utils.py b/src/dymoprint/lib/utils.py index 984cac9..5eaca94 100755 --- a/src/dymoprint/lib/utils.py +++ b/src/dymoprint/lib/utils.py @@ -33,12 +33,16 @@ def draw_image(bitmap): del drawobj -def px_to_mm(px): +def px_to_mm(px) -> float: mm = px / PIXELS_PER_MM # Round up to nearest 0.1mm return math.ceil(mm * 10) / 10 +def mm_to_px(mm) -> float: + return math.ceil(mm * PIXELS_PER_MM) + + @contextlib.contextmanager def system_run(): try: