diff --git a/doc/examples/complex_label_with_qrcode.py b/doc/examples/complex_label_with_qrcode.py index 92cd99d..32741b2 100644 --- a/doc/examples/complex_label_with_qrcode.py +++ b/doc/examples/complex_label_with_qrcode.py @@ -12,10 +12,10 @@ # First let's import all the needed Classes from labelprinterkit.backends.network import TCPBackend -from labelprinterkit.printers import P750W -from labelprinterkit.label import Label, Box, Text, QRCode, Padding -from labelprinterkit.job import Job from labelprinterkit.constants import Media +from labelprinterkit.job import Job +from labelprinterkit.label import Label, Box, Text, QRCode, Padding +from labelprinterkit.printers.P750W import P750W # The label will be created for a 12mm band. The 12mm has 70 pixel/points width. # So let's create a QR code with 70 pixels width. @@ -42,7 +42,7 @@ label.image.save('/tmp/label.png') # Create job with configuration and add label as page -job = Job(Media.W12) +job = Job(Media.TZE_12) job.add_page(label) # Use TCP backend to connect to printer diff --git a/labelprinterkit/__init__.py b/labelprinterkit/__init__.py index 4ee21e9..fbcc9a5 100644 --- a/labelprinterkit/__init__.py +++ b/labelprinterkit/__init__.py @@ -1,5 +1,10 @@ from __future__ import annotations -from pkg_resources import get_distribution + +from pkg_resources import get_distribution,DistributionNotFound __all__ = ["printers", "backends", "label", "job", "page"] -__version__ = get_distribution(__name__).version +try: + __version__ = get_distribution(__name__).version +except DistributionNotFound: + # package is not installed + pass diff --git a/labelprinterkit/backends/__init__.py b/labelprinterkit/backends/__init__.py index fc37b5a..bf3af7e 100644 --- a/labelprinterkit/backends/__init__.py +++ b/labelprinterkit/backends/__init__.py @@ -1,14 +1,21 @@ from __future__ import annotations + from abc import ABC, abstractmethod +from typing import NewType, Type + +from labelprinterkit.commands.BaseCommand import BaseCommand class BaseBackend(ABC): ... +Command = NewType('Command', BaseCommand) + + class UniDirectionalBackend(BaseBackend): @abstractmethod - def write(self, data: bytes): + def write(self, data: bytes | Type[Command]): ... diff --git a/labelprinterkit/backends/bluetooth.py b/labelprinterkit/backends/bluetooth.py index 83a3a0b..3fe90d8 100644 --- a/labelprinterkit/backends/bluetooth.py +++ b/labelprinterkit/backends/bluetooth.py @@ -1,10 +1,12 @@ from __future__ import annotations +from typing import Type + try: import serial except ImportError: serial = None -from . import BiDirectionalBackend +from . import BiDirectionalBackend, Command class BTSerialBackend(BiDirectionalBackend): @@ -24,7 +26,9 @@ def __init__(self, dev_path: str): raise OSError('Device not found') self._dev = dev - def write(self, data: bytes): + def write(self, data: bytes | Type[Command]): + if isinstance(data, Command): + data = data.to_bytes() self._dev.write(data) def read(self, count: int) -> bytes: diff --git a/labelprinterkit/backends/network.py b/labelprinterkit/backends/network.py index 67d75dc..ab49967 100644 --- a/labelprinterkit/backends/network.py +++ b/labelprinterkit/backends/network.py @@ -1,8 +1,12 @@ from __future__ import annotations + import socket +from typing import Type + +from ..commands.BaseCommand import BaseCommand try: - from pysnmp.hlapi import SnmpEngine, getCmd, UdpTransportTarget, Udp6TransportTarget,\ + from pysnmp.hlapi import SnmpEngine, getCmd, UdpTransportTarget, Udp6TransportTarget, \ ContextData, ObjectType, ObjectIdentity, CommunityData except ImportError: SnmpEngine = None @@ -14,7 +18,7 @@ ObjectIdentity = None CommunityData = None -from . import UniDirectionalBackend +from . import UniDirectionalBackend, Command class TCPBackend(UniDirectionalBackend): @@ -34,7 +38,9 @@ def __init__(self, host, port=9100, timeout=10): raise ConnectionError(f"Connection to {host} failed.") self._sock = sock - def write(self, data: bytes) -> None: + def write(self, data: bytes | Type[Command]): + if issubclass(type(data), BaseCommand): + data = data.to_bytes() sent = self._sock.send(data) if sent == 0: raise IOError("Socket connection broken") @@ -62,7 +68,7 @@ def get_status(self): transport, ContextData(), ObjectType(ObjectIdentity('.1.3.6.1.4.1.2435.3.3.9.1.6.1.0')) - ) + ) error_indication, _, _, variables = next(iterator) if error_indication: diff --git a/labelprinterkit/backends/usb.py b/labelprinterkit/backends/usb.py index 3eb2cd7..638d7da 100644 --- a/labelprinterkit/backends/usb.py +++ b/labelprinterkit/backends/usb.py @@ -1,10 +1,12 @@ from __future__ import annotations + from time import sleep +from typing import Type import usb.core import usb.util -from . import BaseBackend +from . import BaseBackend, Command class PyUSBBackend(BaseBackend): @@ -23,7 +25,9 @@ def is_usb_printer(dev) -> bool: return True return False - def write(self, data: bytes): + def write(self, data: bytes | Type[Command]): + if isinstance(data, Command): + data = data.to_bytes() self._dev.write(0x2, data) def read(self, count: int) -> bytes | None: diff --git a/labelprinterkit/commands/AdvancedModeSettings.py b/labelprinterkit/commands/AdvancedModeSettings.py new file mode 100644 index 0000000..0f58044 --- /dev/null +++ b/labelprinterkit/commands/AdvancedModeSettings.py @@ -0,0 +1,22 @@ +import functools +from enum import Enum +from typing import List + +from .BaseCommand import BaseCommand + + +class AdvancedModeSettings(BaseCommand): + class Settings(Enum): + DRAFT_PRINTING = 0b00000001 + HALF_CUT = 0b00000100 + NO_CHAIN_PRINTING = 0b00001000 + SPECIAL_TAPE = 0b00010000 + HIGH_RESOLUTION = 0b01000000 + NO_BUFFER_CLEARING = 0b10000000 + + def __init__(self, settings: List[Settings] | None = None): + super().__init__() + self.settings = settings or [] + + def to_bytes(self) -> bytes: + return b'\x1BiM' + functools.reduce(lambda a, b: a | b.value, self.settings, 0x00).to_bytes() diff --git a/labelprinterkit/commands/BaseCommand.py b/labelprinterkit/commands/BaseCommand.py new file mode 100644 index 0000000..02df734 --- /dev/null +++ b/labelprinterkit/commands/BaseCommand.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class BaseCommand(ABC): + def __mul__(self, other) -> bytes: + if not isinstance(other, int): + raise TypeError("Can only multiply by an int") + return self.to_bytes() * other + + @abstractmethod + def to_bytes(self) -> bytes: + ... diff --git a/labelprinterkit/commands/Initialize.py b/labelprinterkit/commands/Initialize.py new file mode 100644 index 0000000..a9fd426 --- /dev/null +++ b/labelprinterkit/commands/Initialize.py @@ -0,0 +1,6 @@ +from .BaseCommand import BaseCommand + + +class Initialize(BaseCommand): + def to_bytes(self) -> bytes: + return b'\x1b@' diff --git a/labelprinterkit/commands/Invalidate.py b/labelprinterkit/commands/Invalidate.py new file mode 100644 index 0000000..6a60d8e --- /dev/null +++ b/labelprinterkit/commands/Invalidate.py @@ -0,0 +1,6 @@ +from .BaseCommand import BaseCommand + + +class Invalidate(BaseCommand): + def to_bytes(self) -> bytes: + return b'\x00' diff --git a/labelprinterkit/commands/Print.py b/labelprinterkit/commands/Print.py new file mode 100644 index 0000000..cb8ca4d --- /dev/null +++ b/labelprinterkit/commands/Print.py @@ -0,0 +1,13 @@ +from .BaseCommand import BaseCommand + + +class Print(BaseCommand): + def __init__(self, feed: bool = False): + super().__init__() + self.feed = feed + + def to_bytes(self) -> bytes: + if self.feed: + return b'\x1A' + else: + return b'\x0C' diff --git a/labelprinterkit/commands/PrintInformation.py b/labelprinterkit/commands/PrintInformation.py new file mode 100644 index 0000000..8eba050 --- /dev/null +++ b/labelprinterkit/commands/PrintInformation.py @@ -0,0 +1,57 @@ +from enum import Enum + +from .BaseCommand import BaseCommand +from ..constants import MediaType + + +class PrintInformation(BaseCommand): + class Page(Enum): + STARTING_PAGE = 0 + OTHER_PAGE = 1 + LAST_PAGE = 2 + + def __init__(self, + media_type: MediaType | None, + high_resolution: bool | None, + media_width: int | None, + raster_number: int | None = 0, + page: Page = Page.STARTING_PAGE, + recovery: bool = False, + ): + super().__init__() + self.media_type = media_type + self.high_resolution = high_resolution + self.media_width = media_width + self.raster_number = raster_number + self.page = page + self.recovery = recovery + + def to_bytes(self) -> bytes: + valid = 0x00 + if self.media_type is not None: + valid = valid | 0x02 + if self.media_width is not None: + valid = valid | 0x04 + if self.recovery: + valid = valid | 0x80 + + media_type = 0x00 + if media_type == MediaType.LAMINATED_TAPE and self.high_resolution: + media_type = 0x09 + elif media_type == MediaType.HEATSHRINK_TUBE_21: + media_type = 0x11 + elif media_type == MediaType.HEATSHRINK_TUBE_31: + media_type = 0x17 + elif media_type == MediaType.FLE_TAPE: + media_type = 0x13 + + return b'\x1Biz' + \ + valid.to_bytes() + \ + media_type.to_bytes() + \ + self.media_width.to_bytes() if self.media_width is not None else b'\x00' + \ + b'\x00' + \ + ( + self.raster_number if self.raster_number is not None else 0).to_bytes( + 3, 'big') + \ + self.page.value.to_bytes() + \ + b'\x00' diff --git a/labelprinterkit/commands/RasterGraphicsTransfer.py b/labelprinterkit/commands/RasterGraphicsTransfer.py new file mode 100644 index 0000000..5c088f6 --- /dev/null +++ b/labelprinterkit/commands/RasterGraphicsTransfer.py @@ -0,0 +1,10 @@ +from .BaseCommand import BaseCommand + + +class RasterGraphicsTransfer(BaseCommand): + def __init__(self, data: bytes): + super().__init__() + self.data = data + + def to_bytes(self) -> bytes: + return b'G' + len(self.data).to_bytes(2, 'big') + self.data diff --git a/labelprinterkit/commands/SelectCompressionMode.py b/labelprinterkit/commands/SelectCompressionMode.py new file mode 100644 index 0000000..7187cfd --- /dev/null +++ b/labelprinterkit/commands/SelectCompressionMode.py @@ -0,0 +1,16 @@ +from enum import Enum + +from .BaseCommand import BaseCommand + + +class SelectCompressionMode(BaseCommand): + class Compression(Enum): + NO_COMPRESSION = 0x00 + TIFF = 0x02 + + def __init__(self, compression: Compression): + super().__init__() + self.compression = compression + + def to_bytes(self) -> bytes: + return b'M' + self.compression.value.to_bytes() diff --git a/labelprinterkit/commands/SpecifyCutEachPages.py b/labelprinterkit/commands/SpecifyCutEachPages.py new file mode 100644 index 0000000..6eb8456 --- /dev/null +++ b/labelprinterkit/commands/SpecifyCutEachPages.py @@ -0,0 +1,10 @@ +from .BaseCommand import BaseCommand + + +class SpecifyCutEachPages(BaseCommand): + def __init__(self, pages: int): + super().__init__() + self.pages = pages + + def to_bytes(self) -> bytes: + return b'\x1Bid' + self.pages.to_bytes() diff --git a/labelprinterkit/commands/SpecifyMarginAmount.py b/labelprinterkit/commands/SpecifyMarginAmount.py new file mode 100644 index 0000000..62b8786 --- /dev/null +++ b/labelprinterkit/commands/SpecifyMarginAmount.py @@ -0,0 +1,10 @@ +from .BaseCommand import BaseCommand + + +class SpecifyMarginAmount(BaseCommand): + def __init__(self, margin: int): + super().__init__() + self.margin = margin + + def to_bytes(self) -> bytes: + return b'\x1Bid' + self.margin.to_bytes(2, 'little') diff --git a/labelprinterkit/commands/StatusInformationRequest.py b/labelprinterkit/commands/StatusInformationRequest.py new file mode 100644 index 0000000..e7662b0 --- /dev/null +++ b/labelprinterkit/commands/StatusInformationRequest.py @@ -0,0 +1,6 @@ +from .BaseCommand import BaseCommand + + +class StatusInformationRequest(BaseCommand): + def to_bytes(self) -> bytes: + return b'\x1BiS' diff --git a/labelprinterkit/commands/SwitchDynamicCommandMode.py b/labelprinterkit/commands/SwitchDynamicCommandMode.py new file mode 100644 index 0000000..c6097f9 --- /dev/null +++ b/labelprinterkit/commands/SwitchDynamicCommandMode.py @@ -0,0 +1,17 @@ +from enum import Enum + +from .BaseCommand import BaseCommand + + +class SwitchDynamicCommandMode(BaseCommand): + class Modes(Enum): + ESC_P = 0x00 + RASTER = 0x01 + P_TOUCH_TEMPLATE = 0x02 + + def __init__(self, mode: Modes = Modes.RASTER): + super().__init__() + self.mode = mode + + def to_bytes(self) -> bytes: + return b'\x1Bia' + self.mode.value.to_bytes() diff --git a/labelprinterkit/commands/VariousModeSettings.py b/labelprinterkit/commands/VariousModeSettings.py new file mode 100644 index 0000000..27df564 --- /dev/null +++ b/labelprinterkit/commands/VariousModeSettings.py @@ -0,0 +1,18 @@ +import functools +from enum import Enum +from typing import List + +from .BaseCommand import BaseCommand + + +class VariousModeSettings(BaseCommand): + class Settings(Enum): + AUTO_CUT = 0x40 + MIRROR_PRINTING = 0x80 + + def __init__(self, settings: List[Settings] | None = None): + super().__init__() + self.settings = settings or [] + + def to_bytes(self) -> bytes: + return b'\x1BiM' + functools.reduce(lambda a, b: a | b, self.settings, 0x00).to_bytes() diff --git a/labelprinterkit/commands/ZeroRasterGraphics.py b/labelprinterkit/commands/ZeroRasterGraphics.py new file mode 100644 index 0000000..d91b38e --- /dev/null +++ b/labelprinterkit/commands/ZeroRasterGraphics.py @@ -0,0 +1,6 @@ +from .BaseCommand import BaseCommand + + +class ZeroRasterGraphics(BaseCommand): + def to_bytes(self) -> bytes: + return b'Z' diff --git a/labelprinterkit/commands/__init__.py b/labelprinterkit/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labelprinterkit/constants.py b/labelprinterkit/constants.py index 3b6a58b..97ca6d4 100644 --- a/labelprinterkit/constants.py +++ b/labelprinterkit/constants.py @@ -1,6 +1,7 @@ from __future__ import annotations + from enum import Enum -from typing import NamedTuple +from typing import NamedTuple, Tuple class Resolution(Enum): @@ -8,19 +9,6 @@ class Resolution(Enum): HIGH = (180, 320) -class VariousModesSettings(Enum): - AUTO_CUT = 0b01000000 - MIRROR_PRINTING = 0b10000000 - - -class AdvancedModeSettings(Enum): - HALF_CUT = 0b00000100 - CHAIN_PRINTING = 0b00001000 - SPECIAL_TAPE = 0b00010000 - HIGH_RESOLUTION = 0b01000000 - BUFFER_CLEARING = 0b10000000 - - class ErrorCodes(Enum): NO_MEDIA = 0x0001 CUTTER_JAM = 0x0004 @@ -31,6 +19,11 @@ class ErrorCodes(Enum): OVERHEATING = 0x2000 +class PrintHeadHeight(Enum): + PHH128 = 128 + PHH560 = 560 + + class MediaType(Enum): NO_MEDIA = 0x00 LAMINATED_TAPE = 0x01 @@ -39,28 +32,40 @@ class MediaType(Enum): HEATSHRINK_TUBE_31 = 0x17 INCOMPATIBLE_TAPE = 0xFF + # PT-P900/P900W/P950NW/P910BT + FABRIC_TAPE = 0x04 + FLE_TAPE = 0x13 + FLEXIBLE_ID_TAPE = 0x14 + SATIN_TAPE = 0x15 + class TapeInfo(NamedTuple): - width: int - length: int - lmargin: int | None - printarea: int | None - rmargin: int | None media_type: MediaType | None + width: float + length: float | None # None for continuous tape + media_width: int + media_length: int = 0x00 class Media(Enum): - UNSUPPORTED_MEDIA = TapeInfo(0, 0, None, None, None, None) - NO_MEDIA = TapeInfo(0, 0, None, None, None, None) - W3_5 = TapeInfo(4, 0, 52, 24, 52, MediaType.LAMINATED_TAPE) - W6 = TapeInfo(6, 0, 48, 32, 48, MediaType.LAMINATED_TAPE) - W9 = TapeInfo(9, 0, 39, 50, 39, MediaType.LAMINATED_TAPE) - W12 = TapeInfo(12, 0, 29, 70, 29, MediaType.LAMINATED_TAPE) - W18 = TapeInfo(18, 0, 8, 112, 8, MediaType.LAMINATED_TAPE) - W24 = TapeInfo(24, 0, 0, 128, 0, MediaType.LAMINATED_TAPE) + UNSUPPORTED_MEDIA = TapeInfo(None, 0, None, 0x00) + NO_MEDIA = TapeInfo(None, 0, None, 0x00) + TZE_3_5 = TapeInfo(MediaType.LAMINATED_TAPE, 3.5, None, 0x04) + TZE_6 = TapeInfo(MediaType.LAMINATED_TAPE, 6, None, 0x06) + TZE_9 = TapeInfo(MediaType.LAMINATED_TAPE, 9, None, 0x09) + TZE_12 = TapeInfo(MediaType.LAMINATED_TAPE, 12, None, 0x0C) + TZE_18 = TapeInfo(MediaType.LAMINATED_TAPE, 18, None, 0x12) + TZE_24 = TapeInfo(MediaType.LAMINATED_TAPE, 24, None, 0x18) + TZE_36 = TapeInfo(MediaType.LAMINATED_TAPE, 36, None, 0x24) + HS_5_8 = TapeInfo(MediaType.HEATSHRINK_TUBE_21, 5.8, None, 0x06) + HS_8_8 = TapeInfo(MediaType.HEATSHRINK_TUBE_21, 8.8, None, 0x09) + HS_11_7 = TapeInfo(MediaType.HEATSHRINK_TUBE_21, 11.7, None, 0x0C) + HS_17_7 = TapeInfo(MediaType.HEATSHRINK_TUBE_21, 17.7, None, 0x12) + HS_23_6 = TapeInfo(MediaType.HEATSHRINK_TUBE_21, 23.6, None, 0x18) + FLE_21_45 = TapeInfo(MediaType.FLE_TAPE, 21, None, 0x15, 0x2D) @classmethod - def get_media(cls, width: int, media_type: MediaType): + def get_media(cls, width: float, media_type: MediaType): medias = [media_size for media_size in cls if media_size.value.width == width and media_size.value.media_type == media_type] if not medias: @@ -68,6 +73,51 @@ def get_media(cls, width: int, media_type: MediaType): return medias[0] +class PrintHeadMediaAlignment(NamedTuple): + left_margin: int + print_area: int + right_margin: int + + +PRINTHEAD_MEDIA_ALIGNMENT: dict[Tuple[PrintHeadHeight, Media], PrintHeadMediaAlignment] = { + # (PrintHeadHeight.PHW128, Media.TZE_*) + (PrintHeadHeight.PHH128, Media.TZE_3_5): PrintHeadMediaAlignment(52, 24, 52), + (PrintHeadHeight.PHH128, Media.TZE_6): PrintHeadMediaAlignment(48, 32, 48), + (PrintHeadHeight.PHH128, Media.TZE_9): PrintHeadMediaAlignment(39, 50, 39), + (PrintHeadHeight.PHH128, Media.TZE_12): PrintHeadMediaAlignment(29, 70, 29), + (PrintHeadHeight.PHH128, Media.TZE_18): PrintHeadMediaAlignment(8, 112, 8), + (PrintHeadHeight.PHH128, Media.TZE_24): PrintHeadMediaAlignment(0, 128, 0), + + # (PrintHeadHeight.PHW128, Media.HS_*) + (PrintHeadHeight.PHH128, Media.HS_5_8): PrintHeadMediaAlignment(50, 28, 50), + (PrintHeadHeight.PHH128, Media.HS_8_8): PrintHeadMediaAlignment(40, 48, 40), + (PrintHeadHeight.PHH128, Media.HS_11_7): PrintHeadMediaAlignment(31, 66, 31), + (PrintHeadHeight.PHH128, Media.HS_17_7): PrintHeadMediaAlignment(11, 106, 11), + (PrintHeadHeight.PHH128, Media.HS_23_6): PrintHeadMediaAlignment(0, 128, 0), + # PT-E550W/P750W/P710BT additionally support HS 5.2, 9, 11.2, 21 but codes are unknown + + # (PrintHeadHeight.PHW560, Media.TZE_*) + (PrintHeadHeight.PHH560, Media.TZE_3_5): PrintHeadMediaAlignment(248, 48, 264), + (PrintHeadHeight.PHH560, Media.TZE_6): PrintHeadMediaAlignment(240, 64, 256), + (PrintHeadHeight.PHH560, Media.TZE_9): PrintHeadMediaAlignment(219, 106, 235), + (PrintHeadHeight.PHH560, Media.TZE_12): PrintHeadMediaAlignment(197, 150, 213), + (PrintHeadHeight.PHH560, Media.TZE_18): PrintHeadMediaAlignment(155, 234, 171), + (PrintHeadHeight.PHH560, Media.TZE_24): PrintHeadMediaAlignment(112, 320, 128), + (PrintHeadHeight.PHH560, Media.TZE_36): PrintHeadMediaAlignment(45, 454, 61), + + # (PrintHeadHeight.PHW560, Media.HS_*) + (PrintHeadHeight.PHH560, Media.HS_5_8): PrintHeadMediaAlignment(244, 56, 260), + (PrintHeadHeight.PHH560, Media.HS_8_8): PrintHeadMediaAlignment(224, 96, 240), + (PrintHeadHeight.PHH560, Media.HS_11_7): PrintHeadMediaAlignment(206, 132, 222), + (PrintHeadHeight.PHH560, Media.HS_17_7): PrintHeadMediaAlignment(166, 212, 182), + (PrintHeadHeight.PHH560, Media.HS_23_6): PrintHeadMediaAlignment(144, 256, 160), + # PT-P900/P900W/P950NW/P910BT additionally support HS 5.2, 9, 11.2, 21, 31 but codes are unknown +} + +for (phh, media), phma in PRINTHEAD_MEDIA_ALIGNMENT.items(): + assert phma.left_margin + phma.print_area + phma.right_margin == phh.value + + class StatusCodes(Enum): STATUS_REPLY = 0x00 PRINTING_DONE = 0x01 diff --git a/labelprinterkit/job.py b/labelprinterkit/job.py index f86f5f5..606f933 100644 --- a/labelprinterkit/job.py +++ b/labelprinterkit/job.py @@ -1,11 +1,14 @@ from __future__ import annotations + +from .constants import Media, Resolution, PRINTHEAD_MEDIA_ALIGNMENT from .page import PageType -from .constants import Media, Resolution +from typing import Type, TypeVar, NewType class Job: def __init__(self, media: Media, + printer: PrinterType, auto_cut: bool = True, mirror_printing: bool = False, half_cut: bool = False, @@ -16,6 +19,12 @@ def __init__(self, ): self.media = media + self.printer = printer + + try: + self.print_area = PRINTHEAD_MEDIA_ALIGNMENT[(self.printer._PRINT_HEAD_HEIGHT, self.media)].print_area + except KeyError: + raise ValueError(f"Printer {self.printer} does not support media {self.media}") self.auto_cut = auto_cut self.mirror_printing = mirror_printing @@ -36,8 +45,7 @@ def __len__(self): return len(self._pages) def add_page(self, page: PageType): - width = self.media.value.printarea - if page.width != width: + if page.width != self.print_area: raise RuntimeError('Page width does not match media width') if page.resolution != self.resolution: raise RuntimeError('Page resolution does not match media resolution') @@ -48,3 +56,8 @@ def add_page(self, page: PageType): if page.length < min_length: raise RuntimeError('Page is not long enough') self._pages.append(page) + + +from .printers.GenericPrinter import GenericPrinter + +PrinterType = NewType('PrinterType', GenericPrinter) diff --git a/labelprinterkit/label.py b/labelprinterkit/label.py index 4d603c6..36f2143 100644 --- a/labelprinterkit/label.py +++ b/labelprinterkit/label.py @@ -1,10 +1,12 @@ from __future__ import annotations + from abc import ABC, abstractmethod from logging import getLogger from math import ceil from typing import TypeVar, NamedTuple, Optional from PIL import Image, ImageChops, ImageDraw, ImageFont + try: from qrcode import QRCode as _QRCode from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_H, ERROR_CORRECT_Q @@ -92,7 +94,7 @@ def _calc_font_size(self, height: int) -> int: lower = upper upper *= 2 while True: - test = ceil((upper+lower)/2) + test = ceil((upper + lower) / 2) font = ImageFont.truetype(self.font_path, test) image = Image.new("1", font.getsize(self.text), "white") draw = ImageDraw.Draw(image) @@ -204,17 +206,17 @@ class Flag(BasePage): def __init__(self, item1: ItemType, item2: ItemType, spacing=265): rendered_images = [item1.render(), item2.render()] image_max_length = max([rendered_image.size[0] for rendered_image in rendered_images]) - length = 2*image_max_length + spacing + length = 2 * image_max_length + spacing height = max([rendered_image.size[1] for rendered_image in rendered_images]) line_length = 2 + spacing % 2 data = 0x0 - for i in range(height-1): + for i in range(height - 1): data <<= 1 if not i % 8: data |= 0x01 - b_len = round(height/8) - data <<= b_len*8 - height + b_len = round(height / 8) + data <<= b_len * 8 - height bitmap = line_length * data.to_bytes(b_len, byteorder='big') line_image = bitmap_to_image(bitmap, height, line_length) diff --git a/labelprinterkit/page.py b/labelprinterkit/page.py index ff174b6..efd66c6 100644 --- a/labelprinterkit/page.py +++ b/labelprinterkit/page.py @@ -1,4 +1,5 @@ from __future__ import annotations + from abc import ABC from math import ceil from typing import TypeVar, Tuple diff --git a/labelprinterkit/printers.py b/labelprinterkit/printers.py deleted file mode 100644 index 265653d..0000000 --- a/labelprinterkit/printers.py +++ /dev/null @@ -1,274 +0,0 @@ -from __future__ import annotations -import logging -from abc import ABC, abstractmethod -from logging import getLogger -import struct -from typing import TypeVar - -import packbits - -from .backends import BaseBackend, UniDirectionalBackend -from .constants import Resolution, ErrorCodes, MediaType, StatusCodes, NotificationCodes, TapeColor, \ - TextColor, VariousModesSettings, AdvancedModeSettings, Media -from .job import Job - -logger = getLogger(__name__) - -BackendType = TypeVar('BackendType', bound=BaseBackend) - - -class Error: - def __init__(self, byte1: int, byte2: int) -> None: - value = byte1 | (byte2 << 8) - self._errors = { - err.name: bool(value & err_code) - for err_code, err in {x.value: x for x in ErrorCodes}.items() - } - - def any(self): - return any(self._errors.values()) - - def __getattr__(self, attr): - return self._errors[attr] - - def __repr__(self): - return "".format(self._errors) - - -class Status: - def __init__(self, data: bytes) -> None: - _data = dict() - # assert data[0] == 0x80 # Print head mark - # assert data[1] == 0x20 # Size - # assert data[2] == 0x42 # Brother code - # assert data[3] == 0x30 # Series code - _data['model'] = data[4] # Model code - # assert data[4] == 0x30 # Country code - # assert data[5] == 0x00 # Reserved - # assert data[6] == 0x00 # Reserved - _data['errors'] = Error(data[8], data[9]) - _data['media_width'] = int(data[10]) - try: - _data['media_type'] = {x.value: x for x in MediaType}[int(data[11])] - except IndexError: - raise RuntimeError("Unsupported media type {data[11]}") - # assert data[12] == 0x00 # Number of colors - # assert data[13] == 0x00 # Fonts - # assert data[14] == 0x00 # Japanese Fonts - # assert data[15] == 0x00 # Mode - # assert data[16] == 0x00 # Density - # assert data[17] == 0x00 # Media length - try: - _data['status'] = {x.value: x for x in StatusCodes}[int(data[18])] - except IndexError: - raise RuntimeError("Unknown status {data[18]}") - # assert data[19] == 0x00 # Phase type - # assert data[20] == 0x00 # Phase number (higher order bytes) - # assert data[21] == 0x00 # Phase number (lower order bytes) - try: - _data['notification'] = {x.value: x for x in NotificationCodes}[int(data[22])] - except IndexError: - raise RuntimeError("Unknown notification {data[18]}") - # assert data[23] == 0x00 # Expansion area - try: - _data['tape_color'] = {x.value: x for x in TapeColor}[int(data[24])] - except IndexError: - raise RuntimeError("Unknown tape color {data[18]}") - try: - _data['text_color'] = {x.value: x for x in TextColor}[int(data[25])] - except IndexError: - raise RuntimeError("Unknown text color {data[18]}") - # data[26:29] # Hardware settings - # data[30:31] Reserved - - self._data = _data - self._media = None - - def __repr__(self) -> str: - return "".format(self._data) - - def __getattr__(self, attr): - return self._data[attr] - - def ready(self) -> bool: - return not self.errors.any() - - @property - def media(self) -> Media: - if self._media is None: - self._media = Media.get_media(self.media_width, self.media_type) - return self._media - - -class BasePrinter(ABC): - """Base class for printers. All printers define this API. Any other - methods are prefixed with a _ to indicate they are not part of the - printer API""" - - def __init__(self, backend: BackendType): - self._backend = backend - - @abstractmethod - def print(self, job: Job): - ... - - -def encode_line(bitmap_line: bytes, padding) -> bytes: - # The number of bits we need to add left or right is not always a multiple - # of 8, so we need to convert our line into an int, shift it over by the - # left margin and convert it to back again, padding to 16 bytes. - - # print("".join(f"{x:08b}".replace("0", " ") for x in bytes(bitmap_line))) - line_int = int.from_bytes(bitmap_line, byteorder='big') - line_int <<= padding - padded = line_int.to_bytes(16, byteorder='big') - - # pad to 16 bytes - compressed = packbits.encode(padded) - logger.debug("original bitmap: %s", bitmap_line) - logger.debug("padded bitmap %s", padded) - logger.debug("packbit compressed %s", compressed) - # Status: - if hasattr(self._backend, 'get_status'): - data = self._backend.get_status() - elif isinstance(self._backend, UniDirectionalBackend): - raise RuntimeError("Backend is unidirectional") - else: - self.reset() - self._backend.write(b'\x1BiS') - data = self._backend.read(32) - if not data: - raise IOError("No Response from printer") - - if len(data) < 32: - raise IOError("Invalid Response from printer") - - return Status(data) - - def print(self, job: Job): - logger.info("starting print") - - self.reset() - - if job.media in (Media.NO_MEDIA, Media.UNSUPPORTED_MEDIA): - raise RuntimeError('Unsupported Media') - - if job.resolution not in self._SUPPORTED_RESOLUTIONS: - raise RuntimeError('Resolution is not supported by this printer.') - - media_type = job.media.value.media_type.value.to_bytes(1, 'big') - media_size = job.media.value.width.to_bytes(1, 'big') - offset = job.media.value.lmargin - - various_mode = 0 - if job.auto_cut: - various_mode = various_mode | VariousModesSettings.AUTO_CUT.value - auto_cut = True - else: - auto_cut = False - if job.mirror_printing: - various_mode = various_mode | VariousModesSettings.MIRROR_PRINTING.value - various_mode = various_mode.to_bytes(1, 'big') - - advanced_mode = 0 - if job.half_cut: - if not self._FEATURE_HALF_CUT: - raise RuntimeError('Half cut is not supported by this printer.') - advanced_mode = advanced_mode | AdvancedModeSettings.HALF_CUT.value - if not job.chain: - advanced_mode = advanced_mode | AdvancedModeSettings.CHAIN_PRINTING.value - if job.special_tape: - advanced_mode = advanced_mode | AdvancedModeSettings.SPECIAL_TAPE.value - if job.resolution == Resolution.HIGH: - margin = b'\x1C\x00' - advanced_mode = advanced_mode | AdvancedModeSettings.HIGH_RESOLUTION.value - else: - margin = b'\x0E\x00' - advanced_mode = advanced_mode.to_bytes(1, 'big') - - cut_each = job.cut_each.to_bytes(1, 'big') - - for i, page in enumerate(job): - # switch dynamic command mode: enable raster mode - self._backend.write(b'\x1Bia\x01') - - # Print information command - # b'\x1Biz\x86\x01\x0c\x00\x00\x00\00\x00\x00' - information_command = b'\x1Biz\x86' + media_type + media_size + b'\x00\x00\x00\00\x00\x00' - self._backend.write(information_command) - if i == 0 and auto_cut: - # Ugly workaround - # Print information command a second time forces cutting after first page. - # No idea why this is needed, but it works - self._backend.write(information_command) - - # Various mode - logger.debug(f"various_mode: {various_mode}") - self._backend.write(b'\x1BiM' + various_mode) - - # Advanced mode - logger.debug(f"advanced_mode: {advanced_mode}") - self._backend.write(b'\x1biK' + advanced_mode) - - # margin - self._backend.write(b'\x1bid' + margin) - - if auto_cut: - # Configure after how many pages a cut should be done - self._backend.write(b'\x1BiA' + cut_each) - - # Enable compression mode - self._backend.write(b'M\x02') - - # send rastered lines - for line in page: - logging.debug(f"line: {line}") - self._backend.write(b'G' + encode_line(line, offset)) - - self._backend.write(b'Z') - - logging.debug(f"i: {i}") - if i < len(job) - 1: - self._backend.write(b'\x0C') - - # end page - self._backend.write(b'\x1A') - logger.info("end of page") - - -class P700(GenericPrinter): - pass - - -class P750W(GenericPrinter): - pass - - -class H500(GenericPrinter): - _SUPPORTED_RESOLUTIONS = (Resolution.LOW,) - _FEATURE_HALF_CUT = False - - -class E500(H500): - pass - - -class E550W(P750W): - pass diff --git a/labelprinterkit/printers/BasePrinter.py b/labelprinterkit/printers/BasePrinter.py new file mode 100644 index 0000000..f12c2d6 --- /dev/null +++ b/labelprinterkit/printers/BasePrinter.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from typing import TypeVar + +from ..backends import BaseBackend + +BackendType = TypeVar('BackendType', bound=BaseBackend) + + +class BasePrinter(ABC): + """Base class for printers. All printers define this API. Any other + methods are prefixed with a _ to indicate they are not part of the + printer API""" + + def __init__(self, backend: BackendType): + self._backend = backend + + @abstractmethod + def print(self, job: 'Job'): + ... + + +from ..job import Job diff --git a/labelprinterkit/printers/E500.py b/labelprinterkit/printers/E500.py new file mode 100644 index 0000000..7ed5b82 --- /dev/null +++ b/labelprinterkit/printers/E500.py @@ -0,0 +1,5 @@ +from .H500 import H500 + + +class E500(H500): + pass diff --git a/labelprinterkit/printers/E550W.py b/labelprinterkit/printers/E550W.py new file mode 100644 index 0000000..3e3a1aa --- /dev/null +++ b/labelprinterkit/printers/E550W.py @@ -0,0 +1,5 @@ +from .P750W import P750W + + +class E550W(P750W): + pass diff --git a/labelprinterkit/printers/Error.py b/labelprinterkit/printers/Error.py new file mode 100644 index 0000000..947a58c --- /dev/null +++ b/labelprinterkit/printers/Error.py @@ -0,0 +1,19 @@ +from ..constants import ErrorCodes + + +class Error: + def __init__(self, byte1: int, byte2: int) -> None: + value = byte1 | (byte2 << 8) + self._errors = { + err.name: bool(value & err_code) + for err_code, err in {x.value: x for x in ErrorCodes}.items() + } + + def any(self): + return any(self._errors.values()) + + def __getattr__(self, attr): + return self._errors[attr] + + def __repr__(self): + return "".format(self._errors) diff --git a/labelprinterkit/printers/GenericPrinter.py b/labelprinterkit/printers/GenericPrinter.py new file mode 100644 index 0000000..a98d28b --- /dev/null +++ b/labelprinterkit/printers/GenericPrinter.py @@ -0,0 +1,158 @@ +from typing import List + +from .BasePrinter import BasePrinter, BackendType +from .Status import Status +from .logger import logger +from .tools import encode_line +from ..backends import UniDirectionalBackend +from ..commands.AdvancedModeSettings import AdvancedModeSettings +from ..commands.Initialize import Initialize +from ..commands.Invalidate import Invalidate +from ..commands.Print import Print +from ..commands.PrintInformation import PrintInformation +from ..commands.RasterGraphicsTransfer import RasterGraphicsTransfer +from ..commands.SelectCompressionMode import SelectCompressionMode +from ..commands.SpecifyCutEachPages import SpecifyCutEachPages +from ..commands.SpecifyMarginAmount import SpecifyMarginAmount +from ..commands.StatusInformationRequest import StatusInformationRequest +from ..commands.SwitchDynamicCommandMode import SwitchDynamicCommandMode +from ..commands.VariousModeSettings import VariousModeSettings +from ..commands.ZeroRasterGraphics import ZeroRasterGraphics +from ..constants import Resolution, Media, PrintHeadHeight, PRINTHEAD_MEDIA_ALIGNMENT +from ..job import Job + + +class GenericPrinter(BasePrinter): + _SUPPORTED_RESOLUTIONS = (Resolution.LOW, Resolution.HIGH) + _FEATURE_HALF_CUT = True + _PRINT_HEAD_HEIGHT = PrintHeadHeight.PHH128 + + def __init__(self, backend: BackendType): + super().__init__(backend) + + def reset(self): + self._backend.write(Invalidate() * 100) # Invalidate command + self._backend.write(Initialize()) # Initialize command 1b 40 + + def get_status(self) -> Status: + if hasattr(self._backend, 'get_status'): + data = self._backend.get_status() + elif isinstance(self._backend, UniDirectionalBackend): + raise RuntimeError("Backend is unidirectional") + else: + self.reset() + self._backend.write(StatusInformationRequest()) + data = self._backend.read(32) + if not data: + raise IOError("No Response from printer") + + if len(data) < 32: + raise IOError("Invalid Response from printer") + + return Status(data) + + def print(self, job: Job): + try: + phma = PRINTHEAD_MEDIA_ALIGNMENT[(self._PRINT_HEAD_HEIGHT, job.media)] + except KeyError: + raise RuntimeError('Unsupported Media') + + logger.info("starting print") + + self.reset() + + if job.media in (Media.NO_MEDIA, Media.UNSUPPORTED_MEDIA): + raise RuntimeError('Unsupported Media') + + if job.resolution not in self._SUPPORTED_RESOLUTIONS: + raise RuntimeError('Resolution is not supported by this printer.') + + various_modes: List[VariousModeSettings.Settings] = list() + if job.auto_cut: + various_modes.append(VariousModeSettings.Settings.AUTO_CUT) + if job.mirror_printing: + various_modes.append(VariousModeSettings.Settings.MIRROR_PRINTING) + various_mode_command = VariousModeSettings(various_modes) + + advanced_modes: List[AdvancedModeSettings.Settings] = list() + if job.half_cut: + if not self._FEATURE_HALF_CUT: + raise RuntimeError('Half cut is not supported by this printer.') + advanced_modes.append(AdvancedModeSettings.Settings.HALF_CUT) + if not job.chain: + advanced_modes.append(AdvancedModeSettings.Settings.NO_CHAIN_PRINTING) + if job.special_tape: + advanced_modes.append(AdvancedModeSettings.Settings.SPECIAL_TAPE) + if job.resolution == Resolution.HIGH: + margin = 30 + advanced_modes.append(AdvancedModeSettings.Settings.HIGH_RESOLUTION) + else: + margin = 15 + advanced_mode_command = AdvancedModeSettings(advanced_modes) + specify_margin_command = SpecifyMarginAmount(margin) + + cut_each_command = SpecifyCutEachPages(job.cut_each) + + for i, pagedata in enumerate(job): + # switch dynamic command mode: enable raster mode + self._backend.write(SwitchDynamicCommandMode(SwitchDynamicCommandMode.Modes.RASTER)) + + # Print information command + page: PrintInformation.Page = PrintInformation.Page.OTHER_PAGE + if i == 0: + page = PrintInformation.Page.STARTING_PAGE + if i == len(job) - 1: + page = PrintInformation.Page.LAST_PAGE + + information_command = PrintInformation( + media_type=job.media.value.media_type, + high_resolution=job.resolution == Resolution.HIGH, + media_width=job.media.value.width, + page=page, + ) + + self._backend.write(information_command) + if i == 0 and job.auto_cut: + # Ugly workaround + # Print information command a second time forces cutting after first page. + # No idea why this is needed, but it works + self._backend.write(information_command) + + # Various mode + self._backend.write(various_mode_command) + + # Advanced mode + self._backend.write(advanced_mode_command) + + # margin + self._backend.write(specify_margin_command) + + if job.auto_cut: + # Configure after how many pages a cut should be done + self._backend.write(cut_each_command) + + # Enable compression mode + self._backend.write(SelectCompressionMode(SelectCompressionMode.Compression.TIFF)) + + # send rastered lines + for line in pagedata: + logger.debug(f"line: {line}") + self._backend.write( + RasterGraphicsTransfer( + encode_line( + line, + phma, + self._PRINT_HEAD_HEIGHT + ) + ) + ) + + self._backend.write(ZeroRasterGraphics()) + + logger.debug(f"i: {i}") + if i < len(job) - 1: + self._backend.write(Print()) + + # end page + self._backend.write(Print(feed=True)) + logger.info("end of page") diff --git a/labelprinterkit/printers/H500.py b/labelprinterkit/printers/H500.py new file mode 100644 index 0000000..e5e1f79 --- /dev/null +++ b/labelprinterkit/printers/H500.py @@ -0,0 +1,7 @@ +from .GenericPrinter import GenericPrinter +from ..constants import Resolution + + +class H500(GenericPrinter): + _SUPPORTED_RESOLUTIONS = (Resolution.LOW,) + _FEATURE_HALF_CUT = False diff --git a/labelprinterkit/printers/P700.py b/labelprinterkit/printers/P700.py new file mode 100644 index 0000000..6b52ebe --- /dev/null +++ b/labelprinterkit/printers/P700.py @@ -0,0 +1,5 @@ +from .GenericPrinter import GenericPrinter + + +class P700(GenericPrinter): + pass diff --git a/labelprinterkit/printers/P750W.py b/labelprinterkit/printers/P750W.py new file mode 100644 index 0000000..f406414 --- /dev/null +++ b/labelprinterkit/printers/P750W.py @@ -0,0 +1,5 @@ +from .GenericPrinter import GenericPrinter + + +class P750W(GenericPrinter): + pass diff --git a/labelprinterkit/printers/P900.py b/labelprinterkit/printers/P900.py new file mode 100644 index 0000000..9cfbefb --- /dev/null +++ b/labelprinterkit/printers/P900.py @@ -0,0 +1,6 @@ +from .GenericPrinter import GenericPrinter +from ..constants import PrintHeadHeight + + +class P900(GenericPrinter): + _PRINT_HEAD_HEIGHT = PrintHeadHeight.PHH560 diff --git a/labelprinterkit/printers/P900W.py b/labelprinterkit/printers/P900W.py new file mode 100644 index 0000000..ed4b7a8 --- /dev/null +++ b/labelprinterkit/printers/P900W.py @@ -0,0 +1,5 @@ +from labelprinterkit.printers.P900 import P900 + + +class P900W(P900): + pass diff --git a/labelprinterkit/printers/P910NW.py b/labelprinterkit/printers/P910NW.py new file mode 100644 index 0000000..85b5fa4 --- /dev/null +++ b/labelprinterkit/printers/P910NW.py @@ -0,0 +1,5 @@ +from labelprinterkit.printers.P900 import P900 + + +class P910NW(P900): + pass diff --git a/labelprinterkit/printers/P950NW.py b/labelprinterkit/printers/P950NW.py new file mode 100644 index 0000000..e23c580 --- /dev/null +++ b/labelprinterkit/printers/P950NW.py @@ -0,0 +1,5 @@ +from labelprinterkit.printers.P900 import P900 + + +class P950NW(P900): + pass diff --git a/labelprinterkit/printers/Status.py b/labelprinterkit/printers/Status.py new file mode 100644 index 0000000..caba91b --- /dev/null +++ b/labelprinterkit/printers/Status.py @@ -0,0 +1,67 @@ +from .Error import Error +from ..constants import MediaType, StatusCodes, NotificationCodes, TapeColor, TextColor, Media + + +class Status: + def __init__(self, data: bytes) -> None: + _data = dict() + # assert data[0] == 0x80 # Print head mark + # assert data[1] == 0x20 # Size + # assert data[2] == 0x42 # Brother code + # assert data[3] == 0x30 # Series code + _data['model'] = data[4] # Model code + # assert data[4] == 0x30 # Country code + # assert data[5] == 0x00 # Reserved + # assert data[6] == 0x00 # Reserved + _data['errors'] = Error(data[8], data[9]) + _data['media_width'] = int(data[10]) + try: + _data['media_type'] = {x.value: x for x in MediaType}[int(data[11])] + except IndexError: + raise RuntimeError("Unsupported media type {data[11]}") + # assert data[12] == 0x00 # Number of colors + # assert data[13] == 0x00 # Fonts + # assert data[14] == 0x00 # Japanese Fonts + # assert data[15] == 0x00 # Mode + # assert data[16] == 0x00 # Density + # assert data[17] == 0x00 # Media length + try: + _data['status'] = {x.value: x for x in StatusCodes}[int(data[18])] + except IndexError: + raise RuntimeError("Unknown status {data[18]}") + # assert data[19] == 0x00 # Phase type + # assert data[20] == 0x00 # Phase number (higher order bytes) + # assert data[21] == 0x00 # Phase number (lower order bytes) + try: + _data['notification'] = {x.value: x for x in NotificationCodes}[int(data[22])] + except IndexError: + raise RuntimeError("Unknown notification {data[18]}") + # assert data[23] == 0x00 # Expansion area + try: + _data['tape_color'] = {x.value: x for x in TapeColor}[int(data[24])] + except IndexError: + raise RuntimeError("Unknown tape color {data[18]}") + try: + _data['text_color'] = {x.value: x for x in TextColor}[int(data[25])] + except IndexError: + raise RuntimeError("Unknown text color {data[18]}") + # data[26:29] # Hardware settings + # data[30:31] Reserved + + self._data = _data + self._media = None + + def __repr__(self) -> str: + return "".format(self._data) + + def __getattr__(self, attr): + return self._data[attr] + + def ready(self) -> bool: + return not self.errors.any() + + @property + def media(self) -> Media: + if self._media is None: + self._media = Media.get_media(self.media_width, self.media_type) + return self._media diff --git a/labelprinterkit/printers/__init__.py b/labelprinterkit/printers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labelprinterkit/printers/logger.py b/labelprinterkit/printers/logger.py new file mode 100644 index 0000000..db19a4b --- /dev/null +++ b/labelprinterkit/printers/logger.py @@ -0,0 +1,3 @@ +from logging import getLogger + +logger = getLogger(__name__) diff --git a/labelprinterkit/printers/tools.py b/labelprinterkit/printers/tools.py new file mode 100644 index 0000000..8ee20fd --- /dev/null +++ b/labelprinterkit/printers/tools.py @@ -0,0 +1,29 @@ +import struct + +import packbits + +from .logger import logger +from ..constants import PrintHeadMediaAlignment, PrintHeadHeight + + +def encode_line(bitmap_line: bytes, phma: PrintHeadMediaAlignment, phh: PrintHeadHeight) -> bytes: + if phma.left_margin + phma.print_area + phma.right_margin != phh.value: + raise ValueError("Print head media alignment does not match print head height") + + # The number of bits we need to add left or right is not always a multiple + # of 8, so we need to convert our line into an int, shift it over by the + # left margin and convert it to back again, padding to 16 bytes. + + line_int = int.from_bytes(bitmap_line, byteorder='big') + line_int <<= phma.left_margin + padded = line_int.to_bytes(phh.value // 8, byteorder='big') + + # pad to 16 bytes + compressed = packbits.encode(padded) + logger.debug("original bitmap: %s", bitmap_line) + logger.debug("padded bitmap %s", padded) + logger.debug("packbit compressed %s", compressed) + #