# Designing an Intermediate Representation That Makes pdfme and ReportLab Draw the Same Report

> Source: <https://dev.to/wintermaples/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the-same-report-5dej>
> Published: 2026-07-21 22:23:17+00:00

This article was written by AI and edited by a human.

Hi, I'm wintermaples.

I'm building denreport, an open-source tool that lets you design report layouts (such as invoices) in the browser and export them as code or templates for PDF libraries you already use.

If you have ever written report-generation code by hand, you know the drill: specify coordinates as numbers, render the PDF, check it with your own eyes, and repeat.

With denreport, that trial and error becomes drag-and-drop in the browser, and you build the layout while seeing the finished result.

The layout you create is exported as a template or code for a PDF library you already use, so you don't have to bring a new runtime into your production environment.

The layout itself is an open JSON format: you can manage it with Git, and there is no lock-in to any particular tool.

denreport currently has two export targets.

One is pdfme, a TypeScript library, for which it exports template JSON.

The other is ReportLab, a Python library, for which it exports the Python code that generates the PDF.

If you know pdfme, you may be thinking: "doesn't pdfme have its own designer?"

It does, and pdfme is a great library that lets you embed that designer right into your product.

What denreport aims to be is not a replacement, but **a design environment that is not tied to any single library**.

denreport itself has no PDF rendering engine; it keeps your layout in a common format and lets you choose the export target later.

The same layout can be exported to both pdfme and ReportLab, and if your team ever switches libraries, your layout assets come with you.

The promise at the center of denreport is this: **one layout built in the designer produces the same report no matter which target you export it to**.

pdfme is a declarative library that pours data into a template, while ReportLab is an imperative library that issues drawing commands to a canvas; the two are polar opposites in character.

Their coordinate systems differ, and so does the availability of features like dashed lines and justified text.

Still, if an invoice line shifted by one row depending on the export target, it would be a different document.

Reports are deliverables where "mostly the same" is not good enough, so "the same" became the starting point of the design.

This article is about how that "the same" is actually built.

denreport does not convert your layout directly into each library's format.

It first turns the layout into an **intermediate representation**, a common JSON format, and runs each target's export from there.

Intermediate representation (IR)is a common data format placed between a conversion source and its destinations.

It is the same idea as a compiler translating source code into a shared internal form before generating machine code for each CPU.

In denreport, the "layout JSON" plays this role.

```
Browser designer
      |
      v
Intermediate representation (layout JSON)
      |
      +--> pdfme export     -> template JSON
      +--> ReportLab export -> Python code
```

The reason for the extra step is to avoid leaving the interpretation of the layout to the libraries.

If decisions like where text wraps or where a table splits are delegated to each library's features, the subtle differences in those features become differences in output.

So denreport writes the layout rules into the IR specification in as much detail as possible.

For example, the spec fixes the following.

In other words, every decision that affects how the report looks is settled before anything reaches pdfme or ReportLab, and the spec leaves the libraries no room for interpretation.

Even with the rules fixed, the moment you ask an export target to "draw a table", the differences between table implementations come back.

Cell padding, where rules are drawn, and how a table splits across pages all differ slightly from library to library.

So denreport inserts one more step right before export.

The IR has high-level elements such as tables and page numbers, but before export, these are expanded into collections of simple shapes: text, lines, and rectangles.

A table is broken down into the outer rectangle, each individual rule, and the strings inside the cells.

A page number becomes a finalized string like "page 2 of 3".

```
Before expansion             After expansion
---------------              ---------------
Table          ------------> rectangle (outer frame)
                             lines (each individual rule)
                             text (cell strings)
Page number    ------------> text ("2 / 3", finalized)
```

After this expansion, all an export target needs to be capable of is "drawing text, lines, rectangles, ellipses, images, and barcodes at specified positions".

Any PDF library can handle drawing this simple.

Since the libraries' own table features are never used, their differences cannot affect the output.

And when a third export target is added in the future, it only has to meet this same minimal bar.

At this point, "what to draw where" is fully determined.

Even so, implementing the two exporters revealed bumps specific to each library.

For pdfme, the work was filling feature gaps.

pdfme templates have no way to specify dashed lines, so at export time denreport converts a dashed line into "many short solid segments".

Justified text does not exist in pdfme either, so the text is split into one element per line, with letter spacing specified per line to reproduce the effect.

For ReportLab, the work was coordinate translation.

ReportLab's origin is bottom-left with y pointing up, so the IR's top-left, y-down coordinates are converted using the page height.

Rotation direction is also inverted, so the sign of the angle is flipped.

In exchange, dashed lines are natively supported by ReportLab, so no conversion like pdfme's was needed.

Side by side, the two exporters look like this.

| pdfme | ReportLab | |
|---|---|---|
| Coordinates | top-left origin, mm, passed as-is | bottom-left origin, y-up, conversion needed |
| Rotation | as-is | sign flipped |
| Dashed lines | no option → converted to short solid segments | drawn natively |
| Justified text | split per line, letter spacing per line | letter spacing computed at draw time |

Interestingly, the "easy parts" and the "gaps to fill" were exact opposites between pdfme and ReportLab.

Had the IR not fixed the rules, these bumps would have reached users as "the output looks different depending on the export target".

Finally, the honest part: where the line is drawn.

denreport does not guarantee that outputs match pixel for pixel.

Fine details of glyph rendering differ between libraries, and things like underline position and thickness, or the exact bar dimensions of barcodes, follow each library's conventions.

What is guaranteed is the "geometry of the layout": positions, sizes, line wrapping, and pagination.

The spec states explicitly what is guaranteed and what is not.

Drawing the line in a spec is not enough for users to notice, so these differences are surfaced as warnings in the designer.

Every element and attribute is assigned one of three levels: fully supported, approximated, or unsupported, in a per-target support table.

When a layout being designed contains approximations, the designer shows what they are.

As of today, there are zero "unsupported" entries (nothing silently disappears); everything falls within "approximated".

To make two very different libraries draw the same report, denreport does three things.

The deeper internals of the exporters (what the generated Python code looks like, and how tests pin the two targets' PDF outputs to match) will be covered in separate articles.

denreport is under active development, preparing for release as MIT-licensed open source.

Stars and feedback are a huge encouragement for a solo developer.

I'd love your support.

Repository: [https://github.com/denreport/denreport](https://github.com/denreport/denreport)
