import socket import base64 import os from io import BytesIO from PIL import Image, ImageDraw, ImageFont # 引入二维码生成库 try: import qrcode except ImportError: print("❌ 警告: 未安装 qrcode 库,无法生成二维码。请执行: pip install qrcode[pil]") 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) # ================= 2. 布局配置 ================= MARGIN_LEFT = int(2 * DOTS_PER_MM) # 左边距 2mm MARGIN_RIGHT = int(1 * DOTS_PER_MM) # 右边距 1mm TOP_MARGIN = int(5 * DOTS_PER_MM) # 顶部边距 2mm # 二维码尺寸 15mm * 15mm QR_SIZE_MM = 15 QR_SIZE_PX = int(QR_SIZE_MM * DOTS_PER_MM) # 180px # 左右分栏的间距 GAP_COLUMNS = int(2 * DOTS_PER_MM) # 2mm 间距 @staticmethod def _get_font(size): """获取字体 (优先使用黑体/微软雅黑)""" font_names = ["simhei.ttf", "msyh.ttf", "SimHei.ttf", "arial.ttf", "NotoSansCJK-Regular.ttc"] base_dirs = [os.getcwd(), os.path.dirname(__file__), "/usr/share/fonts", "C:\\Windows\\Fonts"] for d in base_dirs: for name in font_names: path = os.path.join(d, name) if os.path.exists(path): try: return ImageFont.truetype(path, size) except: continue return ImageFont.load_default() @staticmethod def _generate_qr_image(content, size_px): """生成指定像素大小的二维码""" try: if not content: content = "000000" # 创建二维码对象 qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=0, ) qr.add_data(content) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") # [重要] 必须转为 RGB 模式 img = img.convert('RGB') # 调整为指定像素大小 return img.resize((size_px, size_px), Image.Resampling.LANCZOS) except Exception as e: print(f"二维码生成失败: {e}") return Image.new('RGB', (size_px, size_px), color='gray') @staticmethod def draw_text_wrap(draw, text, x, y, font, max_width, line_spacing=0, stroke_width=1): """ [核心功能] 自动换行绘制文本 """ if not text: return y lines = [] current_line = "" # 计算折行 for char in text: test_line = current_line + char width = font.getlength(test_line) if width <= max_width: current_line = test_line else: if current_line: lines.append(current_line) current_line = char if current_line: lines.append(current_line) # 绘制 current_y = y font_height = font.size for line in lines: if current_y + font_height > LabelPrintService.LABEL_HEIGHT: break draw.text( (x, current_y), line, font=font, fill='black', stroke_width=stroke_width, # 支持动态调整粗细 stroke_fill='black' ) current_y += font_height + line_spacing return current_y @staticmethod def _create_image_object(data): """ [绘图层] 生成标签图片 新布局逻辑: --------------------------------------- | [QR Code] (15mm) | 名: XXXXXX | | | 规: XXXXXX | | SKU: XXXXX(大/粗)| 属: XXXXXX | | 库: XXXXX (中/粗)| SN: XXXXXX | --------------------------------------- """ # 1. 创建画布 img = Image.new('RGB', (LabelPrintService.LABEL_WIDTH, LabelPrintService.LABEL_HEIGHT), color='white') d = ImageDraw.Draw(img) # 2. 字体配置 (字号再次加大) # [修改] 通用字体加大到 28 font_text = LabelPrintService._get_font(28) # [修改] SKU字体加大到 34 (特大) font_sku = LabelPrintService._get_font(34) # 3. 数据准备 sku_code = str(data.get('sku') or data.get('serial_number') or '000000') name = str(data.get('material_name', '') or '-') spec = str(data.get('spec_model', '') or '-') 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 (cat or typ) else "-" # 底部编号逻辑 bottom_val = "" bottom_label = "NO" if data.get('print_no'): bottom_val = str(data.get('print_no')) l_type = data.get('print_label', '') bottom_label = 'SN' if l_type == '序' else 'BN' if l_type == '批' else 'NO' elif data.get('serial_number'): bottom_label = "SN" bottom_val = str(data.get('serial_number')) elif data.get('batch_number'): bottom_label = "BN" bottom_val = str(data.get('batch_number')) else: bottom_val = sku_code bottom_text_full = f"{bottom_label}:{bottom_val}" # ==================== 绘制区域划分 ==================== # --- A. 左侧区域 (二维码 + SKU + 库位) --- qr_x = LabelPrintService.MARGIN_LEFT qr_y = LabelPrintService.TOP_MARGIN # 1. 绘制二维码 qr_img = LabelPrintService._generate_qr_image(sku_code, LabelPrintService.QR_SIZE_PX) img.paste(qr_img, (qr_x, qr_y)) # 计算中心点,用于 SKU 和 库位 居中 qr_center_x = qr_x + (LabelPrintService.QR_SIZE_PX // 2) # 2. 绘制 SKU (特大 + 特粗) # 位于二维码下方,留 6px 间距 current_left_y = qr_y + LabelPrintService.QR_SIZE_PX + 6 sku_w = font_sku.getlength(sku_code) sku_x = int(qr_center_x - (sku_w // 2)) if sku_x < 2: sku_x = 2 # 边界保护 d.text( (sku_x, current_left_y), sku_code, font=font_sku, fill='black', stroke_width=2, # [修改] SKU 增加到 2px 描边,更粗 stroke_fill='black' ) # 3. 绘制 库位 (放在 SKU 下方) # 位于 SKU 下方,留 6px 间距 current_left_y += 34 + 6 # 34是字号大致高度 loc_text = f"库:{loc}" loc_w = font_text.getlength(loc_text) loc_x = int(qr_center_x - (loc_w // 2)) if loc_x < 2: loc_x = 2 d.text( (loc_x, current_left_y), loc_text, font=font_text, fill='black', stroke_width=1, # 普通加粗 stroke_fill='black' ) # --- B. 右侧区域 (名称、规格、属性、编号) --- # 右侧起始 X right_start_x = LabelPrintService.MARGIN_LEFT + LabelPrintService.QR_SIZE_PX + LabelPrintService.GAP_COLUMNS # 右侧最大宽度 right_max_width = LabelPrintService.LABEL_WIDTH - right_start_x - LabelPrintService.MARGIN_RIGHT current_right_y = LabelPrintService.TOP_MARGIN # [修改] 增大行间距 line_spacing=8 LINE_SPACING = 8 # 1. 名称 current_right_y = LabelPrintService.draw_text_wrap( d, f"名:{name}", right_start_x, current_right_y, font_text, right_max_width, line_spacing=LINE_SPACING ) current_right_y += LINE_SPACING # 2. 规格 current_right_y = LabelPrintService.draw_text_wrap( d, f"规:{spec}", right_start_x, current_right_y, font_text, right_max_width, line_spacing=LINE_SPACING ) current_right_y += LINE_SPACING # 3. 属性 current_right_y = LabelPrintService.draw_text_wrap( d, f"属:{attr}", right_start_x, current_right_y, font_text, right_max_width, line_spacing=LINE_SPACING ) current_right_y += LINE_SPACING # 4. 序列号/批号 LabelPrintService.draw_text_wrap( d, bottom_text_full, right_start_x, current_right_y, font_text, right_max_width, line_spacing=LINE_SPACING ) return img @staticmethod def generate_preview_image(data): """生成 Base64 预览图""" img = LabelPrintService._create_image_object(data) output_buffer = BytesIO() img.save(output_buffer, format='JPEG', quality=95) 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. 转换为灰度 img_gray = img_rgb.convert('L') # 3. 二值化处理 img_bw = img_gray.point(lambda x: 0 if x < 128 else 255, '1') # 4. 生成打印指令 bitmap_data = img_bw.tobytes() width_bytes = (img_bw.width + 7) // 8 height_dots = img_bw.height # 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" # 5. 发送 socket 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)}") if __name__ == "__main__": pass