5
\$\begingroup\$

I made a code that simplifies the job of sending anything to print with the Windows API. The module has a Document object which stores a reference to the actual device context within it. You can access some of the real DC functions, like Document.dc.Polygon to draw polygons. Any recommendation or improvement that occurs to you is welcome.

import win32print
import win32gui
import win32ui
import win32con
import win32api

#https://stackoverflow.com/a/25610343/12913664
# import gdi32 (used to be able to load font files)
import ctypes
gdi32 = ctypes.WinDLL('gdi32')

#if pillow is not installed, this fails silently.
try:
    from PIL import Image, ImageWin
except ImportError:
    pass

class Anchor:
    # Each of these constants signifies a specific origin of coordinates.
    NW = 0
    N  = 1
    NE = 2
    E  = 3
    W  = 4
    SW = 5
    S  = 6
    SE = 7
    C  = 8
    

    def nw_to(new_origin, x, y, width, height):
        "Internal use. Transforms coordinates whose origin is northwest to another origin."
        
        if(new_origin == Anchor.N):
            x -= width//2
        elif(new_origin == Anchor.NE):
            x -= width
        elif(new_origin == Anchor.E):
            x -= width
            y -= height//2
        elif(new_origin == Anchor.W):
            y -= height//2
        elif(new_origin == Anchor.SW):
            y -= height
        elif(new_origin == Anchor.S):
            x -= width//2
            y -= height
        elif(new_origin == Anchor.SE):
            x -= width
            y -= height
        elif(new_origin == Anchor.C):
            x -= width//2
            y -= height//2
            
        return x, y

class Document:
    def __init__(Self, printer=None, properties={}, mapmode=win32con.MM_LOMETRIC):
        """
        Create a new instance of Document. This allows you to send to the printer.

        printer: This argument is passed to the win32print.OpenPrinter function. If None, the default printer is passed.

        properties: This argument should be a dictionary. Each dictionary key must be a valid attribute for the PyDevMode object.
        This dictionary is used to set attributes to the object returned by win32print.GetPrinter(hprinter, 2)["pDevMode"].
        By default this is an empty dictionary.

        mapmode: This argument is passed to Self.dc.SetMapMode(mapmode). By default it is win32con.MM_LOMETRIC.
        """

        #https://newcenturycomputers.net/projects/pythonicwindowsprinting.html

        if(printer is None):
            printer = win32print.GetDefaultPrinter()
        
        hprinter = win32print.OpenPrinter(printer)

        devmode = win32print.GetPrinter(hprinter, 2)["pDevMode"]

        #http://timgolden.me.uk/pywin32-docs/PyDEVMODEW.html
        for k, v in properties.items():
            setattr(devmode, k, v)

        Self.hdc = win32gui.CreateDC("WINSPOOL", printer, devmode)
        Self.dc = win32ui.CreateDCFromHandle(Self.hdc)

        #https://docs.microsoft.com/en-us/cpp/mfc/reference/cdc-class?view=msvc-170#remarks-160
        Self.dc.SetMapMode(mapmode)

    def text(Self, x, y, text, width=None, height=None, format=win32con.DT_LEFT, anchor=Anchor.NW):
        """
        Draw text on the page.

        x: The x coordinate.

        y: The y coordinate.

        text: The text to draw. It can contain line breaks and non-ascii characters among other things.

        width: The width of the rectangle where the text is drawn. By default, it is calculated.

        height: The height of the rectangle where the text is drawn. By default, it is calculated.

        format: Characteristics of the text to draw. More information here:
        https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtextw#parameters. Constants can be obtained with win32con.theconstant.
        To use more than one you can put win32con.theconstant|win32con.theconstant2...
        Default is win32con.DT_LEFT.

        anchor: Specifies the coordinate origin relative to the text. By default it is Anchor.NW.
        """
        
        #https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtextw

        calcwidth = (width is None)
        calcheight = (height is None)

        if(calcwidth or calcheight):
            rect = (0, 0, (0 if(calcwidth) else width), (0 if(calcheight) else height))
            rect = win32gui.DrawTextW(Self.hdc, text, -1, rect, format|win32con.DT_CALCRECT)[1]

            if(calcwidth):
                width = rect[2]
            if(calcheight):
                height = rect[3]

        x, y = Anchor.nw_to(anchor, x, y, width, height)

        win32gui.DrawTextW(Self.hdc, text, -1, (x, y, x+width, y+height), format)
    
    def image(Self, x, y, image, anchor=Anchor.NW):
        """
        Draw an image on the page (requires Pillow)

        x: the x coordinate

        y: the y coordinate

        image: An image-like object or an instance of str that represents the path of the image to draw.

        anchor: Specifies the coordinate origin relative to the image. By default it is Anchor.NW.
        """

        if(isinstance(image, str)):
            image = Image.open(image)

        x, y = Anchor.nw_to(anchor, x, y, image.width, image.height)
        ImageWin.Dib(image).draw(Self.hdc, (x, y, x+image.width, y+image.height))

    def line(Self, *points):
        """
        draw one or more lines.

        Each argument to this function must be a tuple of the format (x, y)
        """
        
        first_point, *points = points

        Self.dc.MoveTo(first_point)

        for point in points:
            Self.dc.LineTo(point)

    @property
    def printable_area_size(Self):
        "Gets the width and height of the printable area in logical units."
        return Self.dc.DPtoLP((Self.dc.GetDeviceCaps(win32con.HORZRES), Self.dc.GetDeviceCaps(win32con.VERTRES)))

    def close(Self):
        "Close the document. Any correction or process that needs to be done to correctly clean the memory is welcome c:"
        Self.dc.EndDoc()
        del Self.dc

def load_font_file(file):
    #https://stackoverflow.com/questions/11993290/truly-custom-font-in-tkinter
    "Upload a font file."
    
    FR_PRIVATE = 0x10
    
    file = ctypes.byref(ctypes.create_unicode_buffer(file))
    font_count = gdi32.AddFontResourceExW(file, FR_PRIVATE, 0)

    if(font_count == 0):
        raise RuntimeError("Error durante la carga de la fuente.")


# example code #
if(__name__ == "__main__"):
    doc = Document("Microsoft Print to PDF", properties=dict(Orientation=win32con.DMORIENT_LANDSCAPE), mapmode=win32con.MM_TEXT)

    # startdoc
    doc.dc.StartDoc("prueba", "prueba.pdf")
    doc.dc.StartPage()

    area_width, area_height = doc.printable_area_size

    # image
    image = Image.new("RGB", (1000, 1000), color=(200, 100, 0))
    
    doc.image(area_width//2, area_height//2, image, anchor=Anchor.C)

    # text
    doc.dc.SetBkMode(win32con.TRANSPARENT)

    font = win32ui.CreateFont({"name":"Calibri", "height":200})
    doc.dc.SelectObject(font)
    
    doc.text(area_width//2,
             area_height//2,
             "Esto es un mensaje de prueba con tilde y ñ: canción",
             width=image.width,
             format=win32con.DT_WORDBREAK|win32con.DT_CENTER,
             anchor=Anchor.C)



    # enddoc
    doc.dc.EndPage()
    doc.close()
\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

Wrong Conceptual Model

An interesting module, but you need to rethink it a bit. It is a print context, not a document. A document would hold things like paragraphs, images, charts, and what not. Your Document class holds a dc and an hdc, but no content. You should rename it to something else.

Autoclose

You have a close() method. You should implement the __enter__ and __exit__ methods, so the class could be used as a context manager in a with statement, so the close() method is automatically called.

Eg)

class Document:
    ... # other methods

    def close(self):
        """Close the document.
        Any correction or process that needs to be done to correctly clean
        the memory is welcome c:"""

        # Only close if not already closed
        if self.dc:
            self.dc.EndDoc()
            self.dc = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

...

if __name__ == '__main__':
    with Document("Microsoft Print to PDF",
                  properties=dict(Orientation=win32con.DMORIENT_LANDSCAPE),
                  mapmode=win32con.MM_TEXT) as doc:
        doc.dc.StartDoc("prueba", "prueba.pdf")
        doc.dc.StartPage()
        ... etc ...

        # no need for doc.close() at the end

You can still call doc.close() if you want to. You don't need to use the with statement and rely on the automatic closing, but if you don't then being correct when handling exceptions becomes much more difficult.

PEP 8

The Style Guide for Python Code enumerates many requirements you should follow. If you ever want to lint/check your code with pylint, pyflakes, pypy, mypy, etc., you'll find you'll have far too many messages generated by the checkers if you do not follow them!

These include:

  • Python does not require ()’s around the condition in an if statements. You should remove these.
  • white space on either side of operators, so statements like x -= width//2 should be x -= width // 2.
  • variables should be snake_case. In particular, Self should be self.

If you intend on publishing the module, and hope to have anyone contribute to help you improve the code, you'll turn those contributors off right at the start if your code isn't even close to being PEP 8 compliant!

Enum

Your Anchor class should be an Enum class. Coding from the hip, this might look like:

from enum import Enum

class Anchor(Enum):
    NW = 0
    N  = 1
    NE = 2
    E  = 3
    W  = 4
    SW = 5
    S  = 6
    SE = 7
    C  = 8

    def from_nw(self, x: int, y: int, width: int, height: int) -> tuple[int, int]:
        if self in {Anchor.NE, Anchor.E, Anchor.SE}:
            x -= width
        elif self in {Anchor.N, Anchor.C, Anchor.S}:    
            x -= width // 2

        if self in {Anchor.SE, Anchor.S, Anchor.SW}:
            y -= height
        elif self in {Anchor.E, Anchor.C, Anchor.W}:    
            y -= height // 2

        return x, y

And instead of:

        x, y = Anchor.nw_to(anchor, x, y, width, height)

you'd use it like:

        x, y = anchor.from_nw(x, y, width, height)

General

Nice to see """docstrings""". You should also add type hints to methods parameters and return values, to improve the documentation even further.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for your tips! I will leave the parentheses around the ifs, since I use them for aesthetic reasons. Self is a habit that I will hardly get rid of. I don't know why I've been doing it like this for years... It seems like a good idea to use Enum! Thanks! I will use type hints! It is my first time creating a module that I will publish, I will give you the credits without a doubt like everyone else. \$\endgroup\$
    – Dante S.
    Jan 25, 2022 at 15:00
  • \$\begingroup\$ However, I will wait for an answer that will clarify whether or not I should improve the close method. Still, you earned an upvote! Thanks again! \$\endgroup\$
    – Dante S.
    Jan 25, 2022 at 15:02
  • \$\begingroup\$ I just saw your edit! Thanks, I'll keep that in mind! However, I recommend that you notify me of the edition next time, since I found out because I took a look at the question for a reputation increase. \$\endgroup\$
    – Dante S.
    Jan 28, 2022 at 11:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.