From aff2254a9587e2d5d998150be4f7d3aef769c241 Mon Sep 17 00:00:00 2001 From: "DESKTOP-064TTA1\\Fai LUK" Date: Wed, 18 Mar 2026 23:50:19 +0800 Subject: [PATCH] no message --- python/Bag2.py | 273 +++++++++++++++++++++++++++------------ python/installAndExe.txt | 8 +- 2 files changed, 200 insertions(+), 81 deletions(-) diff --git a/python/Bag2.py b/python/Bag2.py index 8ddc79a..513ed8d 100644 --- a/python/Bag2.py +++ b/python/Bag2.py @@ -39,13 +39,19 @@ except ImportError: win32gui = None # type: ignore[assignment] try: - from PIL import Image, ImageDraw, ImageFont + from PIL import Image, ImageDraw, ImageFont, ImageOps + try: + from PIL import ImageWin # type: ignore + except Exception: + ImageWin = None # type: ignore[assignment] import qrcode _HAS_PIL_QR = True except ImportError: Image = None # type: ignore[assignment] ImageDraw = None # type: ignore[assignment] ImageFont = None # type: ignore[assignment] + ImageOps = None # type: ignore[assignment] + ImageWin = None # type: ignore[assignment] qrcode = None # type: ignore[assignment] _HAS_PIL_QR = False @@ -205,21 +211,22 @@ def generate_zpl_dataflex( label_esc = _zpl_escape(label_line) # QR payload: prefer JSON {"itemId":..., "stockInLineId":...} when both present; else fall back to lot/batch text if item_id is not None and stock_in_line_id is not None: - qr_value = _zpl_escape(json.dumps({"itemId": item_id, "stockInLineId": stock_in_line_id})) + qr_payload = json.dumps({"itemId": item_id, "stockInLineId": stock_in_line_id}) else: - qr_value = _zpl_escape(label_line if label_line else batch_no.strip()) + qr_payload = label_line if label_line else batch_no.strip() + qr_value = _zpl_escape(qr_payload) return f"""^XA ^CI28 ^PW700 ^LL500 ^PO N ^FO10,20 -^BQR,4,7^FD{qr_value}^FS +^BQN,2,4^FDQA,{qr_value}^FS ^FO170,20 ^A@R,72,72,{font_regular}^FD{desc}^FS -^FO0,290 +^FO0,200 ^A@R,72,72,{font_regular}^FD{label_esc}^FS -^FO75,290 +^FO55,200 ^A@R,88,88,{font_bold}^FD{code}^FS ^XZ""" @@ -262,22 +269,52 @@ def generate_zpl_label_small( ^XZ""" -# Label image size (pixels) for 標簽機 image printing; words drawn bigger for readability -LABEL_IMAGE_W = 520 -LABEL_IMAGE_H = 520 -LABEL_PADDING = 20 -LABEL_FONT_NAME_SIZE = 38 # item name (bigger) -LABEL_FONT_CODE_SIZE = 44 # item code (bigger) -LABEL_FONT_BATCH_SIZE = 30 # batch/lot line -LABEL_QR_SIZE = 140 # QR module size in pixels +# Label image size (pixels) for 標簽機 image printing. +# Enlarged for readability (approx +90% scale). +LABEL_IMAGE_W = 720 +LABEL_IMAGE_H = 530 +LABEL_PADDING = 23 +LABEL_FONT_NAME_SIZE = 42 +LABEL_FONT_CODE_SIZE = 49 +LABEL_FONT_BATCH_SIZE = 34 +LABEL_QR_SIZE = 210 def _get_chinese_font(size: int) -> Optional["ImageFont.FreeTypeFont"]: """Return a Chinese-capable font for PIL, or None to use default.""" if ImageFont is None: return None - # Prefer Traditional Chinese fonts on Windows - for name in ("Microsoft JhengHei UI", "Microsoft JhengHei", "MingLiU", "SimHei", "Microsoft YaHei", "SimSun"): + # Prefer real font files on Windows (font *names* may fail and silently fallback). + if os.name == "nt": + fonts_dir = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts") + for rel in ( + "msjh.ttc", # Microsoft JhengHei + "msjhl.ttc", # Microsoft JhengHei Light + "msjhbd.ttc", # Microsoft JhengHei Bold + "mingliu.ttc", + "mingliub.ttc", + "kaiu.ttf", + "msyh.ttc", # Microsoft YaHei + "msyhbd.ttc", + "simhei.ttf", + "simsun.ttc", + ): + p = os.path.join(fonts_dir, rel) + try: + if os.path.exists(p): + return ImageFont.truetype(p, size) + except (OSError, IOError): + continue + # Fallback: try common font names (may still work depending on Pillow build) + for name in ( + "Microsoft JhengHei UI", + "Microsoft JhengHei", + "MingLiU", + "MingLiU_HKSCS", + "Microsoft YaHei", + "SimHei", + "SimSun", + ): try: return ImageFont.truetype(name, size) except (OSError, IOError): @@ -328,23 +365,60 @@ def render_label_to_image( name_str = (item_name or "—").strip() max_name_w = LABEL_IMAGE_W - x_right - LABEL_PADDING if font_name: - # Wrap by text width (Pillow 8+ textbbox) or by char count + # Wrap rule: after 7 "words" (excl. parentheses). ()() not counted; +=*/. and A–Z/a–z count as 0.5. def _wrap_text(text: str, font, max_width: int) -> list: + ignore = set("()()") + half = set("+=*/.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + max_count = 6.5 + lines: list[str] = [] + current: list[str] = [] + count = 0.0 + + for ch in text: + if ch == "\n": + lines.append("".join(current).strip()) + current = [] + count = 0.0 + continue + + ch_count = 0.0 if ch in ignore else (0.5 if ch in half else 1.0) + if count + ch_count > max_count and current: + lines.append("".join(current).strip()) + current = [] + count = 0.0 + + current.append(ch) + count += ch_count + + if current: + lines.append("".join(current).strip()) + + # Max 2 rows for item name. If still long, keep everything in row 2. + if len(lines) > 2: + lines = [lines[0], "".join(lines[1:]).strip()] + + # Safety: if any line still exceeds pixel width, wrap by width as well. if hasattr(draw, "textbbox"): - words = list(text) - lines, line = [], [] - for c in words: - line.append(c) - bbox = draw.textbbox((0, 0), "".join(line), font=font) - if bbox[2] - bbox[0] > max_width and len(line) > 1: - lines.append("".join(line[:-1])) - line = [line[-1]] - if line: - lines.append("".join(line)) - return lines - # Fallback: ~12 chars per line for Chinese - chunk = 12 - return [text[i : i + chunk] for i in range(0, len(text), chunk)] + out: list[str] = [] + for ln in lines: + buf: list[str] = [] + for ch in ln: + buf.append(ch) + bbox = draw.textbbox((0, 0), "".join(buf), font=font) + if bbox[2] - bbox[0] > max_width and len(buf) > 1: + out.append("".join(buf[:-1]).strip()) + buf = [buf[-1]] + if buf: + out.append("".join(buf).strip()) + out = [x for x in out if x] + if len(out) > 2: + out = [out[0], "".join(out[1:]).strip()] + return out + + lines = [x for x in lines if x] + if len(lines) > 2: + lines = [lines[0], "".join(lines[1:]).strip()] + return lines lines = _wrap_text(name_str, font_name, max_name_w) for i, ln in enumerate(lines): draw.text((x_right, y_line + i * (LABEL_FONT_NAME_SIZE + 4)), ln, font=font_name, fill="black") @@ -368,6 +442,32 @@ def render_label_to_image( return img +def _image_to_zpl_gfa(pil_image: "Image.Image") -> str: + """ + Convert a PIL image into ZPL ^GFA (ASCII hex) so we can print Chinese reliably + on ZPL printers (USB/Windows printer or COM) without relying on GDI drivers. + """ + if Image is None or ImageOps is None: + raise RuntimeError("Pillow is required for image-to-ZPL conversion.") + # Convert to 1-bit monochrome bitmap. Invert so '1' bits represent black in ZPL. + img_bw = ImageOps.invert(pil_image.convert("L")).convert("1") + w, h = img_bw.size + bytes_per_row = (w + 7) // 8 + raw = img_bw.tobytes() + total = bytes_per_row * h + # Ensure length matches expected (Pillow should already pack per row). + if len(raw) != total: + raw = raw[:total].ljust(total, b"\x00") + hex_data = raw.hex().upper() + return f"""^XA +^PW{w} +^LL{h} +^FO0,0 +^GFA,{total},{total},{bytes_per_row},{hex_data} +^FS +^XZ""" + + def send_image_to_label_printer(printer_name: str, pil_image: "Image.Image") -> None: """ Send a PIL Image to 標簽機 via Windows GDI (so Chinese and graphics print correctly). @@ -380,38 +480,58 @@ def send_image_to_label_printer(printer_name: str, pil_image: "Image.Image") -> raise RuntimeError("Image printing is only supported for a Windows printer name (e.g. TSC TTP-246M Pro).") if win32print is None or win32ui is None or win32con is None or win32gui is None: raise RuntimeError("pywin32 is required. Run: pip install pywin32") - # Draw image to printer DC via temp BMP (GDI uses BMP) - with tempfile.NamedTemporaryFile(suffix=".bmp", delete=False) as f: - tmp_bmp = f.name + dc = win32ui.CreateDC() + dc.CreatePrinterDC(dest) + dc.StartDoc("FPSMS Label") + dc.StartPage() try: - pil_image.save(tmp_bmp, "BMP") - hbm = win32gui.LoadImage( - 0, tmp_bmp, win32con.IMAGE_BITMAP, 0, 0, - win32con.LR_LOADFROMFILE | win32con.LR_CREATEDIBSECTION, - ) - if hbm == 0: - raise RuntimeError("Failed to load label image as bitmap.") - dc = win32ui.CreateDC() - dc.CreatePrinterDC(dest) - dc.StartDoc("FPSMS Label") - dc.StartPage() + bmp_w = pil_image.width + bmp_h = pil_image.height + # Scale-to-fit printable area (important for smaller physical labels). try: - mem_dc = win32ui.CreateDCFromHandle(win32gui.CreateCompatibleDC(dc.GetSafeHdc())) - # PyCBitmap.FromHandle works across pywin32 versions - bmp = getattr(win32ui, "CreateBitmapFromHandle", lambda h: win32ui.PyCBitmap.FromHandle(h))(hbm) - mem_dc.SelectObject(bmp) - bmp_w = pil_image.width - bmp_h = pil_image.height - dc.StretchBlt((0, 0), (bmp_w, bmp_h), mem_dc, (0, 0), (bmp_w, bmp_h), win32con.SRCCOPY) - finally: - win32gui.DeleteObject(hbm) + page_w = int(dc.GetDeviceCaps(win32con.HORZRES)) + page_h = int(dc.GetDeviceCaps(win32con.VERTRES)) + except Exception: + page_w, page_h = bmp_w, bmp_h + if page_w <= 0 or page_h <= 0: + page_w, page_h = bmp_w, bmp_h + scale = min(page_w / max(1, bmp_w), page_h / max(1, bmp_h)) + out_w = max(1, int(bmp_w * scale)) + out_h = max(1, int(bmp_h * scale)) + x0 = max(0, (page_w - out_w) // 2) + y0 = max(0, (page_h - out_h) // 2) + + # Most reliable: render via Pillow ImageWin directly to printer DC. + if ImageWin is not None: + dib = ImageWin.Dib(pil_image.convert("RGB")) + dib.draw(dc.GetHandleOutput(), (x0, y0, x0 + out_w, y0 + out_h)) + else: + # Fallback: Draw image to printer DC via temp BMP (GDI uses BMP) + with tempfile.NamedTemporaryFile(suffix=".bmp", delete=False) as f: + tmp_bmp = f.name + try: + pil_image.save(tmp_bmp, "BMP") + hbm = win32gui.LoadImage( + 0, tmp_bmp, win32con.IMAGE_BITMAP, 0, 0, + win32con.LR_LOADFROMFILE | win32con.LR_CREATEDIBSECTION, + ) + if hbm == 0: + raise RuntimeError("Failed to load label image as bitmap.") + try: + mem_dc = win32ui.CreateDCFromHandle(win32gui.CreateCompatibleDC(dc.GetSafeHdc())) + bmp = getattr(win32ui, "CreateBitmapFromHandle", lambda h: win32ui.PyCBitmap.FromHandle(h))(hbm) + mem_dc.SelectObject(bmp) + dc.StretchBlt((x0, y0), (out_w, out_h), mem_dc, (0, 0), (bmp_w, bmp_h), win32con.SRCCOPY) + finally: + win32gui.DeleteObject(hbm) + finally: + try: + os.unlink(tmp_bmp) + except OSError: + pass + finally: dc.EndPage() dc.EndDoc() - finally: - try: - os.unlink(tmp_bmp) - except OSError: - pass def run_dataflex_continuous_print( @@ -1333,27 +1453,20 @@ def main() -> None: lot_no = j.get("lotNo") n = 100 if count == -1 else count try: - # Prefer image printing so Chinese displays correctly; words are bigger - if _HAS_PIL_QR and os.name == "nt" and not com.upper().startswith("COM"): - label_img = render_label_to_image( - b, item_code, item_name, - item_id=item_id, stock_in_line_id=stock_in_line_id, - lot_no=lot_no, - ) - for i in range(n): - send_image_to_label_printer(com, label_img) - if i < n - 1: - time.sleep(0.5) - else: - zpl = generate_zpl_label_small( - b, item_code, item_name, - item_id=item_id, stock_in_line_id=stock_in_line_id, - lot_no=lot_no, - ) - for i in range(n): - send_zpl_to_label_printer(com, zpl) - if i < n - 1: - time.sleep(0.5) + # Always render to image (Chinese OK), then send as ZPL graphic (^GFA). + # This is more reliable than Windows GDI and works for both Windows printer name and COM. + if not _HAS_PIL_QR: + raise RuntimeError("請先安裝 Pillow + qrcode(pip install Pillow qrcode[pil])。") + label_img = render_label_to_image( + b, item_code, item_name, + item_id=item_id, stock_in_line_id=stock_in_line_id, + lot_no=lot_no, + ) + zpl_img = _image_to_zpl_gfa(label_img) + for i in range(n): + send_zpl_to_label_printer(com, zpl_img) + if i < n - 1: + time.sleep(0.5) msg = f"已送出列印:{n} 張標簽" if count != -1 else f"已送出列印:{n} 張標簽 (連續)" messagebox.showinfo("標簽機", msg) except Exception as err: diff --git a/python/installAndExe.txt b/python/installAndExe.txt index b3d54d2..1c596cd 100644 --- a/python/installAndExe.txt +++ b/python/installAndExe.txt @@ -5,4 +5,10 @@ py -m PyInstaller --onefile --windowed --name "Bag1" Bag1.py python -m pip install pyinstaller python -m pip install --upgrade pyinstaller -python -m PyInstaller --onefile --windowed --name "Bag1" Bag1.py \ No newline at end of file +python -m PyInstaller --onefile --windowed --name "Bag1" Bag1.py + + +pip install Pillow "qrcode[pil]" + + +py -m pip install Pillow "qrcode[pil]" \ No newline at end of file