{"slug": "designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the", "title": "Designing an Intermediate Representation That Makes pdfme and ReportLab Draw the Same Report", "summary": "Wintermaples is building denreport, an open-source tool that lets users design report layouts in the browser and export them as code or templates for PDF libraries like pdfme and ReportLab. The tool uses an intermediate representation (IR) to ensure the same layout produces identical output across different libraries, fixing decisions like text wrapping and table splitting before export.", "body_md": "This article was written by AI and edited by a human.\n\nHi, I'm wintermaples.\n\nI'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.\n\nIf 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.\n\nWith denreport, that trial and error becomes drag-and-drop in the browser, and you build the layout while seeing the finished result.\n\nThe 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.\n\nThe layout itself is an open JSON format: you can manage it with Git, and there is no lock-in to any particular tool.\n\ndenreport currently has two export targets.\n\nOne is pdfme, a TypeScript library, for which it exports template JSON.\n\nThe other is ReportLab, a Python library, for which it exports the Python code that generates the PDF.\n\nIf you know pdfme, you may be thinking: \"doesn't pdfme have its own designer?\"\n\nIt does, and pdfme is a great library that lets you embed that designer right into your product.\n\nWhat denreport aims to be is not a replacement, but **a design environment that is not tied to any single library**.\n\ndenreport itself has no PDF rendering engine; it keeps your layout in a common format and lets you choose the export target later.\n\nThe same layout can be exported to both pdfme and ReportLab, and if your team ever switches libraries, your layout assets come with you.\n\nThe 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**.\n\npdfme 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.\n\nTheir coordinate systems differ, and so does the availability of features like dashed lines and justified text.\n\nStill, if an invoice line shifted by one row depending on the export target, it would be a different document.\n\nReports are deliverables where \"mostly the same\" is not good enough, so \"the same\" became the starting point of the design.\n\nThis article is about how that \"the same\" is actually built.\n\ndenreport does not convert your layout directly into each library's format.\n\nIt first turns the layout into an **intermediate representation**, a common JSON format, and runs each target's export from there.\n\nIntermediate representation (IR)is a common data format placed between a conversion source and its destinations.\n\nIt is the same idea as a compiler translating source code into a shared internal form before generating machine code for each CPU.\n\nIn denreport, the \"layout JSON\" plays this role.\n\n```\nBrowser designer\n      |\n      v\nIntermediate representation (layout JSON)\n      |\n      +--> pdfme export     -> template JSON\n      +--> ReportLab export -> Python code\n```\n\nThe reason for the extra step is to avoid leaving the interpretation of the layout to the libraries.\n\nIf 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.\n\nSo denreport writes the layout rules into the IR specification in as much detail as possible.\n\nFor example, the spec fixes the following.\n\nIn 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.\n\nEven with the rules fixed, the moment you ask an export target to \"draw a table\", the differences between table implementations come back.\n\nCell padding, where rules are drawn, and how a table splits across pages all differ slightly from library to library.\n\nSo denreport inserts one more step right before export.\n\nThe 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.\n\nA table is broken down into the outer rectangle, each individual rule, and the strings inside the cells.\n\nA page number becomes a finalized string like \"page 2 of 3\".\n\n```\nBefore expansion             After expansion\n---------------              ---------------\nTable          ------------> rectangle (outer frame)\n                             lines (each individual rule)\n                             text (cell strings)\nPage number    ------------> text (\"2 / 3\", finalized)\n```\n\nAfter this expansion, all an export target needs to be capable of is \"drawing text, lines, rectangles, ellipses, images, and barcodes at specified positions\".\n\nAny PDF library can handle drawing this simple.\n\nSince the libraries' own table features are never used, their differences cannot affect the output.\n\nAnd when a third export target is added in the future, it only has to meet this same minimal bar.\n\nAt this point, \"what to draw where\" is fully determined.\n\nEven so, implementing the two exporters revealed bumps specific to each library.\n\nFor pdfme, the work was filling feature gaps.\n\npdfme templates have no way to specify dashed lines, so at export time denreport converts a dashed line into \"many short solid segments\".\n\nJustified 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.\n\nFor ReportLab, the work was coordinate translation.\n\nReportLab's origin is bottom-left with y pointing up, so the IR's top-left, y-down coordinates are converted using the page height.\n\nRotation direction is also inverted, so the sign of the angle is flipped.\n\nIn exchange, dashed lines are natively supported by ReportLab, so no conversion like pdfme's was needed.\n\nSide by side, the two exporters look like this.\n\n| pdfme | ReportLab | |\n|---|---|---|\n| Coordinates | top-left origin, mm, passed as-is | bottom-left origin, y-up, conversion needed |\n| Rotation | as-is | sign flipped |\n| Dashed lines | no option → converted to short solid segments | drawn natively |\n| Justified text | split per line, letter spacing per line | letter spacing computed at draw time |\n\nInterestingly, the \"easy parts\" and the \"gaps to fill\" were exact opposites between pdfme and ReportLab.\n\nHad the IR not fixed the rules, these bumps would have reached users as \"the output looks different depending on the export target\".\n\nFinally, the honest part: where the line is drawn.\n\ndenreport does not guarantee that outputs match pixel for pixel.\n\nFine 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.\n\nWhat is guaranteed is the \"geometry of the layout\": positions, sizes, line wrapping, and pagination.\n\nThe spec states explicitly what is guaranteed and what is not.\n\nDrawing the line in a spec is not enough for users to notice, so these differences are surfaced as warnings in the designer.\n\nEvery element and attribute is assigned one of three levels: fully supported, approximated, or unsupported, in a per-target support table.\n\nWhen a layout being designed contains approximations, the designer shows what they are.\n\nAs of today, there are zero \"unsupported\" entries (nothing silently disappears); everything falls within \"approximated\".\n\nTo make two very different libraries draw the same report, denreport does three things.\n\nThe 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.\n\ndenreport is under active development, preparing for release as MIT-licensed open source.\n\nStars and feedback are a huge encouragement for a solo developer.\n\nI'd love your support.\n\nRepository: [https://github.com/denreport/denreport](https://github.com/denreport/denreport)", "url": "https://wpnews.pro/news/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the", "canonical_source": "https://dev.to/wintermaples/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the-same-report-5dej", "published_at": "2026-07-21 22:23:17+00:00", "updated_at": "2026-07-21 22:59:24.821902+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["denreport", "pdfme", "ReportLab", "wintermaples"], "alternates": {"html": "https://wpnews.pro/news/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the", "markdown": "https://wpnews.pro/news/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the.md", "text": "https://wpnews.pro/news/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the.txt", "jsonld": "https://wpnews.pro/news/designing-an-intermediate-representation-that-makes-pdfme-and-reportlab-draw-the.jsonld"}}