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