2026-02-02 15:06:20 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 顶部留白
|
|
|
|
|
|
TOP_MARGIN_MM = 1.5
|
2026-02-02 15:06:20 +08:00
|
|
|
|
TOP_MARGIN_PX = int(TOP_MARGIN_MM * DOTS_PER_MM)
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 定义左边距 (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
|
|
|
|
|
|
|
2026-02-02 15:06:20 +08:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _get_font(size):
|
|
|
|
|
|
"""获取字体"""
|
2026-02-02 16:43:35 +08:00
|
|
|
|
# 尝试加载中文字体,否则乱码
|
2026-02-02 15:06:20 +08:00
|
|
|
|
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):
|
2026-02-04 10:35:13 +08:00
|
|
|
|
"""生成真实的条形码图片"""
|
2026-02-02 15:06:20 +08:00
|
|
|
|
try:
|
2026-02-04 10:35:13 +08:00
|
|
|
|
if not content: content = "0000000000"
|
2026-02-02 15:06:20 +08:00
|
|
|
|
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')
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
@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
|
|
|
|
|
|
|
2026-02-02 15:06:20 +08:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _create_image_object(data):
|
|
|
|
|
|
"""
|
|
|
|
|
|
[绘图层] 生成标签图片
|
|
|
|
|
|
"""
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 1. 创建画布
|
2026-02-02 15:06:20 +08:00
|
|
|
|
img = Image.new('RGB', (LabelPrintService.LABEL_WIDTH, LabelPrintService.LABEL_HEIGHT), color='white')
|
|
|
|
|
|
d = ImageDraw.Draw(img)
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 2. 字体配置
|
|
|
|
|
|
# [保持] 正文内容维持 24号,以节省空间
|
|
|
|
|
|
font_body = LabelPrintService._get_font(24)
|
|
|
|
|
|
# [修改] 编码(SKU)字体设置为 30号
|
|
|
|
|
|
font_code = LabelPrintService._get_font(30)
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 3. 数据准备
|
2026-02-02 15:06:20 +08:00
|
|
|
|
sku_code = data.get('sku')
|
|
|
|
|
|
if not sku_code:
|
2026-02-02 16:43:35 +08:00
|
|
|
|
sku_code = data.get('serial_number') or str(data.get('global_print_id', '0000000000')).zfill(10)
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
|
|
|
|
|
# ==================== 绘制布局 ====================
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
GLOBAL_OFFSET_X = LabelPrintService.MARGIN_LEFT
|
2026-02-02 15:06:20 +08:00
|
|
|
|
CURRENT_Y = LabelPrintService.TOP_MARGIN_PX
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# --- A. 绘制条形码 (居中) ---
|
2026-02-06 11:28:48 +08:00
|
|
|
|
bc_w = int(37 * LabelPrintService.DOTS_PER_MM)
|
|
|
|
|
|
bc_h = int(8 * LabelPrintService.DOTS_PER_MM) # 高度
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
|
|
|
|
|
bc_img = LabelPrintService._generate_barcode_image(sku_code, bc_w, bc_h)
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# [修改核心] 计算条形码的居中 X 坐标
|
|
|
|
|
|
# 公式:(标签总宽 - 条码宽) / 2
|
|
|
|
|
|
bc_x_centered = (LabelPrintService.LABEL_WIDTH - bc_w) // 2
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
img.paste(bc_img, (bc_x_centered, CURRENT_Y))
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# --- B. 绘制条形码下方数字 (居中 + 30号字) ---
|
|
|
|
|
|
text_y_pos = CURRENT_Y + bc_h + 2
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# [修改核心] 计算文字宽度 并 居中
|
|
|
|
|
|
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
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# --- C. 绘制其余信息 (保持左对齐 + 24号字 + 自动换行) ---
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 准备完整文本
|
|
|
|
|
|
name = str(data.get('material_name', '') or '-')
|
|
|
|
|
|
spec = str(data.get('spec_model', '') or '-')
|
2026-02-02 15:06:20 +08:00
|
|
|
|
loc = str(data.get('warehouse_loc', '') or '-')
|
|
|
|
|
|
|
|
|
|
|
|
cat = str(data.get('category', '') or '')
|
|
|
|
|
|
typ = str(data.get('material_type', '') or '')
|
|
|
|
|
|
attr = f"{cat}/{typ}"
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 底部文字
|
2026-02-02 16:43:35 +08:00
|
|
|
|
bottom_text = ""
|
|
|
|
|
|
if data.get('print_no'):
|
|
|
|
|
|
val = str(data.get('print_no'))
|
|
|
|
|
|
label_type = data.get('print_label', '')
|
2026-02-04 10:35:13 +08:00
|
|
|
|
bottom_text = f"{'SN' if label_type == '序' else 'BN' if label_type == '批' else 'NO'}: {val}"
|
2026-02-02 16:43:35 +08:00
|
|
|
|
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}"
|
|
|
|
|
|
|
2026-02-04 10:35:13 +08:00
|
|
|
|
# 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
|
|
|
|
|
|
)
|
2026-02-02 15:06:20 +08:00
|
|
|
|
|
|
|
|
|
|
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)}")
|