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) # 顶部留白 TOP_MARGIN_MM = 1.5 TOP_MARGIN_PX = int(TOP_MARGIN_MM * DOTS_PER_MM) # 定义左边距 (3mm) - 用于正文左对齐 MARGIN_LEFT = int(3 * DOTS_PER_MM) # 定义右边距 (防止文字贴边,留 2mm) MARGIN_RIGHT = int(2 * DOTS_PER_MM) # 计算文字允许的最大像素宽度 MAX_TEXT_WIDTH = LABEL_WIDTH - MARGIN_LEFT - MARGIN_RIGHT @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 = barcode.get('code128', content, writer=ImageWriter()) buffer = BytesIO() 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 draw_text_wrap(draw, text, x, y, font, max_width, line_spacing=0): """ [核心功能] 自动换行绘制文本 :param line_spacing: 行与行之间的额外像素距离 :return: 绘制结束后的 Y 坐标 """ if not text: return y lines = [] current_line = "" # 1. 计算折行逻辑 for char in text: # 预测加入新字符后的宽度 test_line = current_line + char width = font.getlength(test_line) if width <= max_width: current_line = test_line else: # 宽度超出,将当前行存入,新字符作为下一行开头 lines.append(current_line) current_line = char # 将最后剩余的内容加入 if current_line: lines.append(current_line) # 2. 绘制每一行 current_y = y font_height = font.size # 获取字号高度 for line in lines: # 边界检查:如果超出图片高度,停止绘制 if current_y + font_height > LabelPrintService.LABEL_HEIGHT: break # 绘制文字 (stroke_width=1 加粗) draw.text((x, current_y), line, font=font, fill='black', stroke_width=1, stroke_fill='black') # 更新 Y 坐标 (字高 + 行间距) current_y += font_height + line_spacing return current_y @staticmethod def _create_image_object(data): """ [绘图层] 生成标签图片 """ # 1. 创建画布 img = Image.new('RGB', (LabelPrintService.LABEL_WIDTH, LabelPrintService.LABEL_HEIGHT), color='white') d = ImageDraw.Draw(img) # 2. 字体配置 # [保持] 正文内容维持 24号,以节省空间 font_body = LabelPrintService._get_font(24) # [修改] 编码(SKU)字体设置为 30号 font_code = LabelPrintService._get_font(30) # 3. 数据准备 sku_code = data.get('sku') if not sku_code: sku_code = data.get('serial_number') or str(data.get('global_print_id', '0000000000')).zfill(10) # ==================== 绘制布局 ==================== GLOBAL_OFFSET_X = LabelPrintService.MARGIN_LEFT CURRENT_Y = LabelPrintService.TOP_MARGIN_PX # --- A. 绘制条形码 (居中) --- bc_w = int(35 * LabelPrintService.DOTS_PER_MM) bc_h = int(7 * LabelPrintService.DOTS_PER_MM) # 高度 bc_img = LabelPrintService._generate_barcode_image(sku_code, bc_w, bc_h) # [修改核心] 计算条形码的居中 X 坐标 # 公式:(标签总宽 - 条码宽) / 2 bc_x_centered = (LabelPrintService.LABEL_WIDTH - bc_w) // 2 img.paste(bc_img, (bc_x_centered, CURRENT_Y)) # --- B. 绘制条形码下方数字 (居中 + 30号字) --- text_y_pos = CURRENT_Y + bc_h + 2 # [修改核心] 计算文字宽度 并 居中 text_width = font_code.getlength(sku_code) text_x_centered = (LabelPrintService.LABEL_WIDTH - text_width) // 2 d.text( (text_x_centered, text_y_pos), sku_code, font=font_code, # 使用30号字体 fill='black', stroke_width=1, stroke_fill='black' ) # 更新 Y 坐标,准备开始绘制正文 # 30(字高) + 4(间距) CURRENT_Y = text_y_pos + 30 + 4 # --- C. 绘制其余信息 (保持左对齐 + 24号字 + 自动换行) --- # 1. 准备完整文本 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}" # 底部文字 bottom_text = "" if data.get('print_no'): val = str(data.get('print_no')) label_type = data.get('print_label', '') bottom_text = f"{'SN' if label_type == '序' else 'BN' if label_type == '批' else 'NO'}: {val}" elif data.get('serial_number'): bottom_text = f"SN: {data.get('serial_number')}" elif data.get('batch_number'): bottom_text = f"BN: {data.get('batch_number')}" else: bottom_text = f"NO: {sku_code}" # 2. 依次调用自动换行绘制函数 (使用正文字体 font_body,且坐标使用 GLOBAL_OFFSET_X) # 绘制名称 CURRENT_Y = LabelPrintService.draw_text_wrap( d, f"名: {name}", GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2 ) CURRENT_Y += 2 # 绘制规格 CURRENT_Y = LabelPrintService.draw_text_wrap( d, f"规: {spec}", GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2 ) CURRENT_Y += 2 # 绘制库位 CURRENT_Y = LabelPrintService.draw_text_wrap( d, f"库: {loc}", GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2 ) CURRENT_Y += 2 # 绘制属性 CURRENT_Y = LabelPrintService.draw_text_wrap( d, f"属: {attr}", GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2 ) CURRENT_Y += 2 # 绘制底部编号 CURRENT_Y = LabelPrintService.draw_text_wrap( d, bottom_text, GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2 ) 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: img_rgb = LabelPrintService._create_image_object(data) 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 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" 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)}")