diff --git a/python/Bag1.py b/python/Bag1.py index dce3502..45a1478 100644 --- a/python/Bag1.py +++ b/python/Bag1.py @@ -12,6 +12,7 @@ import os import select import socket import sys +import tempfile import threading import time import tkinter as tk @@ -28,8 +29,25 @@ except ImportError: try: import win32print # type: ignore[import] + import win32ui # type: ignore[import] + import win32con # type: ignore[import] + import win32gui # type: ignore[import] except ImportError: win32print = None # type: ignore[assignment] + win32ui = None # type: ignore[assignment] + win32con = None # type: ignore[assignment] + win32gui = None # type: ignore[assignment] + +try: + from PIL import Image, ImageDraw, ImageFont + import qrcode + _HAS_PIL_QR = True +except ImportError: + Image = None # type: ignore[assignment] + ImageDraw = None # type: ignore[assignment] + ImageFont = None # type: ignore[assignment] + qrcode = None # type: ignore[assignment] + _HAS_PIL_QR = False DEFAULT_BASE_URL = os.environ.get("FPSMS_BASE_URL", "http://localhost:8090/api") # When run as PyInstaller exe, save settings next to the exe; otherwise next to script @@ -240,6 +258,158 @@ 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 + + +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"): + try: + return ImageFont.truetype(name, size) + except (OSError, IOError): + continue + try: + return ImageFont.load_default() + except Exception: + return None + + +def render_label_to_image( + batch_no: str, + item_code: str, + item_name: str, + item_id: Optional[int] = None, + stock_in_line_id: Optional[int] = None, + lot_no: Optional[str] = None, +) -> "Image.Image": + """ + Render 標簽機 label as a PIL Image (white bg, black text + QR). + Use this image for printing so Chinese displays correctly; words are drawn bigger. + Requires Pillow and qrcode. Raises RuntimeError if not available. + """ + if not _HAS_PIL_QR or Image is None or qrcode is None: + raise RuntimeError("Pillow and qrcode are required for image labels. Run: pip install Pillow qrcode[pil]") + img = Image.new("RGB", (LABEL_IMAGE_W, LABEL_IMAGE_H), "white") + draw = ImageDraw.Draw(img) + # QR payload (same as ZPL) + if item_id is not None and stock_in_line_id is not None: + qr_data = json.dumps({"itemId": item_id, "stockInLineId": stock_in_line_id}) + else: + qr_data = f"QA,{batch_no}" + # Draw QR top-left area + qr = qrcode.QRCode(box_size=4, border=2) + qr.add_data(qr_data) + qr.make(fit=True) + qr_img = qr.make_image(fill_color="black", back_color="white") + _resample = getattr(Image, "Resampling", Image).NEAREST + qr_img = qr_img.resize((LABEL_QR_SIZE, LABEL_QR_SIZE), _resample) + img.paste(qr_img, (LABEL_PADDING, LABEL_PADDING)) + # Fonts (bigger for readability) + font_name = _get_chinese_font(LABEL_FONT_NAME_SIZE) + font_code = _get_chinese_font(LABEL_FONT_CODE_SIZE) + font_batch = _get_chinese_font(LABEL_FONT_BATCH_SIZE) + x_right = LABEL_PADDING + LABEL_QR_SIZE + LABEL_PADDING + y_line = LABEL_PADDING + # Line 1: item name (wrap within remaining width) + 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 + def _wrap_text(text: str, font, max_width: int) -> list: + 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)] + 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") + y_line += len(lines) * (LABEL_FONT_NAME_SIZE + 4) + 8 + else: + draw.text((x_right, y_line), name_str[:30], fill="black") + y_line += LABEL_FONT_NAME_SIZE + 12 + # Item code (bigger) + code_str = (item_code or "—").strip() + if font_code: + draw.text((x_right, y_line), code_str, font=font_code, fill="black") + else: + draw.text((x_right, y_line), code_str, fill="black") + y_line += LABEL_FONT_CODE_SIZE + 6 + # Batch/lot line + batch_str = (lot_no or batch_no or "—").strip() + if font_batch: + draw.text((x_right, y_line), batch_str, font=font_batch, fill="black") + else: + draw.text((x_right, y_line), batch_str, fill="black") + return img + + +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). + Only supported when target is a Windows printer name (not COM port). Requires pywin32. + """ + dest = (printer_name or "").strip() + if not dest: + raise ValueError("Label printer destination is empty.") + if os.name != "nt" or dest.upper().startswith("COM"): + 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 + 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() + 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) + dc.EndPage() + dc.EndDoc() + finally: + try: + os.unlink(tmp_bmp) + except OSError: + pass + + def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: """Send ZPL to DataFlex printer via TCP. Raises on connection/send error.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -998,17 +1168,29 @@ def main() -> None: item_id = j.get("itemId") stock_in_line_id = j.get("stockInLineId") lot_no = j.get("lotNo") - 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, - ) n = 100 if count == "C" else int(count) try: - for i in range(n): - send_zpl_to_label_printer(com, zpl) - if i < n - 1: - time.sleep(0.5) + # 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) msg = f"已送出列印:{n} 張標簽" if count != "C" else f"已送出列印:{n} 張標簽 (連續)" messagebox.showinfo("標簽機", msg) except Exception as err: diff --git a/python/installAndExe.txt b/python/installAndExe.txt new file mode 100644 index 0000000..b3d54d2 --- /dev/null +++ b/python/installAndExe.txt @@ -0,0 +1,8 @@ +py -m pip install pyinstaller +py -m pip install --upgrade pyinstaller +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 diff --git a/python/requirements.txt b/python/requirements.txt index 23eb05b..52b3367 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,3 +1,5 @@ # Python dependencies for FPSMS backend integration requests>=2.28.0 pyserial>=3.5 +Pillow>=9.0.0 +qrcode[pil]>=7.0