Sending a Fax with Python; Using DLLs/COM objects

As engineers, we know that we should avoid reinventing the wheel. When we can, we want to use libraries written by other people to do some heavy-lifting for us. In this post, I’m going to share with you some things I learned on how to leverage existing libraries from DLLs (or any other files with COM type information like TLB or OCX files). Specifically, I’ll share some things I learned on my journey to figure out how to use Python to send a fax. So we’ll eventually show how you could use a Windows DLL that is behind the functionality in the Windows Fax and Scan utility.

What is a DLL

A DLL is a Dynamic-Linked Library. DLLs are kind of like executables. They can contain code, data, and other resources. There’s a lot to be said about DLLs, but for this post, we’re most concerned with the code part of DLLs.

So, all you need to know right now is that there is code (methods that can be called, properties that can be read, etc.) that was written by other engineers that we want to reuse in these DLL files.

How do we use DLLs from Python?

Like mentioned fore DLLs are kind of like executables. Except, rather than being executed on their own, they are loaded and used by applications instead. Once a DLL is loaded, it’s just a matter of invoking its methods properly.

There’s a few ways to go about this.

Using ctypes

ctypes is part of the Python standard library and, in part, it helps us use DLLs.

As a brief example, I’ll show you how to leverage the GetSystemMetrics method, which exists in the User32.dll which lives at Windows\System32\user32.dll.

The first step is to load the DLL. This is pretty straightforward.

import ctypes
User32 = ctypes.WinDLL('User32.dll')

Now we can start calling methods from this DLL directly!

>>> User32.GetSystemMetrics(1) # Get the height of the primary monitor
1440

Pretty cool! But you may be asking yourself: how did I know to access a method called .GetSystemMetrics and how did I know to pass in 1 as an argument to get the height of my monitor?

How do I know what methods and properties exist?

This is a hard problem. In the world of DLLs, usually you are expected to know about the methods ahead of time. That usually means being told or reading the docs. For the example above, we can read in the docs that Microsoft provides that GetSystemMetrics is a method that accepts one parameter, an integer representing an index of system information to retrieve:

int GetSystemMetrics(
  int nIndex
);

The docs also specify how an integer parameter maps to a system metric. In the table, we see SM_CYSCREEN has an index of 1 and is described as “The height of the screen of the primary display monitor, in pixels”. Based on this information, we put together than we can call User32.GetSystemMetrics(1) to get the height of the primary monitor.

What if we don’t have the docs?

Sometimes we’re not fortunate enough to know ahead of time or have references available to us. They’re also not super convenient even when you have them, either. You’ll notice that, unlike a lot of ordinary Python classes, the User32 object we made before doesn’t tell us what methods exist in the DLL. You can try calling dir(User32) but that won’t yield you any useful information.

If you went to venture on how to get this information without docs, you’d probably be told to use a DLL exporter or COM browser. Enter pywin32.

Using PyWin32

Pywin32 is an amazing library that lets you interact with the Windows API via Python. Among its many features is the win32com.client component that lets you interact with DLLs. One of the lesser-known features in PyWin32 is the ability to generate Python classes for all the DLLs methods. You can also use PythonCOM as a COM browser. You can install pywin32 using pip python -m pip install pywin32

To browse COM libraries on your system:

python -m win32com.client.combrowse

There are better COM browsers out there, but it is neat to use this.

But the really interesting part is being able to generate Python classes for the COM interfaces automagically. To get started generating a Python file you can run this command from a shell

python -m win32com.client.makepy -i

If you don’t give it an input of what library you want to generate, it will prompt you to choose one. I chose the Fax service COM Type Library (the one behind the Windows Fax and Scan tool) You’ll see an output something like this:

Microsoft Fax Service Extended COM Type Library
 {2BF34C1A-8CAC-419F-8547-32FDF6505DB8}, lcid=0, major=1, minor=0
 >>> # Use these commands in Python code to auto generate .py support
 >>> from win32com.client import gencache
 >>> gencache.EnsureModule('{2BF34C1A-8CAC-419F-8547-32FDF6505DB8}', 0, 1, 0)

Following these instructions, we can do something like this:

from win32com.client import gencache
faxcomex = gencache.EnsureModule('{2BF34C1A-8CAC-419F-8547-32FDF6505DB8}', 0, 1, 0)
print(dir(faxcomex)) # Unlike before, we can actually see some method names
print(repr(faxcomex)) # You'll notice the generated filename there, if you're curious to look.

What is interesting about the generated code is that it comes complete with docstrings.

class IFaxServer(DispatchBaseClass):
    'IFaxServer Interface'
    CLSID = IID('{D73733C7-CC80-11D0-B225-00C04FB6C2F5}')
    coclass_clsid = IID('{D73733C8-CC80-11D0-B225-00C04FB6C2F5}')

    def Connect(self, ServerName=defaultNamedNotOptArg):
        'Makes a connection to a fax server'
        return self._oleobj_.InvokeTypes(1, LCID, 1, (24, 0), ((8, 0),),ServerName
            )

    def CreateDocument(self, FileName=defaultNamedNotOptArg):
        'Creates a fax document to send'
        return self._ApplyTypes_(4, 1, (12, 0), ((8, 0),), 'CreateDocument', None,FileName
            )

So you could even do things like help(faxcomex.IFaxServer.Connect) in an interactive shell. Pretty neat! If you’re so inclined, you could even copy that generated code to something like faxcomex.py and instead of faxcomex = gencache.EnsureModule('{2BF34C1A-8CAC-419F-8547-32FDF6505DB8}', 0, 1, 0) you can simply use import faxcomex

Sending a fax

Exploring the generated Python code and reading up the Microsoft docs for the FaxComEx interfaces, I come up with the following function.

from faxcomex import FaxDocument, FaxServer
def send_fax(number, subject, recipient_name='', servername='', body_doc='C:\\Path\\To\\SomeFile.tiff')
    doc = FaxDocument()
    doc.Body = body_doc
    doc.Subject = subject
    doc.Recipients.Add(number, recipient_name)
    server = FaxServer()
    server.Connect(servername)
    doc.ConntectedSubmit(server)
    server.Disconnect()

Assuming you have a fax server running locally and fax modem setup already in the Windows Fax and Scan tool, servername='' assumes the local server.

And this works!

To make things a bit nicer and feel more like Python, we can hide some of the artifacts of the COM library by subclassing the generated Python classes.

from faxcomex import FaxServer, FaxDocument
class PyFaxServer(FaxServer):
    def __init__(self, servername=''):
        """get servername on object creation, so its not needed later"""
        super().__init__()
        self.__servername = servername
    def connect(self):
        """a python-naming-convention-compliant alias for `Connect`"""
        return self.Connect()
    def Connect(self):
        """override this so we can call connect without arguments"""
        return super().Connect(self.__servername)

    def _connection_manager(self):
        """manage connection and disconnection in a context manager"""
        try:
            yield self.connect()
        finally:
            self.Disconnect()

    def send(self, doc):
        """convenience method to connect to the server and send a document"""
        with self._connection_manager():
            doc.ConnectedSubmit(self)

class PyFaxDocument(FaxDocument):
    def __init__(self, *recipients, subject, body):
        super().__init__()
        for recipient_number, recipient_name in recipients:
            self.Recipients.Add(recipient_number, recipient_name)
        self.Subject = subject
        self.Body = body
    def submit(self, server):
        """Convenience method to submit document to a PyFaxServer object)"""
        server.send(self)

Now our send_fax function looks and feels a bit more like Python, even though it’s an MS DLL under the hood.

def send_fax(number, subject, body_doc, recipient_name='', servername=''):
    server = PyFaxServer(servername)
    recipient = (number, recipient_name)
    document = PyFaxDocument(recipient, subject=subject, body=body_doc)
    server.send(document)

So we’ve implemented a nice fax server interface without writing almost any code of our own. How cool!

That’s all I got for now.

Resources

Related