Files
KCGL/inventory-backend/app/services/print/label_service.py

231 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)}")