用 Python 在纸上打印🖨️
原始打印工具
重新实现的打印工具
创建 ODF 文件
PNG 文件内容
一个彩色像素如何解决问题
原始打印工具
2019年,我受邀设计一套纸质标牌打印解决方案。这些标牌包含序列号、订单号以及其他与存放计算机设备的货架相关的数据。这些数据需要以二维码和条形码的形式呈现,以便仓库人员能够扫描读取。
我拿到了一些废弃的联想T470笔记本电脑,这些电脑被放在装配部门,供员工使用某个应用程序。由于我只能使用Windows系统,而且需要创建一个易于使用的图形用户界面(GUI),所以我决定在Microsoft Visual Studio中用C#编写程序,因为这样创建Windows的GUI非常容易。程序会使用条形码渲染框架(Barcode Rendering Framework)NuGet包生成本地HTML文件,用于创建二维码和条形码,并将其发送到Windows默认打印机。它甚至还使用NLog进行日志记录。我把这个应用程序命名为“打印工具”。
重新实现的打印工具
2021年,我决定将打印工具集成到另一个用Python和Qt GUI工具包编写的程序中。这个程序是我之前用C#编写的另一个项目的Python重写版本。PyQt绑定确实让使用Qt Designer创建漂亮的GUI界面成为可能。
对于二维码,我使用了qrcode这个Python 模块。它可以轻松生成PNG文件。
很简单:
import qrcode
qrcode.make('1234')
对于条形码,我使用了python-barcode模块。
在最初的打印工具中,我使用 HTML 文件并进行打印。WebBrowser 类负责处理文档打印,这很方便。
private void PrintDocument(object sender, WebBrowserDocumentCompletedEventArgs e) {
// Print the document now that it is fully loaded.
((WebBrowser)sender).Print();
// Dispose the WebBrowser now that the task is complete.
((WebBrowser)sender).Dispose();
}
private void PrintHelpPage(Uri uri) {
logger.Info(String.Format("Print: {0}", uri));
// Create a WebBrowser instance.
WebBrowser webBrowserForPrinting = new WebBrowser();
// Add an event handler that prints the document after it loads.
webBrowserForPrinting.DocumentCompleted +=
new WebBrowserDocumentCompletedEventHandler(PrintDocument);
// Set the Url property to load the document.
webBrowserForPrinting.Url = uri;
}
但用 Python 实现起来有点困难。我使用了win32api模块进行打印。该模块会调用 Windows 注册表中设置的应用程序来打印文件。
win32api.ShellExecute(0, 'print', filename, f'/d:"{win32print.GetDefaultPrinter()}"', '.', 0)
Python 使用“print”命令,让 Windows 将文件定向到打印机。这称为文件关联。这些文件关联可以在Windows 注册表中找到。
以下是一些例子:
""C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"" /p /h "%1"
%SystemRoot%\system32\NOTEPAD.EXE /p %1
"%ProgramFiles%\Windows NT\Accessories\WORDPAD.EXE" /p "%1"
"%systemroot%\system32\mspaint.exe" /p "%1"
打印 HTML 文件很困难。Firefox 以前有一个命令行参数可以实现此功能,该参数仍然保存在 Windows 注册表中,但现在已经失效。其他程序则直接打印 HTML 源代码。
放弃 HTML 后,我使用了xlsxwriter模块。我之前用过这个模块,所以觉得它是个不错的选择。我可以生成一个Excel工作表临时文件,并在其中插入一张 PNG 图片。
在我的开发笔记本电脑上(电脑上安装了 Excel),xlsx 文件的“打印”命令绑定到了以下命令:
C:\Program Files (x86)\Microsoft Office\Root\Office16\EXCEL.EXE /q "%1"
虽然我的开发笔记本电脑可以打印出这个文件,但IT部门不希望在微软平板电脑上安装Excel。他们的理由是:
- 它需要许可证。
- 这会占用宝贵的存储空间。
- 它会影响性能
显然,公司内部存在某种协议,禁止在这些平板电脑上安装微软Office软件……
这也排除了使用PyPDF生成PDF文件的可能性,因为我也无法在这些平板电脑上安装Adobe Acrobat。使用过时的旧软件,例如 Excel Viewer 和 Excel Mobile,也不是一个好的长期选择。
我当时觉得,Windows 自带的写字板应用程序会是更好的选择。它使用以下命令进行打印:
"%ProgramFiles%\Windows NT\Accessories\WORDPAD.EXE" /p "%1"
然后我尝试用 Python 生成RTF文件并在其中插入图片。PyRTF3模块确实可以生成包含文本的 RTF 文件,而且这些文本也能正确打印。但是它的图像处理功能总是无法正确读取和解析 PNG 和 JPG 格式的图片,因为它无法正确读取和解析这些图片的二进制头部信息。
由于束手无策,我决定看看WordPad还能处理哪些其他文件格式。我发现了OpenDocument文件的.odt扩展名。事实证明,这些文件格式非常理想。我可以选择odfpy模块或relatorio模块。使用relatorio模块,我可以轻松地用OpenOffice Writer创建一个模板文件。然后在Python中加载该模板,填充数据和图像,最后打印出来。
这是我在测试期间创建的一个独立脚本。
import qrcode
import relatorio
from relatorio.templates.opendocument import Template
import tempfile
import time
import win32api
import win32print
def generate_qr(qr_input, data_holder):
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmpfile:
qr = qrcode.make(qr_input)
qr.save(tmpfile, qr.format, quality=100)
print(tmpfile.name)
return (open(tmpfile.name, 'rb'), 'image/png')
def main():
inv = {}
inv['shipmentnumber'] = '6'
inv['units'] = '3'
inv['model'] = 'MyModel'
inv['systemnumber'] = '4'
inv['productline'] = 'TestLine'
inv['page'] = '1'
inv['pagetotal'] = '99'
inv['ordernumber'] = '9341'
inv['ordernumberqr'] = generate_qr(inv['ordernumber'], inv)
basic = Template(source='', filepath='basic.odt')
with open('placard.odt', 'wb') as f:
f.write(basic.generate(o=inv).render().getvalue())
time.sleep(2)
filename = 'placard.odt'
print(f'Printing: {filename}')
win32api.ShellExecute(0, 'print', filename, f'/d:"{win32print.GetDefaultPrinter()}"', '.', 0)
if __name__ == '__main__':
main()
上述代码运行正常,但是……
我在平板电脑上测试应用程序时,打印出来的不是图片而是黑框。原来,在Word或OpenOffice Writer 中创建的模板里,如果使用占位符嵌入了图片,用WordPad打印出来就会变成这样。看来我在开发机上打印的时候,仍然使用了 Word 来打印这些文档。
创建 ODF 文件
与其在模板文件中填充占位符并嵌入图片,我想……或许我应该从头开始创建 ODF 文件,然后更直接地插入图片,而不是使用占位符。于是我求助于odfpy。
我创建了文档,并在写字板中查看,程序内一切正常。但打印时,又出现了黑框。
以下是与odfpy一起使用的代码:
from barcode.codex import Code128
from barcode.writer import ImageWriter
import qrcode
import PIL.Image
from PIL.PngImagePlugin import PngImageFile, PngInfo
from odf.opendocument import OpenDocumentText
from odf import style, text
from odf.text import P
from odf.draw import Frame, Image
from odf.style import Style, GraphicProperties, TableColumnProperties, ParagraphProperties, TableCellProperties
from odf.table import Table, TableColumn, TableRow, TableCell
import io
from os.path import join, dirname
import tempfile
import time
import win32api
import win32print
def generate_qr(qr_input: str):
qr_name = ''
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmpfile:
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(qr_input)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img = img.convert('RGB')
pixels = img.load()
pixels[0, 0] = (255, 0, 0)
img.save(tmpfile.name, dpi=(96, 96))
qr_name = tmpfile.name
return qr_name
def generate_bar(bar_input: str):
bar_name = ''
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmpfile:
Code128(bar_input, writer=ImageWriter(),).write(tmpfile, options={"write_text": False})
img = PIL.Image.open(tmpfile)
img = img.convert('RGB')
pixels = img.load()
pixels[0, 0] = (255, 0, 0)
img.save(tmpfile.name, dpi=(96, 96))
bar_name = tmpfile.name
return bar_name
def main():
outfp = io.BytesIO()
textdoc = OpenDocumentText()
p = P(text="QR code placard")
textdoc.text.addElement(p)
# Main table
tablecontents = Style(name="Table Contents")
tablecontents.addElement(ParagraphProperties(numberlines="true", linenumber="0"))
textdoc.styles.addElement(tablecontents)
widthshort = Style(name="Wshort", family="table-column")
widthshort.addElement(TableColumnProperties(columnwidth="25%"))
textdoc.automaticstyles.addElement(widthshort)
widthwide = Style(name="Wwide", family="table-column")
widthwide.addElement(TableColumnProperties(columnwidth="25%"))
textdoc.automaticstyles.addElement(widthwide)
table = Table()
table.addElement(TableColumn(numbercolumnsrepeated=4,stylename=widthshort))
table.addElement(TableColumn(numbercolumnsrepeated=3,stylename=widthwide))
tr = TableRow()
table.addElement(tr)
tc = TableCell()
tr.addElement(tc)
tc.addElement(P(stylename=tablecontents,text='Shipment:'))
tc = TableCell()
tr.addElement(tc)
tc.addElement(P(stylename=tablecontents,text=' 2'))
tc = TableCell()
tr.addElement(tc)
tc.addElement(P(stylename=tablecontents,text='System: '))
tc = TableCell()
tr.addElement(tc)
tc.addElement(P(stylename=tablecontents,text=' 3'))
textdoc.text.addElement(table)
# separate Images
order_number = '12345678'
qr = generate_qr(order_number)
p = P()
textdoc.text.addElement(p)
photoframe = Frame(width="310px", height="310px")
print(f'QR: {qr}')
href = textdoc.addPicture(qr)
photoframe.addElement(Image(href=href))
p.addElement(photoframe)
bar = generate_bar(order_number)
p = P()
textdoc.text.addElement(p)
photoframe = Frame(width="340px", height="120px")
href = textdoc.addPicture(bar)
photoframe.addElement(Image(href=href))
p.addElement(photoframe)
p = P(text='End of placard')
textdoc.text.addElement(p)
time.sleep(1)
# save tempfile
with tempfile.NamedTemporaryFile(delete=True) as tmpfile:
textdoc.save(tmpfile.name, True)
print(f'Printing: {tmpfile.name}')
if __name__ == '__main__':
main()
PNG 文件内容
然后我尝试在 Word 中打开 ODF 文件,右键单击二维码图像,选择“另存为图片”,再将这张图片重新添加到写字板中。使用重新添加的图片打印时,二维码可以正常打印出来。但由于压缩造成的图像质量略有下降,出现了一些模糊和像素化。这后来成为了解决问题的线索。
我还添加了其他PNG文件,这些文件打印效果也很好。由此我得出结论,问题肯定出在PNG图像本身,而不是ODF文档的构建方式。最终,我找到了一个专门用于检查PNG文件的实用程序: TweakPNG。我了解到PNG文件是由许多块组成的。
http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
我使用Python 图像库/Pillow模块将生成的图像转换为RGB 格式。这改变了图像的头部信息:
- 起始值:1 位/采样灰度
- 转换为:8 位/样本真彩色
img = PIL.Image.open(qr_name)
img = img.convert('RGB')
我还尝试了调色板转换模式。
img = img.convert('P')
虽然添加了调色板,但打印出来的仍然是一个黑框。
我尝试将已打印的 PNG 图像片段复制到 Python 生成的二维码和条形码 PNG 图像中。TweakPNG 工具和 PIL 模块都可以轻松地修改这些片段。
metadata = PngInfo()
pnginfo.add(b'grAb', struct.pack('>II', 10, 49))
PIL 文档中有相关说明:https://pillow.readthedocs.io/en/3.1.x/PIL.html? highlight=PngInfo#PIL.PngImagePlugin.PngInfo
但是……还是不行。我尽力复制粘贴,WordPad 仍然打印出纯黑色的方框。
一个彩色像素如何解决问题
我之前提到过,我先在Word中打开了二维码,然后使用“另存为图片”功能将其保存为 PNG 图片,但由于压缩造成的失真,图片略显模糊。我注意到这些失真部分略呈灰色。
然后我做了一个实验。我用画图软件创建了一张纯白背景的图片。在这张图片上,我画了几个黑色(无锯齿)的圆圈。我又创建了另一张图片,背景也是纯白的,但圆圈是彩色的。然后我手动将这两张图片添加到 ODF 文档中并打印出来。结果,带有黑色圆圈的图片打印出来变成了一个黑色方框,而带有彩色圆圈的图片则打印正常。图片内容本身并不重要,只有颜色的使用才会影响打印效果。
WordPad(至少是我在2021年使用的版本)似乎无法处理仅由黑白像素组成的图像。如果我在PNG图像上添加一个彩色像素,WordPad就能正确处理图像并进行打印了。
使用 PIL 模块修改 QR 码和条形码 PNG 图像,添加 1 个红色像素,然后将图像添加到 ODT 文档中,最终实现了正确的打印。
我恢复了 relatorio 代码,以下是最终脚本:
import qrcode
import relatorio
from relatorio.templates.opendocument import Template
from os.path import join, dirname
import tempfile
import time
import win32api
import win32print
def generate_qr(qr_input: str):
qr_name = ''
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmpfile:
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(qr_input)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img = img.convert('RGB')
pixels = img.load()
pixels[0, 0] = (255, 0, 0)
img.save(tmpfile.name, dpi=(96, 96))
qr_name = tmpfile.name
return qr_name
def main():
inv = {}
inv['shipmentnumber'] = '6'
inv['units'] = '3'
inv['model'] = 'MyModel'
inv['systemnumber'] = '4'
inv['productline'] = 'TestLine'
inv['page'] = '1'
inv['pagetotal'] = '99'
inv['ordernumber'] = '9341'
inv['ordernumberqr'] = (open(generate_qr(inv['ordernumber']), 'rb'), 'image/png')
basic = Template(source='', filepath='basic.odt')
with open('placard.odt', 'wb') as f:
f.write(basic.generate(o=inv).render().getvalue())
time.sleep(2)
filename = 'placard.odt'
print(f'Printing: {filename}')
# win32api.ShellExecute(0, 'print', filename, f'/d:"{win32print.GetDefaultPrinter()}"', '.', 0)
if __name__ == '__main__':
main()
是的,我可以给二维码本身上色,但花了几天时间,浪费了那么多纸,最后用一个红色像素解决了这个问题,这种感觉让我很有成就感。感谢阅读!
“艺术的敌人是缺乏限制” ——奥逊·威尔斯
文章来源:https://dev.to/pa4kev/printing-on-paper-with-python-n79









