319 lines
10 KiB
Python
319 lines
10 KiB
Python
import socket # .material -> .base refactor checked
|
|
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
|