Printing on paper with Python 🖨️
Tags: python
Published: 2021-05-11 15:09:11

# The original print tool

In 2019, I was requested to create a solution for printing placards on paper. These placards contained serial numbers, an order number and other data related to racks with computer equipment. This data had to be represented with [QR codes](https://en.wikipedia.org/wiki/QR_code) and [bar codes](https://en.wikipedia.org/wiki/Barcode) so that the warehouse could scan them.

I was given discarded [Lenovo T470s](https://en.wikipedia.org/wiki/ThinkPad_T_series) laptops that were put in the assembly department so the personnel could use the application. Because I was tied to Windows and had to create a [GUI](https://en.wikipedia.org/wiki/Graphical_user_interface) for easy usage, I decided to create a [C#](https://en.wikipedia.org/wiki/C_Sharp_(programming_language)) program in [Microsoft Visual Studio](https://en.wikipedia.org/wiki/Microsoft_Visual_Studio) because creating a GUI for Windows was very easy that way. It would create local [HTML](https://en.wikipedia.org/wiki/HTML) files using the [Barcode Rendering Framework](https://www.nuget.org/packages/Zen.Barcode.Rendering.Framework/) [NuGet](https://www.nuget.org/) package for creating QR- and bar codes and send it to the printer set as default in Windows. It even had some logging using [NLog](https://nlog-project.org/). I just named this application "print tool".

**Original placard**
The placard rendered by the C# application
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6l0917h7fkoq1f4qonp9.png)

---

# The re-implemented print tool

In 2021, I decided to integrate the print tool into another program written in [Python](https://www.python.org/) using the [Qt](https://en.wikipedia.org/wiki/Qt_(software)) GUI toolkit. This program was a Python re-write of another C# project of mine. And the [PyQt](https://riverbankcomputing.com/software/pyqt/intro) binding really allows for a nice GUI creation with the [Qt Designer](https://doc.qt.io/qt-5/qtdesigner-manual.html). 

For the QR codes, I used the [qrcode](https://pypi.org/project/qrcode/) python module. It easily generates [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) files.

It is as simple as:
```python
import qrcode
qrcode.make('1234')
```
For bar codes, I used the [python-barcode](https://pypi.org/project/python-barcode/) module.

In the original print tool, I used HTML files and printed them. The WebBrowser class handled printing of documents, which was convenient.
```C#
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;
}
```
But with Python, this proved a little difficult. I used the to [win32api](https://pypi.org/project/pywin32/) module for printing. Which calls an application set in Windows registry to use an print the file.
```python
win32api.ShellExecute(0, 'print', filename, f'/d:"{win32print.GetDefaultPrinter()}"', '.', 0)
```
Python uses this "print" [verb](https://docs.microsoft.com/en-us/windows/win32/shell/fa-verbs), to let Windows direct a file to a printer. This is called a File Association. These file associations can be found in [Windows Registry](https://en.wikipedia.org/wiki/Windows_Registry).

Here are some examples:
```
""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"
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxba3dqtzskrnnbdvh20.png)

Printing the HTML files proved to be difficult. Firefox used to have a command line parameter to do this, which is still found in Windows Registry, but this did not work. Other programs simply printed out the HTML source.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8l459gr9dr0sbxwplkp6.png)

I tried editing the print verb for HTML files, but no luck.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1gbka8yue6fiyu7r6sp7.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vfxe78rtvgs42ok6d3v4.png)

**Printing HTML**
Printing an HTML file with notepad or WordPad also did not work.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3oysl8e1w71dz47tnbnc.png)

---

Moving away from HTML, I used the [xlsxwriter](https://pypi.org/project/XlsxWriter/) module. I had experience with this module, so it seemed a good choice. I could generate an [Excel](https://en.wikipedia.org/wiki/Microsoft_Excel) worksheet [tempfile](https://docs.python.org/3/library/tempfile.html) and put a PNG image in there.
On my development laptop, which had Excel installed, the "print" verb for xlsx files was connected to the command:
```
C:\Program Files (x86)\Microsoft Office\Root\Office16\EXCEL.EXE /q "%1"
```
While this did print on my development laptop, the IT department did not want Excel installed on the Microsoft tablets. Their reasoning was;

* It required **licenses**
* It costs valuable **storage space**
* It impacts **performance**

And apparently there was an agreement somehow in the company that prohibited the installment of MS Office on these tablets...

This also ruled out generation of [PDF](https://en.wikipedia.org/wiki/PDF) files with [PyPDF](https://pypi.org/search/?q=pypdf), because I could not install [Adobe Acrobat](https://en.wikipedia.org/wiki/Adobe_Acrobat) on these tablets either. Using deprecated, old software such as Excel Viewer and Excel Mobile would not be a good long-term option either.

I then thought the [WordPad](https://en.wikipedia.org/wiki/WordPad) application, which is native to Windows, would be a better choice. It uses this command to print:
``` 
"%ProgramFiles%\Windows NT\Accessories\WORDPAD.EXE" /p "%1"
```
I then tried to generate [RTF](https://en.wikipedia.org/wiki/Rich_Text_Format) files with Python and include images in them. The [PyRTF3](https://pypi.org/project/PyRTF3/) module did allow me to generate RTF files with text and these properly printed as well. But its image processing always rejected PNG and JPG images because it could not properly read and parse their binary headers.

Running out of options and ideas, I decided to take a look at other file formats that WordPad could handle. There I saw the **.odt** extension for [OpenDocument](https://en.wikipedia.org/wiki/OpenDocument) files. These files have proven to be the perfect outcome. I could choose between the [odfpy](https://pypi.org/project/odfpy/) module and the [relatorio](https://pypi.org/project/relatorio/) module. With relatorio, I could simply create a template file with [OpenOffice](https://en.wikipedia.org/wiki/OpenOffice.org) Writer. Load the template in Python, fill in the data and images, finally print it.

Here is a stand-alone script I created during testing.
```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()
```

The above code works fine, however...

When I tested my application on the tablet, it printed black boxes instead of images. It turns out that images that were embedded using placeholders in a template created in [Word](https://en.wikipedia.org/wiki/Microsoft_Word) or [OpenOffice](https://en.wikipedia.org/wiki/OpenOffice.org) Writer, become like that when printed by [WordPad](https://en.wikipedia.org/wiki/WordPad). While printing on my development machine, I must have still used Word to have printed these documents.

**Black boxes**
Just when you think you were finished...
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvcg7f1f064q5hul5io3.png)

---

# Creating the ODF file

Instead of filling in a template file with placeholders and embed images, I thought... maybe I need to create the ODF file from scratch and insert the images more directly rather than use placeholders. I turned to [odfpy](https://pypi.org/project/odfpy/).

I created the document and viewed it in WordPad, all looked fine inside the program. But when printing, black boxes came out yet again.

Here is the code used with [odfpy](https://pypi.org/project/odfpy/):
```python
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 file contents

I then tried to open the ODF file in Word, then right-click on the image of the QR code and use "save as picture" and then re-add this new image back into WordPad. When printing with the re-added image, the QR code was properly printed. The image was slightly less quality, and thus a little blurry pixelated due to [compression artefacts](https://en.wikipedia.org/wiki/Compression_artifact). This later was a clue to actually solve the problem.

I was able to also add other PNG files, which also printed just fine. I concluded, the [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) image itself must be the problem, not the way the ODF document is built. Eventually I found a handy program: [TweakPNG](http://entropymine.com/jason/tweakpng/), specifically to inspect PNG files. I learnt that PNG files are made up of chunks.

[http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html](http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html)

I used the [Python Image Library / Pillow](https://pypi.org/project/Pillow/) module to convert the generated image into [RGB](https://en.wikipedia.org/wiki/RGB_color_model). This changed its header:

* from: **1 bit/sample grayscale**
* into: **8 bits/sample truecolor**

```python
img = PIL.Image.open(qr_name)
img = img.convert('RGB')
```
I also tried a palettised conversion mode.
```python
img = img.convert('P')
```
Which added a palette, but this still printed out a black box.

I tried to copy the chunks of the PNG that did print, into the QR- and Bar code PNG images that Python generated. The TweakPNG tool and the PIL module both allowed to easily change these chunks.
```python
metadata = PngInfo()
pnginfo.add(b'grAb', struct.pack('>II', 10, 49))
```
PIL docs on this: [https://pillow.readthedocs.io/en/3.1.x/PIL.html?highlight=PngInfo#PIL.PngImagePlugin.PngInfo](https://pillow.readthedocs.io/en/3.1.x/PIL.html?highlight=PngInfo#PIL.PngImagePlugin.PngInfo)

But... still no good. Trying to copy the chunks as closely as possible, WordPad still printed solid black boxes.

---

# How 1 coloured pixel solved the problem

I mentioned how I was able to properly print the QR code I first opened in [Word](https://en.wikipedia.org/wiki/Microsoft_Word), then use "Save as picture" to create a new PNG image of it, which was slighly blurry because of [compression artefacts](https://en.wikipedia.org/wiki/Compression_artifact). I noticed that the artefacts, were slightly grey.

I then tried an experiment. With [MS Paint](https://en.wikipedia.org/wiki/Microsoft_Paint), I created a solid white image. On that image I drew several black (non-aliased) circles. I drew another image, also solid white background, but with coloured circles. I then added both these images manually to the ODF document and print it. The image with the black circles got printed as a solid black box, while the coloured circles got printed properly. The chunk contents did not matter at all, only the use of colour mattered.

**Colour circle test**
Testing to see how WordPad printing responds
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jz49pt7ov6qjbwjlnjiq.png)


[WordPad](https://en.wikipedia.org/wiki/WordPad) (at least, the version I was working with in 2021) apparently cannot handle images that are consistent of only black and white pixels. If I added a single coloured pixel anywhere on the PNG, WordPad suddenly is able to process the image properly for printing.

**WordPad version**
About WordPad
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ktfn48mkfhn4wkeyvrv2.png)


Using the PIL module to modify the QR- and Bar code PNG images by adding 1 red pixel and then add the image to the ODT document, finally allowed proper printing.

I reverted back to the relatorio code and here is the final script:
```python
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()
```

Yes, I could colour the QR codes itself, but adding a red pixel to resolve this after several days and much wasted paper just felt very gratifying to me. Thank you for reading!

**Final placard**
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fj1slhz34cj8kb5ae59q.png)

---

*"The enemy of art is the absense of limitations"* - [Orson Welles](https://en.wikipedia.org/wiki/Orson_Welles)