(first post, may not be great):
I work at an automotive manufacturing company and am currently developing a new tracking system for our part information. I'm writing a simple software to install on low-level line-side computers that are used to enter information and print labels for in-house tracking.
The goal is to use my chosen Python GUI library to get part information from the operator, then use the QRCode
and PIL.Image
modules to generate a label as a .JPG
file. All of that is working fine. The labels are generated exactly how I want them and are saved to the machine, etc.
The part I can't seem to get is printing. Most of the solutions I've found and tried open the file in the printing dialog and require the operator to click "Print" or make some sort of input action. I don't want this; it opens the door for operator errors and other label format issues.
TL;DR:
Is there a good way to send a print command to the OS from Python WITHOUT using a system dialog or prompt? I'm including what I've attempted and my current solution.
OS: Windows; Language: Python 3.12.6
First Solution:
I tried using the os.startfile()
method with the 'print'
operation, but this created the above mentioned issue of a dialog box:
from os import path, getcwd, startfile
...
# Attempt to open label.jpg file with 'print' operation
try:
startfile(file_path, "print")
# label.jpg file couldn't be found/opened
except FileNotFoundError:
...
Second Solution (Current):
After that, I looked into using the win32print
library. This got me a bit further, but I'm stumped by an error...
from PIL import Image, ImageDraw, ImageFont, ImageWin
from os import path, getcwd, startfile
import win32print
...
# prints a passed label
def print_label(filepath: str | bytes) -> None:
"""
Accepts a filepath that is opened and printed.
"""
# print the label at the passed filepath
try:
# open the label from the passed filepath
label = Image.open(filepath)
# get the default printer
printer_name = win32print.GetDefaultPrinter()
# create a printer from the default printer name
printer = win32print.OpenPrinter(printer_name)
# create a document printer
doc_printer = win32print.StartDocPrinter(printer, 1, (filepath, None, "RAW"))
# start the printer
win32print.StartPagePrinter(doc_printer)
# rotate the image to improve orientation
label = label.rotate(90, expand = True)
# save the new width and height
width, height = label.size
# draw the image on the printer
bitmap = ImageWin.Dib(label)
bitmap.draw(doc_printer, (0, 0, width, height))
# end the print job
win32print.EndPagePrinter(doc_printer)
win32print.EndDocPrinter(doc_printer)
win32print.ClosePrinter(printer)
# there was an error printing the label
except OSError as ex:
# if the code is 1155 (no printing application assigned)
if ex.winerror == 1155:
# show a popup warning the user
...
# else show a popup with the error
else:
...
Output & Error:
Default Printer: \\[servername]\[printername]
Printer Object: <PyPrinterHANDLE:2151737373152>
Initialized Printer: 199
Traceback (most recent call last):
File "\\...\wip_label_generator.py", line 290, in generate_labels
print_label(label)
File "\\...\wip_label_generator.py", line 36, in print_label
win32print.StartPagePrinter(doc_printer)
pywintypes.error: (6, 'StartPagePrinter', 'The handle is invalid.')
Conclusion:
Is there some reason this isn't working? I can't seem to find a concise or applicable explanation of this error occurring in this scope. I feel as if its in the way I'm initializing the DocPrinter
, but I'm honestly stumped.
If there's a better way to achieve this direct print operation, please tell me. I'm sure its possible using subprocess.run()
or something similar. Thanks in advance!
After a lot more research, I've come up with a pretty good solution for this specific case. Thanks to feedback from @user2357112, @MSalters, and @Mark Ransom, I was able to find some new resources.
Research & Explanation:
Combining information from these sources:
https://python-forum.io/thread-13884.html
https://stackoverflow.com/a/12725233/25419268
https://timgolden.me.uk/python/win32_how_do_i/print.html
My solution is heavily based on the Single Image code snippet on Tim Golden's site. While his code goes to great lengths to programmatically check the physical printing aspects of the printer, I didn't need this. The main breakthrough comes when Tim mentions the Device-Independent Bitmap function:
Without any extra tools, printing an image on a Windows machine is almost insanely difficult, involving at least three device contexts all related to each other at different levels and a fair amount of trial-and-error. Fortunately, there is such a thing as a device-independent bitmap (DIB) which lets you cut the Gordian knot -- or at least some of it. Even more fortunately, the Python Imaging Library supports the beast.
This leans toward what Mark Ransom mentioned, using a printer like a window.
My adaptation simply takes the pre-generated .PNG
file, scales it up, justifies it to the top-left corner of the page, and prints the file.
Code:
from PIL import ImageWin
import win32print
import win32ui
# prints a passed label
def print_label(filepath: str | bytes) -> None:
"""
Accepts a filepath that is opened and printed.
"""
# set an error flag
print_error = False
# print the label at the passed filepath
try:
# access the OS' default printer
printer_name = win32print.GetDefaultPrinter()
# create a device context from a named printer and assess the printable size of the paper.
hDC = win32ui.CreateDC()
# convert from a basic device context to a printer device context
hDC.CreatePrinterDC(printer_name)
# open the image bitmap
label = Image.open(filepath)
if label.size[0] > label.size[4]:
label = label.rotate(90)
# start the print job
hDC.StartDoc(filepath)
hDC.StartPage()
# draw the bitmap to the printer device (uses the device-independent bitmap feature)
dib = ImageWin.Dib(label)
# scale the label image up
scaled_width = label.size[0] * 3
scaled_height = label.size[4] * 3
# set the left horizontal bound of the image on the page
x1 = 0
# set the top vertical bound of the image on the page
y1 = 0
# set the right horizontal bound of the image on the page
x2 = x1 + scaled_width
# set the top vertical bound of the image on the page
y2 = y1 + scaled_height
# draw the label image onto the device context's handle using the device-independent bitmap
dib.draw(hDC.GetHandleOutput(), (x1, y1, x2, y2))
# end the print job
hDC.EndPage()
hDC.EndDoc()
hDC.DeleteDC()
# there was an error printing the label
except OSError as ex:
# if the code is 1155 (no printing application assigned)
if ex.winerror == 1155:
# show a popup warning the user
...
# else show a popup with the error
else:
...
# if the error flag was not set
if not print_error:
# show popup to confirm printing
...
Thanks for all the feedback and I hope this helps someone in the future!