Refactoring PyPDF2’s Transformation Interface | by Martin Thoma | Jun, 2022

To create a system that’s simpler to keep up

Picture from Wikipedia Commons (Sears Sports activities Middle, public area)

I like writing good software program. Software program that’s dependable and intuitively comprehensible by customers in addition to simple to keep up. It’s tremendous onerous to be taught and educate what good software program appears to be like like, however I hope to plant some concepts with this text.

After studying this text, you should have discovered two points of excellent software program in addition to a design sample.

PyPDF2 is a free and open supply pure-python PDF library able to splitting, merging, cropping, and reworking the pages of PDF recordsdata. It could possibly additionally add customized knowledge, viewing choices, and passwords to PDF recordsdata. PyPDF2 can retrieve textual content and metadata from PDFs as properly.

It was actively developed from 2011 to 2016. It really works for lots of use instances and thus the Python group nonetheless makes use of the library rather a lot — though it didn’t obtain any replace till 2022. In April 2022, I grew to become the maintainer of PyPDF2.

PyPDF2 is a fork of pyPdf — a means older mission. The builders of PyPDF2 all the time wished to maintain backward compatibility. I’m not feeling that want. If I feel the general mission advantages from breaking compatibility, I’ll do it.

An interface is a boundary to a system. It permits customers to work together with it. These customers is perhaps builders themselves. Customers shouldn’t have the necessity to know concerning the internal mechanics of a system.

Automobile mechanics might be drivers — however you shouldn’t must be a mechanic to drive a automobile. The general public interface is the steering wheel, the fuel pedal, the brakes, and some extra issues. The non-public interfaces are the inner CAN bus used for digital elements within the automobile and the requirements which outline which gears are used.

The consumer has the expectation that interfaces keep secure — or that there’s a minimum of a transparent announcement when they’re modified.

This implies you would possibly have to help a sub-optimal resolution you made years in the past. In some instances, a few years — take into consideration Python 2 or the truth that the US nonetheless makes use of imperial models.

Maintainers need small interfaces: The larger a public interface, the extra the developer must help. It’s extra susceptible to errors and inconsistencies.

But additionally customers revenue from small interfaces: There may be much less to find and hopefully precisely one approach to do what they need. Eradicating duplication makes code within the wild (or by coworkers) that makes use of the identical library extra constant.

It’s onerous for any developer to say “no” to a function request, particularly when it’s simple to satisfy. We need to make folks joyful. Simply including a parameter right here or a brand new comfort perform/class/technique there doesn’t do any hurt, proper?

That is the way you get bloated interfaces. I’ve briefly identified why you need small interfaces. If software program makes use of well-defined interfaces, you possibly can guess perform names and the names/order of parameters. You possibly can see the sample getting used to design it.

Take into consideration transformations you would possibly need to do to PDF pages. Rotation, shifting, scaling, cropping, overlay one PDF onto one other PDF. There is perhaps much more you possibly can consider, however these 5 are those we concentrate on for the second.

The PyPDF2 PageObject class has the next strategies:

Let’s test what we don’t like about these eight strategies:

  1. Not PEP-8 compliant: Not following the anticipated naming scheme is the very first thing that pops into my thoughts. In all probability the one I care least about, however the obvious one.
  2. Offering a couple of means: mergeRotatedTranslatedPage can rotate and translate — why do we want mergeRotatedPage, then? mergeRotatedScaledTranslatedPage appears to have the ability to do all transformations. Do we want any others?
  3. Uncertainty concerning the outcomes: Assume you need to do two operations. (a) You need to rotate a picture by 90° and (b) transfer it 10 cm to the fitting. Doing first (a) after which (b) is totally different than the opposite means round, assuming that the rotation middle is relative to the canvas.

I may use mergeRotatedScaledTranslatedPage solely and deprecate the remainder.

This has an enormous disadvantage: the order of operations issues!

The order of operations issues: You probably have the middle of the coordinate system on the pink dot and also you do (1) a shift of the determine on the web page to the fitting and (2) a rotation by 180°, you’ll get totally different outcomes. Picture by Martin Thoma.

The talked about strategies have one fairly apparent shortcoming: If you wish to simply do one of many operations with out merging with one other web page, you can’t do it. So it is sensible to have three strategies that function instantly on the web page itself and one which does merging:

class PageObject:
def merge_page(self, page2, increase=True): ...
def scale(self, scale, increase=True): ...
def rotate(self, rotation, increase=True): ...
def translate(self, tx, ty, increase=True): ...

Now you possibly can independently apply operations and merge, in any order you need.

In the event you merge two paperwork with many pages, you would possibly do the identical operation over and over. All of these three operations are represented by matrices. Executing them one after one other is a matrix multiplication. So when you’ve got 100 pages and also you do a rotation, a scaling operation, and a translation it will do 100 x 3 matrix multiplications or 300 matrix multiplications.

Whenever you first characterize the mixed operation as one matrix, you want to do two matrix multiplications. After that, you’ve got 100 matrix multiplications. So, 102 matrix multiplications in whole.

One other property of concept two is that you just can’t have a fluid interface: These operations ought to occur in place, as copying a web page is perhaps a relatively heavy operation. So the web page.scale operation ought to return None . Meaning you can’t do web page.scale(2).rotate(180) .

To eliminate this shortcoming, we add a metamorphosis object:

class Transformation:
def scale(self, sx, sy) -> Transformation: ...
def rotate(self, diploma) -> Transformation: ...
def translate(self, tx, ty) -> Transformation: ...
class PageObject:
def merge_page(self, page2, increase=True): ...
def remodel(self, transformation): ...

That is the Builder sample: The Transformation class encapsulates the transformation a consumer needs to use and helps them to create that matrix. The transformation matrix is what the PDF format and PyPDF2 really need, however that’s hidden from the PyPDF2 customers. The customers simply use the Transformation builder class:

transformation = Transformation().scale(2).rotate(180)

Because the transformation class holds little or no knowledge, we are able to make it immutable and all the time return a duplicate. That makes a fluent interface doable. Fashionable IDEs with auto-complete can now inform the consumer which operations they’ll do. Because the PageObject has fewer strategies, it’s simpler to check and the consumer has a better time discovering what they want.

You’ve seen how the builder sample might be utilized to allow a fluent interface and scale back the core objects’ public strategies from eight to 2. On the identical time, the consumer grew to become extra versatile and testing grew to become simpler.

You’ve observed that maintainability and customers’ wants generally go hand in hand. Small and well-defined interfaces simply make everybody’s lives simpler.

More Posts