From 4aa43a060712e8399ab1c5c1febc14a98d849b68 Mon Sep 17 00:00:00 2001 From: dxc Date: Wed, 4 Feb 2026 10:35:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=8D=B0=E6=A0=87=E7=AD=BE=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E4=BB=A5=E5=8F=8A=E5=B0=BA=E5=AF=B8=E7=A1=AE=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/services/print/label_service.py | 214 ++++++++++-------- 1 file changed, 125 insertions(+), 89 deletions(-) diff --git a/inventory-backend/app/services/print/label_service.py b/inventory-backend/app/services/print/label_service.py index b5c3d3a..315c471 100644 --- a/inventory-backend/app/services/print/label_service.py +++ b/inventory-backend/app/services/print/label_service.py @@ -25,10 +25,18 @@ class LabelPrintService: 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_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): """获取字体""" @@ -45,138 +53,177 @@ class LabelPrintService: @staticmethod def _generate_barcode_image(content, width_px, height_px): - """ - 生成真实的条形码图片 - """ + """生成真实的条形码图片""" try: - # 确保内容字符串有效 - if not content: - content = "0000000000" - - # 使用 Code128 编码(通用性最强) + if not content: content = "0000000000" 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 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. 创建画布 (白底) + # 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) + # 2. 字体配置 + # [保持] 正文内容维持 24号,以节省空间 + font_body = LabelPrintService._get_font(24) + # [修改] 编码(SKU)字体设置为 30号 + font_code = LabelPrintService._get_font(30) - # 3. 提取条码内容 (优先使用 SKU) - # 逻辑:使用自动生成的 SKU (如 00000001) 作为条码内容 + # 3. 数据准备 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 处 + GLOBAL_OFFSET_X = LabelPrintService.MARGIN_LEFT 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 + # --- 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, Y) - img.paste(bc_img, (GLOBAL_OFFSET_X - 10, CURRENT_Y)) - # 更新 Y 坐标 (条码高度 + 间距) - CURRENT_Y += bc_h + 5 + # [修改核心] 计算条形码的居中 X 坐标 + # 公式:(标签总宽 - 条码宽) / 2 + bc_x_centered = (LabelPrintService.LABEL_WIDTH - bc_w) // 2 - # --- B. 绘制文字 (统一加粗) --- + 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 '-') - 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. 如果没有前端打印参数,检查数据中的序列号 + 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')}" - - # 3. 检查数据中的批号 elif data.get('batch_number'): bottom_text = f"BN: {data.get('batch_number')}" - - # 4. 兜底 (如果什么都没有,才显示 SKU) else: bottom_text = f"NO: {sku_code}" - # ----------------------------------------------------------- + # 2. 依次调用自动换行绘制函数 (使用正文字体 font_body,且坐标使用 GLOBAL_OFFSET_X) - lines = [ - f"名: {name}", - f"规: {spec}", - f"库: {loc}", - f"属: {attr}", - f"{bottom_text}" # 这里放置计算好的 SN/BN 文字 - ] + # 绘制名称 + 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 - line_height = 36 # 行高 (字号30 + 6间距) + # 绘制规格 + 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 - for line in lines: - # 边界检查 - if CURRENT_Y + UNIFIED_FONT_SIZE > LabelPrintService.LABEL_HEIGHT: - break + # 绘制库位 + 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 - # 绘制文字 (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 + # 绘制属性 + 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 @@ -194,19 +241,12 @@ class LabelPrintService: 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" @@ -214,18 +254,14 @@ class LabelPrintService: "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)}") \ No newline at end of file