One line of xhtml2pdf's dependency list is the whole architecture, and it is reportlab<5,>=4.0.4. The package translates parsed markup onto ReportLab's drawing model, so the fidelity ceiling of every document it produces is set by what that model can express rather than by what a browser would draw, and the version of the model it can express is decided by somebody else's release policy. Two calendars govern one feature from here on, with the lower of the two fixing the ceiling, which is a different commitment from rendering the markup as a browser renders it or from producing the document from one package.
Full disclosure. IronPDF comes from our team at Iron Software. What follows weighs what xhtml2pdf's pinned ReportLab ceiling and documented layout errors cost a build, and how IronPDF renders the same markup through a browser engine.
How Deep Does the ReportLab Pin Go?
Incoming markup is parsed with html5lib and mapped onto ReportLab's Platypus document model to produce the final file, which is the pipeline the project documents. Nothing in that path evaluates CSS the way a layout engine does, so what reaches the page is whatever Platypus can be asked to draw. Raising the ceiling therefore means waiting for ReportLab's model to gain a capability and then waiting for xhtml2pdf to translate onto it, which is two upstream projects deep for a change that a browser engine would already have.
One useful side effect comes with that pin. ReportLab's serious historical CVEs, including remote code execution and injection findings, are scoped to its 3.x series, and reportlab<5,>=4.0.4 sits outside that range, so none of those advisories applies to a current xhtml2pdf install. It also means the 5.x line, and the security review that release was built around, sits on the far side of a pin that has not moved. Watching two projects to know where one library stands is the maintenance shape being adopted here, where a single documented API surface puts that dependency question inside one package.
Why Does a Scanner Still Flag 0.2.17?
A single CVE sits against xhtml2pdf's own code. CVE-2024-25885 is a moderate-severity regular-expression denial-of-service issue in the getcolor function, affecting versions up to and including 0.2.16. The wrinkle is in the record rather than the code, because both the GitHub Advisory Database and the NVD list it with no fixed version recorded, which surfaces in a naive dependency scan as an open, unpatched issue. The project's own 0.2.17 release notes list a fix for the reDoS issue in getColor among the bug fixes, and Snyk's scan of 0.2.17 reports nothing outstanding, so a scan that stops at the advisory database's fixed-version field is reading a stale record rather than the state of the package. That distinction is worth carrying into the same review as document metadata and permissions, because somebody has to answer for it every audit.
Release cadence is slow and the commit record is steadier than it. Version 0.2.17 was published on 23 February 2025 and is still the current release eighteen months later, while the repository is neither archived nor disabled and took commits as recently as 30 August 2026. That is a community-run project still taking contributions between releases, and it is also a project whose next release date nobody can plan around.
The Narrow Case for a Pure-Python Install
Every required dependency is pure Python, covering html5lib, Pillow, pypdf, python-bidi, arabic-reshaper, pyHanko, pyhanko-certvalidator, svglib, and ReportLab itself, so the install needs nothing on the system path, meaning no Pango, no browser binary, no apt-get layer, and no root access. That is decisive inside a slim Docker image, a Lambda deployment package, or a CI runner where system packages are not available, and only an optional pycairo extra tied to one SVG backend pulls in a system graphics library at all. The document furniture is better than the category average too, with a working table-of-contents tag, numbered pages, barcode generation, right-to-left and bidirectional text through required rather than optional dependencies, and digital signatures through pyHanko, all under the Apache License 2.0 with the patent grant that MIT and BSD terms do not carry. Where a build cannot install anything outside pip and the documents are simple and tabular, that install is the one case for a translation layer over a rendering engine, though the constraint that forces it is rarer each year, and IronPDF answers the browser-authored half of the same problem from one package.
The integration surface for a basic conversion is one function.
from xhtml2pdf import pisa
def html_to_pdf(source_html: str, output_path: str) -> bool:
with open(output_path, "wb") as output_file:
result = pisa.CreatePDF(source_html, dest=output_file)
return not result.err
invoice_html = "<html><body><h1>Invoice #1042</h1><p>Total due: $412.00</p></body></html>"
html_to_pdf(invoice_html, "invoice.pdf")
That writes the invoice to disk and returns True when the conversion reports no errors. The pisa module name is a holdover from the project's original name before it was renamed and relicensed around 2010, rather than a sign that an older unrelated package has been installed by mistake. Page numbering and the table-of-contents tag come from xhtml2pdf's own tag set, where a browser engine takes page numbers and breaks from the stylesheet.
| Capability | xhtml2pdf 0.2.17
|
IronPDF for Python |
|---|---|---|
| Rendering path |
pisa.CreatePDF onto ReportLab's Platypus model |
Chromium through ChromePdfRenderer
|
| CSS coverage | Page, frame, and vendor properties, no flexbox or grid | Modern CSS as the browser implements it, flexbox and grid included |
| JavaScript on the page | No scripting stage in the pipeline | Runs before RenderHtmlAsPdf captures |
| System libraries to install | None for the core conversion path | Handled inside the packaged runtime |
| A table cell spanning a page break | Documented as an error | Paginated as the stylesheet asks |
| Floated or inline images | Rendered in their own paragraph | Positioned where the layout puts them |
| Right-to-left and bidirectional text | python-bidi and arabic-reshaper, required | Rendered through the browser text stack |
| Digital signatures | pyHanko, a separate dependency | Signing on the same PdfDocument
|
| Licence | Apache License 2.0 | Commercial, one tier |
| Python versions accepted | 3.8 to 3.12 | 3.7 and later |
Table 1. Conversion path and document output only, xhtml2pdf against IronPDF for Python, as each project documents itself.
The second, fifth, and sixth rows are where a design and its output stop matching, because flexbox, a table crossing a page, and a floated image are all ordinary in markup written for a browser. The rest is a question of what the build environment allows.
Where Does the Translation Layer Stop?
Documented CSS support here is page-shaped rather than layout-shaped. What the reference covers is page size and margins, frame position and dimensions, and vendor extensions such as -pdf-frame-content, -pdf-keep-with-next, and the -pdf-outline family, and nothing in it names flexbox, grid, position, float, box-shadow, background-image, or z-index. Markup written for a browser layout does not degrade politely against that list, so a card layout, a sticky header, or a grid dashboard comes out as whatever the flowable model can approximate, which is often nothing like the design it was built from.
No scripting stage appears anywhere in the xhtml2pdf pipeline and no JavaScript engine appears in its dependency tree, so a page that assembles its content client-side, whether through a charting library, a templating framework injecting DOM after load, or a table populated by a later call, has to be pre-rendered to static markup before conversion. That pre-render step is a service to write and operate, and it is the point where charts drawn in the browser stop being a template detail and become infrastructure.
Three limits are documented in the reference in those words. The main restriction, as the docs put it, is that table cells longer than one page lead to an error, tables cannot float left or right and cannot be inlined, and images always render in a separate paragraph because ReportLab does not support images inside paragraphs, so floating is not available. An invoice with a long line-item table or a report that wraps text around a figure meets a stated limit, not a bug that a workaround can route around, which makes it a design constraint on every template the pipeline will ever convert from existing markup.
What Chromium Renders That a Mapping Cannot
The template usually outgrows the translation layer one panel at a time. An invoice that started as a plain table picks up a summary panel built with flexbox, and the layout that made the pure-Python install possible stops keeping up.
from ironpdf import *
renderer = ChromePdfRenderer()
pdf = renderer.RenderHtmlAsPdf("""
<html>
<body style="font-family: sans-serif; display: flex; gap: 16px;">
<div style="flex: 1; background: #f4f4f4; padding: 12px;">Line items</div>
<div style="flex: 1; background: #eef; padding: 12px;">Totals</div>
</body>
</html>
""")
pdf.SaveAs("invoice.pdf")
Both panels come out side by side with their spacing and background colours intact, because the flexbox rules are evaluated by the same engine a browser uses rather than mapped onto a drawing model first. Once the document exists, assembling it with other files happens on the same object.
IronPDF renders that markup through a browser engine, so flexbox and grid arrive as designed, a long table breaks across pages instead of raising an error, and an image floats where the stylesheet puts it. The narrow case that stays with xhtml2pdf is a build that cannot install anything outside pip, and fewer teams are held to that every year. IronPDF has a free trial if you want to run the template that broke against a rendering engine before rewriting it to fit the property list.
What comes with xhtml2pdf either way is the line at the top of this piece. reportlab<5,>=4.0.4 sits in another project's dependency list, and it decides when the layout ceiling moves, which ReportLab release a security review is actually reviewing, and how long a template waits for a property that browsers shipped years ago. Reading that line first tells you more about the next three years of the pipeline than any feature table will.
Is your own template on the near or the far side of that ceiling? Tell us in the comments, especially if you have hit the long-table or floated-image constraint in production and found a route around it that held up.
xhtml2pdf and ReportLab are the property of their respective maintainers, and we have no affiliation with either project. The dependency, licence, and advisory details above are drawn from xhtml2pdf's own README, PyPI metadata, and release notes at the time of writing. If a detail has changed since, the comments are open.
Top comments (0)