import socket import base64 import os from io import BytesIO from PIL import Image, ImageDraw, ImageFont # 引入条形码生成库 try: import barcode from barcode.writer import ImageWriter except ImportError: print("❌ 警告: 未安装 python-barcode 库,无法生成真实条形码。请执行: pip install python-barcode") class LabelPrintService: PRINTER_IP = "192.168.9.205" PRINTER_PORT = 9100 # ================= 1. 尺寸与分辨率配置 (300 DPI) ================= DOTS_PER_MM = 12 # 300 DPI LABEL_WIDTH_MM = 40 LABEL_HEIGHT_MM = 30 # 画布像素: 40mm -> 480px, 30mm -> 360px LABEL_WIDTH = int(LABEL_WIDTH_MM * DOTS_PER_MM) LABEL_HEIGHT = int(LABEL_HEIGHT_MM * DOTS_PER_MM) # 顶部留白: 10mm (120px) - 内容从这里开始 TOP_MARGIN_MM = 2 TOP_MARGIN_PX = int(TOP_MARGIN_MM * DOTS_PER_MM) @staticmethod def _get_font(size): """获取字体""" # 尝试加载中文字体,否则乱码 font_names = ["simhei.ttf", "msyh.ttf", "SimHei.ttf", "arial.ttf"] base_dirs = [os.getcwd(), os.path.dirname(__file__)] for d in base_dirs: for name in font_names: path = os.path.join(d, name) if os.path.exists(path): return ImageFont.truetype(path, size) return ImageFont.load_default() @staticmethod def _generate_barcode_image(content, width_px, height_px): """ 生成真实的条形码图片 """ try: # 确保内容字符串有效 if not content: content = "0000000000" # 使用 Code128 编码(通用性最强) code128 = barcode.get('code128', content, writer=ImageWriter()) # 渲染到内存 buffer = BytesIO() # write_text=False: 不在条码下方生成自带的数字(我们自己画) code128.write(buffer, options={"write_text": False, "module_height": 10.0, "quiet_zone": 1.0}) # 打开生成的条码图 buffer.seek(0) bc_img = Image.open(buffer) # 强制调整大小以适应我们的布局 return bc_img.resize((width_px, height_px), Image.Resampling.LANCZOS) except Exception as e: print(f"条形码生成失败: {e}") # 生成失败时返回一个黑色方块占位 return Image.new('RGB', (width_px, height_px), color='black') @staticmethod def _create_image_object(data): """ [绘图层] 生成标签图片 """ # 1. 创建画布 (白底) img = Image.new('RGB', (LabelPrintService.LABEL_WIDTH, LabelPrintService.LABEL_HEIGHT), color='white') d = ImageDraw.Draw(img) # 2. 统一字体配置 (字号 30) UNIFIED_FONT_SIZE = 30 font = LabelPrintService._get_font(UNIFIED_FONT_SIZE) # 3. 提取条码内容 (优先使用 SKU) # 逻辑:使用自动生成的 SKU (如 00000001) 作为条码内容 sku_code = data.get('sku') if not sku_code: # 兜底逻辑:如果没有 SKU,尝试用 serial_number 或 global_id sku_code = data.get('serial_number') or str(data.get('global_print_id', '0000000000')).zfill(10) # ==================== 绘制布局 ==================== # X 轴偏移 (5mm) GLOBAL_OFFSET_X = int(2 * LabelPrintService.DOTS_PER_MM) # Y 轴起始: 10mm 处 CURRENT_Y = LabelPrintService.TOP_MARGIN_PX # --- A. 绘制真实条形码 (内容为 SKU) --- bc_w = int(35 * LabelPrintService.DOTS_PER_MM) # 条码宽约 28mm bc_h = int(10 * LabelPrintService.DOTS_PER_MM) # 条码高约 6mm # 生成并粘贴条码 bc_img = LabelPrintService._generate_barcode_image(sku_code, bc_w, bc_h) # 粘贴位置: (X, Y) img.paste(bc_img, (GLOBAL_OFFSET_X - 10, CURRENT_Y)) # 更新 Y 坐标 (条码高度 + 间距) CURRENT_Y += bc_h + 5 # --- B. 绘制文字 (统一加粗) --- # 准备数据 (名称、规格等) name = str(data.get('material_name', '') or '-') if len(name) > 8: name = name[:7] + ".." spec = str(data.get('spec_model', '') or '-') if len(spec) > 11: spec = spec[:10] + ".." loc = str(data.get('warehouse_loc', '') or '-') cat = str(data.get('category', '') or '') typ = str(data.get('material_type', '') or '') attr = f"{cat}/{typ}" if len(attr) > 11: attr = attr[:10] + ".." # ----------------------------------------------------------- # [修改点] 底部显示的文字:不再显示 NO.SKU,而是显示 SN 或 BN # ----------------------------------------------------------- bottom_text = "" # 1. 优先检查前端是否传了明确的 print_no (打印值) 和 print_label (序/批) if data.get('print_no'): val = str(data.get('print_no')) label_type = data.get('print_label', '') if label_type == '序': bottom_text = f"SN: {val}" elif label_type == '批': bottom_text = f"BN: {val}" else: bottom_text = f"NO: {val}" # 2. 如果没有前端打印参数,检查数据中的序列号 elif data.get('serial_number'): bottom_text = f"SN: {data.get('serial_number')}" # 3. 检查数据中的批号 elif data.get('batch_number'): bottom_text = f"BN: {data.get('batch_number')}" # 4. 兜底 (如果什么都没有,才显示 SKU) else: bottom_text = f"NO: {sku_code}" # ----------------------------------------------------------- lines = [ f"名: {name}", f"规: {spec}", f"库: {loc}", f"属: {attr}", f"{bottom_text}" # 这里放置计算好的 SN/BN 文字 ] line_height = 36 # 行高 (字号30 + 6间距) for line in lines: # 边界检查 if CURRENT_Y + UNIFIED_FONT_SIZE > LabelPrintService.LABEL_HEIGHT: break # 绘制文字 (stroke_width=1 实现加粗效果) d.text((GLOBAL_OFFSET_X, CURRENT_Y), line, font=font, fill='black', stroke_width=1, stroke_fill='black') CURRENT_Y += line_height return img @staticmethod def generate_preview_image(data): img = LabelPrintService._create_image_object(data) output_buffer = BytesIO() img.save(output_buffer, format='JPEG') base64_str = base64.b64encode(output_buffer.getvalue()).decode('utf-8') return f"data:image/jpeg;base64,{base64_str}" @staticmethod def send_to_printer(data): ip = LabelPrintService.PRINTER_IP port = LabelPrintService.PRINTER_PORT try: # 1. 获取图像 (RGB) img_rgb = LabelPrintService._create_image_object(data) # 2. 图像转二值化 (白=1, 黑=0 的逻辑,不反转) img_gray = img_rgb.convert('L') img_bw = img_gray.convert('1', dither=Image.Dither.NONE) # 获取位图数据 bitmap_data = img_bw.tobytes() width_bytes = (img_bw.width + 7) // 8 height_dots = img_bw.height # 3. TSPL 指令 header = ( f"SIZE {LabelPrintService.LABEL_WIDTH_MM} mm, {LabelPrintService.LABEL_HEIGHT_MM} mm\r\n" "GAP 2 mm, 0 mm\r\n" "CLS\r\n" "DIRECTION 1\r\n" "REFERENCE 0, 0\r\n" ).encode('gbk') bitmap_cmd = f"BITMAP 0,0,{width_bytes},{height_dots},0,".encode('gbk') footer = b"\r\nPRINT 1,1\r\n" # 4. 发送 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((ip, port)) s.sendall(header + bitmap_cmd + bitmap_data + footer) s.close() return True except Exception as e: print(f"❌ 打印异常: {e}") raise Exception(f"打印机连接失败: {str(e)}")