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

작성자

카테고리:

← 피드로
DEV Community · wintermaples · 2026-07-22 개발(SW)

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.

Collect the layout rules into an intermediate representation

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

Enter fullscreen mode Exit fullscreen mode

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.

  • Units and coordinates: every length is in mm, the origin is top-left, y points down. Only font sizes are in pt.
  • Vertical text position: the height each line of text sits at is determined by a formula computed from font metrics. As long as the same font is used, text lands at the same height regardless of the export target.
  • Wrapping and line-break rules: the spec decides at which character long text wraps, and how characters that must not start a line (such as Japanese punctuation) are handled.
  • Pagination: how many pages the document has, and at which row a long table splits across pages, are uniquely determined by the layout and the data.

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.

Never hand over a table as a “table”

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)

Enter fullscreen mode Exit fullscreen mode

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.

The bumps that remain anyway

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”.

Write down what is not guaranteed, too

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”.

Summary

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

  1. Fix the layout rules (units, text position, wrapping, pagination) in the IR specification, leaving no interpretation to the libraries
  2. Expand high-level elements such as tables into simple shapes before export, removing dependence on library-specific features
  3. State explicitly in the spec what rendering details are not guaranteed, and surface the differences to users as warnings

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

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다