You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Python中将A4尺寸发票转换为POS尺寸发票?或使用ReportLab生成动态高度POS发票的方案

解决方案:用ReportLab直接生成80mm宽的动态高度POS发票

既然你更习惯用ReportLab,完全可以直接用它生成符合热敏打印机要求的POS发票,而不是先做A4再转换(转换容易出现布局错乱、字体过小的问题)。下面是具体的实现思路和代码示例,适配80mm宽度的热敏纸,并且高度会根据内容自动调整。

核心思路

热敏纸的关键参数是固定宽度(80mm)+ 动态高度,ReportLab可以通过以下方式实现:

  1. 直接用mm单位定义宽度,ReportLab会自动完成单位转换
  2. 初始化画布时设置一个足够大的临时高度(确保能容纳所有内容)
  3. 逐行绘制发票内容,实时记录最后一行的位置
  4. 最后调整画布的实际高度为内容的总高度,避免生成空白过多的PDF

完整代码示例

from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib.fonts import addMapping
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

# 可选:注册中文字体(如果需要打印中文,且打印机支持该字体)
pdfmetrics.registerFont(TTFont("SimHei", "SimHei.ttf"))
addMapping("SimHei", 0, 0, "SimHei")

def generate_pos_invoice(invoice_data, output_path):
    # 定义POS热敏纸宽度:80mm
    POS_WIDTH = 80 * mm
    # 设置足够大的临时高度(2000mm,几乎能容纳所有长发票)
    TEMP_HEIGHT = 2000 * mm
    
    # 初始化画布
    c = canvas.Canvas(output_path, pagesize=(POS_WIDTH, TEMP_HEIGHT))
    # 设置字体:优先用等宽或黑体,适配热敏打印,字号建议8-12号
    c.setFont("SimHei" if "SimHei" in pdfmetrics.getRegisteredFontNames() else "Helvetica", 10)
    
    # -------------------------- 绘制发票内容 --------------------------
    # 初始化当前y坐标(从顶部开始,预留边距)
    current_y = TEMP_HEIGHT - 10 * mm
    
    # 1. 发票抬头(居中)
    c.drawCentredString(POS_WIDTH / 2, current_y, "XX连锁超市")
    current_y -= 12 * mm
    c.drawCentredString(POS_WIDTH / 2, current_y, "POS消费发票")
    current_y -= 15 * mm
    
    # 2. 基础信息
    c.drawString(5 * mm, current_y, f"顾客姓名: {invoice_data['customer_name']}")
    c.drawRightString(POS_WIDTH - 5 * mm, current_y, f"日期: {invoice_data['date']}")
    current_y -= 8 * mm
    c.drawString(5 * mm, current_y, f"单号: {invoice_data['order_no']}")
    current_y -= 10 * mm
    
    # 3. 商品列表表头
    c.drawString(5 * mm, current_y, "商品名称")
    c.drawRightString(POS_WIDTH - 20 * mm, current_y, "单价")
    c.drawRightString(POS_WIDTH - 5 * mm, current_y, "总价")
    current_y -= 8 * mm
    # 绘制分隔线
    c.line(5 * mm, current_y, POS_WIDTH - 5 * mm, current_y)
    current_y -= 8 * mm
    
    # 4. 商品明细
    for item in invoice_data['items']:
        # 商品名称太长时截断处理,避免超出宽度
        truncated_name = item['name'][:15] + "..." if len(item['name']) > 15 else item['name']
        c.drawString(5 * mm, current_y, truncated_name)
        c.drawRightString(POS_WIDTH - 20 * mm, current_y, f"¥{item['unit_price']:.2f}")
        c.drawRightString(POS_WIDTH - 5 * mm, current_y, f"¥{item['total_price']:.2f}")
        current_y -= 8 * mm
    
    # 5. 总计信息
    current_y -= 5 * mm
    c.line(5 * mm, current_y, POS_WIDTH - 5 * mm, current_y)
    current_y -= 8 * mm
    c.drawString(5 * mm, current_y, "应收金额:")
    c.drawRightString(POS_WIDTH - 5 * mm, current_y, f"¥{invoice_data['total_amount']:.2f}")
    current_y -= 8 * mm
    c.drawString(5 * mm, current_y, "实收金额:")
    c.drawRightString(POS_WIDTH - 5 * mm, current_y, f"¥{invoice_data['paid_amount']:.2f}")
    current_y -= 8 * mm
    c.drawString(5 * mm, current_y, "找零:")
    c.drawRightString(POS_WIDTH - 5 * mm, current_y, f"¥{invoice_data['change']:.2f}")
    current_y -= 10 * mm
    
    # 6. 底部提示
    c.drawCentredString(POS_WIDTH / 2, current_y, "感谢您的光临,欢迎下次再来!")
    current_y -= 10 * mm
    
    # -------------------------- 调整画布高度 --------------------------
    # 计算实际内容的总高度
    actual_height = TEMP_HEIGHT - current_y
    # 设置画布的实际页面大小
    c.setPageSize((POS_WIDTH, actual_height))
    
    # 保存PDF
    c.save()

# 测试数据
sample_invoice = {
    "customer_name": "李四",
    "date": "2024-05-20 14:30",
    "order_no": "POS20240520001",
    "items": [
        {"name": "精品红富士苹果", "unit_price": 12.9, "total_price": 25.8},
        {"name": "纯牛奶250ml*12", "unit_price": 39.9, "total_price": 39.9},
        {"name": "全麦吐司面包", "unit_price": 8.5, "total_price": 8.5},
        {"name": "瓶装矿泉水", "unit_price": 2.0, "total_price": 4.0}
    ],
    "total_amount": 78.2,
    "paid_amount": 100.0,
    "change": 21.8
}

# 生成POS发票
generate_pos_invoice(sample_invoice, "pos_invoice.pdf")

关键注意事项

  1. 字体适配:如果需要打印中文,记得注册热敏打印机支持的中文字体(比如黑体),避免出现乱码。如果打印机内置中文字体,也可以直接用ReportLab的默认字体配合打印机的字体映射。
  2. 内容截断/换行:商品名称过长时,要处理成换行或截断,避免超出80mm宽度。
  3. 边距设置:预留5mm左右的左右边距,防止内容被打印机的裁切边切掉。
  4. 打印设置:生成PDF后,打印时选择“适合纸张宽度”选项,确保内容完全适配热敏纸。

关于A4转POS的替代方案

如果一定要基于已有的A4发票转换,不建议直接缩放PDF(会导致字体过小、布局错乱),更靠谱的方式是:

  1. pdfplumberPyPDF2提取A4发票中的文本和结构化数据(抬头、商品明细、总计等)
  2. 用上面的generate_pos_invoice函数,将提取到的数据重新生成POS格式的发票

这种方式能保证POS发票的可读性和布局合理性,比直接缩放效果好得多。

内容的提问来源于stack exchange,提问作者Hanzla Khalid

火山引擎 最新活动