pythonwindowsprinting

Is there a better way to print a .JPG file with Python?


(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!


Solution

  • 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!