|
|
@@ -12,6 +12,7 @@ import os |
|
|
import select |
|
|
import select |
|
|
import socket |
|
|
import socket |
|
|
import sys |
|
|
import sys |
|
|
|
|
|
import tempfile |
|
|
import threading |
|
|
import threading |
|
|
import time |
|
|
import time |
|
|
import tkinter as tk |
|
|
import tkinter as tk |
|
|
@@ -28,8 +29,25 @@ except ImportError: |
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
import win32print # type: ignore[import] |
|
|
import win32print # type: ignore[import] |
|
|
|
|
|
import win32ui # type: ignore[import] |
|
|
|
|
|
import win32con # type: ignore[import] |
|
|
|
|
|
import win32gui # type: ignore[import] |
|
|
except ImportError: |
|
|
except ImportError: |
|
|
win32print = None # type: ignore[assignment] |
|
|
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") |
|
|
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 |
|
|
# 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""" |
|
|
^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: |
|
|
def send_zpl_to_dataflex(ip: str, port: int, zpl: str) -> None: |
|
|
"""Send ZPL to DataFlex printer via TCP. Raises on connection/send error.""" |
|
|
"""Send ZPL to DataFlex printer via TCP. Raises on connection/send error.""" |
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
@@ -998,17 +1168,29 @@ def main() -> None: |
|
|
item_id = j.get("itemId") |
|
|
item_id = j.get("itemId") |
|
|
stock_in_line_id = j.get("stockInLineId") |
|
|
stock_in_line_id = j.get("stockInLineId") |
|
|
lot_no = j.get("lotNo") |
|
|
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) |
|
|
n = 100 if count == "C" else int(count) |
|
|
try: |
|
|
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} 張標簽 (連續)" |
|
|
msg = f"已送出列印:{n} 張標簽" if count != "C" else f"已送出列印:{n} 張標簽 (連續)" |
|
|
messagebox.showinfo("標簽機", msg) |
|
|
messagebox.showinfo("標簽機", msg) |
|
|
except Exception as err: |
|
|
except Exception as err: |
|
|
|