如何在Python中将A4尺寸发票转换为POS尺寸发票?或使用ReportLab生成动态高度POS发票的方案
解决方案:用ReportLab直接生成80mm宽的动态高度POS发票
既然你更习惯用ReportLab,完全可以直接用它生成符合热敏打印机要求的POS发票,而不是先做A4再转换(转换容易出现布局错乱、字体过小的问题)。下面是具体的实现思路和代码示例,适配80mm宽度的热敏纸,并且高度会根据内容自动调整。
核心思路
热敏纸的关键参数是固定宽度(80mm)+ 动态高度,ReportLab可以通过以下方式实现:
- 直接用
mm单位定义宽度,ReportLab会自动完成单位转换 - 初始化画布时设置一个足够大的临时高度(确保能容纳所有内容)
- 逐行绘制发票内容,实时记录最后一行的位置
- 最后调整画布的实际高度为内容的总高度,避免生成空白过多的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")
关键注意事项
- 字体适配:如果需要打印中文,记得注册热敏打印机支持的中文字体(比如黑体),避免出现乱码。如果打印机内置中文字体,也可以直接用ReportLab的默认字体配合打印机的字体映射。
- 内容截断/换行:商品名称过长时,要处理成换行或截断,避免超出80mm宽度。
- 边距设置:预留5mm左右的左右边距,防止内容被打印机的裁切边切掉。
- 打印设置:生成PDF后,打印时选择“适合纸张宽度”选项,确保内容完全适配热敏纸。
关于A4转POS的替代方案
如果一定要基于已有的A4发票转换,不建议直接缩放PDF(会导致字体过小、布局错乱),更靠谱的方式是:
- 用
pdfplumber或PyPDF2提取A4发票中的文本和结构化数据(抬头、商品明细、总计等) - 用上面的
generate_pos_invoice函数,将提取到的数据重新生成POS格式的发票
这种方式能保证POS发票的可读性和布局合理性,比直接缩放效果好得多。
内容的提问来源于stack exchange,提问作者Hanzla Khalid




