修改条形码为二维码,同时对于扫码展示部分进行修改

This commit is contained in:
dxc
2026-02-09 14:48:09 +08:00
parent bdee5fb27a
commit fdf22b9973
5 changed files with 402 additions and 205 deletions

View File

@ -4,12 +4,11 @@ import os
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
# 引入条形码生成库
# 引入二维码生成库
try:
import barcode
from barcode.writer import ImageWriter
import qrcode
except ImportError:
print("❌ 警告: 未安装 python-barcode 库,无法生成真实条形码。请执行: pip install python-barcode")
print("❌ 警告: 未安装 qrcode 库,无法生成二维码。请执行: pip install qrcode[pil]")
class LabelPrintService:
@ -25,53 +24,65 @@ class LabelPrintService:
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)
# ================= 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
# 定义左边距 (3mm) - 用于正文左对齐
MARGIN_LEFT = int(3 * DOTS_PER_MM)
# 定义右边距 (防止文字贴边,留 2mm)
MARGIN_RIGHT = int(2 * DOTS_PER_MM)
# 二维码尺寸 15mm * 15mm
QR_SIZE_MM = 15
QR_SIZE_PX = int(QR_SIZE_MM * DOTS_PER_MM) # 180px
# 计算文字允许的最大像素宽度
MAX_TEXT_WIDTH = LABEL_WIDTH - MARGIN_LEFT - MARGIN_RIGHT
# 左右分栏的间距
GAP_COLUMNS = int(2 * DOTS_PER_MM) # 2mm 间距
@staticmethod
def _get_font(size):
"""获取字体"""
# 尝试加载中文字体,否则乱码
font_names = ["simhei.ttf", "msyh.ttf", "SimHei.ttf", "arial.ttf"]
base_dirs = [os.getcwd(), os.path.dirname(__file__)]
"""获取字体 (优先使用黑体/微软雅黑)"""
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):
return ImageFont.truetype(path, size)
try:
return ImageFont.truetype(path, size)
except:
continue
return ImageFont.load_default()
@staticmethod
def _generate_barcode_image(content, width_px, height_px):
"""生成真实的条形码图片"""
def _generate_qr_image(content, size_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)
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', (width_px, height_px), color='black')
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):
def draw_text_wrap(draw, text, x, y, font, max_width, line_spacing=0, stroke_width=1):
"""
[核心功能] 自动换行绘制文本
:param line_spacing: 行与行之间的额外像素距离
:return: 绘制结束后的 Y 坐标
"""
if not text:
return y
@ -79,36 +90,36 @@ class LabelPrintService:
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)
if current_line: lines.append(current_line)
current_line = char
# 将最后剩余的内容加入
if current_line:
lines.append(current_line)
# 2. 绘制每一行
# 绘制
current_y = y
font_height = font.size # 获取字号高度
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 坐标 (字高 + 行间距)
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
@ -117,121 +128,144 @@ class LabelPrintService:
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. 字体配置
# [保持] 正文内容维持 24号以节省空间
font_body = LabelPrintService._get_font(24)
# [修改] 编码(SKU)字体设置为 30号
font_code = LabelPrintService._get_font(30)
# 2. 字体配置 (字号再次加大)
# [修改] 通用字体加大到 28
font_text = LabelPrintService._get_font(28)
# [修改] SKU字体加大到 34 (特大)
font_sku = LabelPrintService._get_font(34)
# 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)
sku_code = str(data.get('sku') or data.get('serial_number') or '000000')
# ==================== 绘制布局 ====================
GLOBAL_OFFSET_X = LabelPrintService.MARGIN_LEFT
CURRENT_Y = LabelPrintService.TOP_MARGIN_PX
# --- A. 绘制条形码 (居中) ---
bc_w = int(37 * LabelPrintService.DOTS_PER_MM)
bc_h = int(8 * 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}"
attr = f"{cat}/{typ}" if (cat or typ) else "-"
# 底部文字
bottom_text = ""
# 底部编号逻辑
bottom_val = ""
bottom_label = "NO"
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}"
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_text = f"SN: {data.get('serial_number')}"
bottom_label = "SN"
bottom_val = str(data.get('serial_number'))
elif data.get('batch_number'):
bottom_text = f"BN: {data.get('batch_number')}"
bottom_label = "BN"
bottom_val = str(data.get('batch_number'))
else:
bottom_text = f"NO: {sku_code}"
bottom_val = sku_code
# 2. 依次调用自动换行绘制函数 (使用正文字体 font_body且坐标使用 GLOBAL_OFFSET_X)
bottom_text_full = f"{bottom_label}:{bottom_val}"
# 绘制名称
CURRENT_Y = LabelPrintService.draw_text_wrap(
d, f"名: {name}", GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2
# ==================== 绘制区域划分 ====================
# --- 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'
)
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
# 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'
)
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
# --- 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_Y += 2
current_right_y += LINE_SPACING
# 绘制属性
CURRENT_Y = LabelPrintService.draw_text_wrap(
d, f"属: {attr}", GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2
# 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_Y += 2
current_right_y += LINE_SPACING
# 绘制底部编号
CURRENT_Y = LabelPrintService.draw_text_wrap(
d, bottom_text, GLOBAL_OFFSET_X, CURRENT_Y, font_body, LabelPrintService.MAX_TEXT_WIDTH, line_spacing=2
# 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')
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}"
@ -241,12 +275,21 @@ class LabelPrintService:
port = LabelPrintService.PRINTER_PORT
try:
# 1. 获取 RGB 图像
img_rgb = LabelPrintService._create_image_object(data)
# 2. 转换为灰度
img_gray = img_rgb.convert('L')
img_bw = img_gray.convert('1', dither=Image.Dither.NONE)
# 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"
@ -254,8 +297,12 @@ 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"
# 5. 发送 socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((ip, port))
@ -264,4 +311,8 @@ class LabelPrintService:
return True
except Exception as e:
print(f"❌ 打印异常: {e}")
raise Exception(f"打印机连接失败: {str(e)}")
raise Exception(f"打印机连接失败: {str(e)}")
if __name__ == "__main__":
pass