""" Query a Windows printer driver for its page and printable area. Why this matters for the P-touch: the PT-E560BT driver reports its media as 65 x 24 mm, but its *printable* area is only about 61.1 x 18.06 mm, because the print head cannot reach the edges. Anything drawn outside that window is cropped by the driver, which is why a 63 x 20 mm design printed short. Reading the values from the driver (rather than hardcoding them) means the layout adapts automatically if the tape width or media size changes. The 18 mm printable height is a hardware limit: the PT-E560BT has a 128 dot print head at 180 dpi, and 128 / 180 * 25.4 = 18.06 mm. No layout can exceed it. """ from __future__ import annotations from dataclasses import dataclass from typing import Optional # GetDeviceCaps indices (wingdi.h) HORZSIZE, VERTSIZE = 4, 6 HORZRES, VERTRES = 8, 10 LOGPIXELSX, LOGPIXELSY = 88, 90 PHYSICALWIDTH, PHYSICALHEIGHT = 110, 111 PHYSICALOFFSETX, PHYSICALOFFSETY = 112, 113 @dataclass class PrintArea: """Page and printable-area geometry of a printer driver, in millimetres.""" printer: str dpi_x: int dpi_y: int # Physical media media_width_mm: float media_height_mm: float # Area the head can actually reach printable_width_mm: float printable_height_mm: float # Where the printable area starts, relative to the physical page corner offset_x_mm: float offset_y_mm: float def describe(self) -> str: return ( f"{self.printer}: media {self.media_width_mm:.2f} x {self.media_height_mm:.2f} mm, " f"printable {self.printable_width_mm:.2f} x {self.printable_height_mm:.2f} mm " f"at ({self.offset_x_mm:.2f}, {self.offset_y_mm:.2f}) mm, " f"{self.dpi_x}x{self.dpi_y} dpi" ) def _mm(dots: float, dpi: float) -> float: return dots / dpi * 25.4 if dpi else 0.0 def query_print_area(printer_name: str) -> Optional[PrintArea]: """Read a printer's page and printable geometry. Returns None when the information is unavailable (non-Windows platform, the printer is missing, or pywin32 is not installed). Callers must fall back to configured defaults in that case. """ try: import win32ui except ImportError: return None try: dc = win32ui.CreateDC() dc.CreatePrinterDC(printer_name) except Exception as exc: # noqa: BLE001 - caller falls back to defaults print(f"Could not query print area for '{printer_name}': {exc}") return None try: dpi_x = dc.GetDeviceCaps(LOGPIXELSX) or 180 dpi_y = dc.GetDeviceCaps(LOGPIXELSY) or 180 return PrintArea( printer=printer_name, dpi_x=dpi_x, dpi_y=dpi_y, media_width_mm=_mm(dc.GetDeviceCaps(PHYSICALWIDTH), dpi_x), media_height_mm=_mm(dc.GetDeviceCaps(PHYSICALHEIGHT), dpi_y), printable_width_mm=_mm(dc.GetDeviceCaps(HORZRES), dpi_x), printable_height_mm=_mm(dc.GetDeviceCaps(VERTRES), dpi_y), offset_x_mm=_mm(dc.GetDeviceCaps(PHYSICALOFFSETX), dpi_x), offset_y_mm=_mm(dc.GetDeviceCaps(PHYSICALOFFSETY), dpi_y), ) except Exception as exc: # noqa: BLE001 print(f"GetDeviceCaps failed for '{printer_name}': {exc}") return None finally: try: dc.DeleteDC() except Exception: # noqa: BLE001 pass def apply_to_config(config, printer_name: str, margin_mm: float = 0.1) -> bool: """Resize a LabelConfig to the printer's printable area. The label content is set to the printable area (minus a small safety margin) so nothing the driver cannot reach is ever drawn. Args: config: a LabelConfig instance, modified in place printer_name: printer to query margin_mm: shrink each dimension by this much to stay clear of rounding Returns: bool: True if the config was updated """ area = query_print_area(printer_name) if area is None: return False config.media_width_mm = round(area.media_width_mm, 2) config.media_height_mm = round(area.media_height_mm, 2) config.page_width_mm = round(area.printable_width_mm - margin_mm, 2) config.page_height_mm = round(area.printable_height_mm - margin_mm, 2) config.gutter_mm = 0.0 # the content box is now the printable area itself config.match_driver_media = True # Keep the three columns tiling the new width, preserving their proportions. total = config.col_logo_mm + config.col_text_mm + config.col_code_mm if total > 0: scale = config.page_width_mm / total config.col_logo_mm = round(config.col_logo_mm * scale, 2) config.col_text_mm = round(config.col_text_mm * scale, 2) return True