Documentation page: PD4ML for Python. This page is also available as Markdown at pd4ml-python.md.

PD4ML for Python

pd4ml brings PD4ML to Python: it converts HTML and CSS into PDF, PDF/A, PDF/UA, ZUGFeRD/Factur-X invoices, DOCX, RTF and page images, and gives Python applications the complete public PD4ML API.

PD4ML is a Java library. The module runs it in a Java virtual machine inside the Python process (through JPype), so every PD4ML class and method can be called directly, with no server or subprocess in between, and adds Pythonic I/O on top: paths, bytes and file objects instead of Java streams.

The PD4ML manual, which this guide refers to throughout, is at https://pd4ml.com/docs/.

Contents


1. Prerequisites

Requirement Notes
Python 3.9 or later CPython, 64-bit. Tested up to Python 3.14.
Java 8 or later A JDK or JRE of the same bitness as Python; an LTS release (17, 21) is recommended. The module finds it through JAVA_HOME (or the system's default Java); set PD4ML_JVM to point at a specific jvm.dll / libjvm.so.
JPype1 Installed automatically with the module.
PD4ML Downloaded automatically on first use, or ahead of time with pd4ml install (see below); or any PD4ML build you point the module at.
Network access Only for downloading PD4ML. Conversions need none, unless the HTML references remote resources.
A PD4ML license Optional for evaluation: without one, PD4ML runs in evaluation mode and watermarks its output. See Licensing and 5.18.

Check Java with java -version. On a server without Java, install a JRE/JDK package (for example apt install openjdk-21-jre-headless, or a Temurin or Microsoft Build of OpenJDK installer on Windows) and set JAVA_HOME to its directory.

2. Installation

2.1 Install the module

From PyPI:

pip install pd4ml

This installs the module, its dependency JPype, and the pd4ml command. For development, from a checkout of the source: pip install -e ".[test]".

2.2 Install PD4ML

The package contains the Python module only. The PD4ML library itself comes from the PD4ML Maven repository (https://pd4ml.com/maven2), in one of two ways.

Automatically, on first use. If no PD4ML build is installed or configured (2.3), the first use of PD4ML in a process downloads the most recent PD4ML 4.x release before starting Java. A notice on standard error reports it. So after pip install pd4ml, this is all it takes:

import pd4ml

pdf = pd4ml.html_to_pdf(b"<h1>Hello</h1>")      # the first call downloads PD4ML (once)

Ahead of time, with the pd4ml command. This is better for servers, CI and container images, where a download in the middle of a request is unwelcome:

pd4ml install                 # also: python -m pd4ml install

Either way, the module does the following:

  • downloads the most recent PD4ML 4.x release;
  • downloads the Bouncy Castle jars PD4ML needs for encryption and signing, from Maven Central;
  • checks every file against its published SHA-1 checksum;
  • stores them in a per-user cache directory: %LOCALAPPDATA%\pd4ml-python on Windows, ~/.cache/pd4ml-python elsewhere, or PD4ML_HOME.

The module then uses that build. Later runs do not check for newer PD4ML releases: run pd4ml install again to update to the most recent one. Updating the Python package (pip install -U pd4ml) does not change the PD4ML build.

Option
(none) the most recent 4.x release, or an update to it
--snapshots the most recent 4.x build, including development snapshots
--version 4.0.23fx2 an exact version
--jakarta the -jakarta variant (for Jakarta EE / Servlet 6 web applications)
--list list the available versions
--home DIR install into DIR (default: PD4ML_HOME, else the cache directory)
--force download again
--no-bouncycastle skip the Bouncy Castle jars (encryption and signing will not work)

The same is available from Python, which suits a script's initialisation:

import pd4ml

pd4ml.install()                 # downloads only when that build is not there yet
print(pd4ml.installed_version(), pd4ml.installed_classpath())

To check the installation, run pd4ml version.

Turning the automatic download off. Set PD4ML_AUTO_INSTALL=0, for example on servers that must not reach the internet, or where the build is provisioned separately. The module then raises pd4ml.ClasspathError until a build is installed or configured. The automatic download never runs when a build is configured through 2.3.

Services and containers: the account the application runs as must see the installed build. Install it as that account, or install into a shared directory and set PD4ML_HOME for the application too. In a Dockerfile:

RUN pip install pd4ml && PD4ML_HOME=/opt/pd4ml pd4ml install
ENV PD4ML_HOME=/opt/pd4ml PD4ML_AUTO_INSTALL=0

2.3 Other ways to provide PD4ML

The module looks for PD4ML in this order:

  1. pd4ml.start(classpath=[...]), called before the first conversion;
  2. the PD4ML_CLASSPATH environment variable — jars and class directories separated by ; (Windows) or : (elsewhere), dir/* for all jars of a directory;
  3. the PD4ML_CLASSPATH_FILE environment variable — a file with one entry per line;
  4. the build pd4ml install (or the first use) downloaded.

Typical reasons to use them:

  • Offline / air-gapped servers: run pd4ml install --home ./pd4ml-libs on a machine with network access, copy the directory, and set PD4ML_HOME (or PD4ML_CLASSPATH) and PD4ML_AUTO_INSTALL=0 on the target.
  • A licensed or specific jar: PD4ML_CLASSPATH=/opt/pd4ml/pd4ml.jar:/opt/pd4ml/lib/*.
  • A PD4ML development build (the v42 layout engine, see 4.3): python tools/pd4ml_classpath.py <pd4ml-maven-project> writes pd4ml.classpath with the build's classes and its Maven dependencies; set PD4ML_CLASSPATH_FILE to it.

3. Quick start

import pd4ml

pd = pd4ml.PD4ML()
pd.setPageSize(pd4ml.PageSize.A4)
pd.read_html_string("<h1>Hello, PDF</h1><p>Generated with <b>PD4ML</b>.</p>")
pd.writePDF("hello.pdf")

Or in one call:

import pd4ml

pdf_bytes = pd4ml.html_to_pdf(b"<h1>Invoice 42</h1>")
assert pdf_bytes.startswith(b"%PDF")

4. How it works

4.1 The two-phase model

Every conversion is read once, written any number of times: readHTML parses and lays out the document; each writePDF, writeDOCX, writeRTF or renderAsImages then produces one output from that same layout, without parsing again. Settings (page size, headers, fonts...) go before readHTML. See the two-phase model.

import pd4ml

pd = pd4ml.PD4ML()
pd.read_html_string("<h1>Quarterly report</h1>")
pd.writePDF("report.pdf")
pd.writeDOCX("report.docx")
thumbnails = pd.render_png()          # list of PNG images, one per page

4.2 The Java virtual machine

  • The JVM starts on the first use of PD4ML and lives as long as the Python process. A process can start it only once: choose its options (memory, system properties) before the first conversion, with pd4ml.start(...) or PD4ML_JVM_OPTIONS.
  • It runs headless (java.awt.headless=true), so it works on servers without a display.
  • Startup takes roughly a second; afterwards a conversion of an ordinary document takes milliseconds. Long-running processes (web servers, workers) pay the startup once.
import pd4ml

pd4ml.start(jvm_options=["-Xmx2g", "-Dfile.encoding=UTF-8"])   # before anything else

4.3 Layout engines

PD4ML 4.2 introduces a second HTML layout engine, v42, written for close agreement with current browsers (flexbox, grid, modern CSS). pd4ml.PD4ML() uses it by default:

import pd4ml

pd = pd4ml.PD4ML()                  # v42 where available
legacy = pd4ml.PD4ML(engine="v40")  # the previous engine
print(pd.engine, legacy.engine)

Builds before 4.2 — including the current public release, 4.1.x — have one engine only. With such a build the module uses it, warns once, and pd4ml.has_layout_engines() returns False; asking for an engine explicitly raises an error.

4.4 Python and Java values

  • Java methods keep their Java names (setPageSize, readHTML...) and accept Python values: str, int, float, bool, bytes for byte[], dict for Map, None for null.
  • Java strings come back as str and null as None; numbers may come back as Java boxed numbers — wrap them in int(...) / float(...) when you need Python ones.
  • Java constants, enums and classes are module attributes: pd4ml.Constants.AllowPrint, pd4ml.PdfSpec.PDFA_3B, pd4ml.PageSize.LETTER, pd4ml.Units.MM.
  • Any other Java class is reachable with pd4ml.java_class("java.util.Locale").
  • The PD4ML object wraps the Java one, available as pd.java.

4.5 Exceptions

Java exceptions are raised as Python exceptions and can be caught by their Java class:

import pd4ml

IOException = pd4ml.java_class("java.io.IOException")
try:
    pd = pd4ml.PD4ML()
    pd.readHTML("file:///no/such/document.html")
    pd.writePDF()
except IOException as e:
    print("conversion failed:", e)

The PD4ML exception classes are exported directly: pd4ml.InvokeException, pd4ml.InvalidPasswordException, pd4ml.ForcedTerminationException, pd4ml.ConflictingParametersException, pd4ml.PdfSpecViolationException, pd4ml.PdfSpecUnsupportedException.


5. Use cases

The snippets below are complete and runnable; files they read (report.html...) are assumed to exist in the working directory.

5.1 HTML string to PDF bytes

The common case for web applications and services: generate HTML (with Jinja2, Django templates, ...), convert it in memory.

import pd4ml

html = "<html><body><h1>Order 1001</h1><p>Thank you for your order.</p></body></html>"

pd = pd4ml.PD4ML()
pd.read_html_string(html)
pdf = pd.writePDF()            # bytes, ready to send or store

writePDF, writeDOCX and writeRTF return bytes without a target, write to a path given as str or pathlib.Path, or write into any binary file object (io.BytesIO, an open file, a web framework's response stream).

5.2 Files and URLs

import pathlib
import pd4ml

pd = pd4ml.PD4ML()
pd.readHTML(pathlib.Path("report.html"))     # also: "report.html", "https://example.com/report"
pd.writePDF(pathlib.Path("report.pdf"))

Relative links (<img src="logo.png">, <link href="style.css">) resolve against the document's own location when it is read from a path or URL. When the HTML comes from a string, bytes or a stream, pass base_url — otherwise images and stylesheets silently go missing (see reading the source):

import pd4ml

html = '<link rel="stylesheet" href="style.css"><p class="note">Styled</p>'
pd = pd4ml.PD4ML()
pd.read_html_string(html, base_url=".")      # a directory, path or URL
pd.writePDF("styled.pdf")

For a document in a known non-UTF-8 encoding: pd.readHTML(data, base_url, "windows-1251").

5.3 Page size, orientation, margins and scaling

import pd4ml

pd = pd4ml.PD4ML()
pd.setPageSize(pd4ml.PageSize.LETTER.rotate())                        # landscape
pd.setPageMargins(pd4ml.PageMargins(15, 20, 15, 20, pd4ml.Units.MM))   # left, top, right, bottom
pd.setHtmlWidth(1000)                    # the virtual browser width the page is laid out in
pd.read_html_string("<table width='100%'><tr><td>wide content</td></tr></table>")
pd.writePDF("landscape.pdf")

The layout is scaled to the page: scale = printable width / html width. A smaller HTML width makes everything larger. adjustHtmlWidth(True) uses the document's own natural width; fitPageVertically(pd4ml.Constants.CENTER_ALIGN) shrinks the whole document onto a single page. Page size and margins accept a page scope ("1", "2+", "odd"...) to differ between pages. More in page layout.

Custom sizes: pd4ml.PageSize(100, 150, pd4ml.Units.MM); named sizes: pd4ml.PageSize.getInstance("A5").

5.4 Headers, footers and page numbers

Headers and footers are HTML fragments; $[page], $[total] and $[title] are filled in per page, and setDynamicData adds placeholders of your own:

import pd4ml

pd = pd4ml.PD4ML()
pd.setDynamicData({"customer": "Acme Corp.", "invoice": "INV-2026-0031"})
pd.setPageHeader("<div style='font-size:9pt'><b>$[title]</b></div>", 30, "1+")
pd.setPageFooter(
    "<div style='font-size:8pt; text-align:right'>"
    "Invoice $[invoice] &mdash; $[customer] &mdash; page $[page] of $[total]</div>",
    24, "1+")
pd.read_html_string("<title>Invoice</title><h1>Invoice</h1>" + "<p>Line item</p>" * 80)
pd.writePDF("invoice.pdf")

The second argument is the area height in pixels, the third the page scope: "1" (first page), "2+" (all but the first), "odd", "even", "3-7"... Headers and footers can also be written inline in the HTML with PD4ML's extension tags. See headers, footers, watermarks and backgrounds and the usage examples.

5.5 Watermarks and page backgrounds

import pd4ml

pd = pd4ml.PD4ML()
pd.setWatermark("<span style='font-size:72pt; color:#c00'>DRAFT</span>",
                120, 350,       # left, top (px)
                0.15,           # opacity
                45,             # rotation, degrees
                1.0,            # scale
                True, True,     # visible on screen, in print
                "1+")           # pages
pd.setPageBackground("<div style='background:#f7f7f2; height:100%'></div>", "1+")
pd.read_html_string("<h1>Proposal</h1>")
pd.writePDF("draft.pdf")

5.6 Fonts

PD4ML covers Unicode broadly with its built-in fonts. For a specific typeface (a corporate font, CJK coverage), give it a font directory:

import pd4ml

pd = pd4ml.PD4ML()
pd.useTTF("/opt/fonts", True)       # a directory of TTF/OTF fonts (indexed: see below)
pd.embedTTFs(True, False)           # embed only the glyphs used
pd.applyKerning(True)
pd.enableLigatures(True)

Index a font directory once ahead of time — PD4ML then reads the index (pd4fonts.properties) instead of scanning all fonts on every conversion (font indexing):

java -jar <pd4ml jar> -configure.fonts /opt/fonts

(python -c "import pd4ml; print(pd4ml.installed_jar())" prints the jar's path.) CSS @font-face web fonts in the document work as well.

import pd4ml

pd = pd4ml.PD4ML()
pd.generateBookmarksFromHeadings(True)     # one PDF bookmark per <h1>..<h6>
pd.enableHyperlinks(True)                  # <a href> become clickable links (the default)
pd.read_html_string("<h1>Introduction</h1><p>...</p><h1 style='page-break-before:always'>Details</h1>")
pd.writePDF("bookmarked.pdf")

A table of contents with page numbers can be generated from the headings with PD4ML's <pd4ml:toc> tag: see the usage examples. Explicit page breaks: CSS page-break-before / break-before, or <pd4ml:page.break>.

5.8 PDF/A and PDF/UA

import pd4ml

pd = pd4ml.PD4ML()
pd.read_html_string("<html lang='en'><title>Annual report</title><h1>Annual report</h1>")
pd.writePDF("archive.pdf", pd4ml.PdfSpec.PDFA_3B)                       # archival
pd.writePDF("accessible.pdf", pd4ml.PdfSpec.PDFUA_1)                    # accessible
pd.writePDF("both.pdf", pd4ml.PdfSpec.PDF_1_7.combine(pd4ml.PdfSpec.PDFA_2A))

status = pd.getLastRenderInfo(pd4ml.Constants.PD4ML_PDFA_STATUS)        # after writing

The spec can also be given as a keyword string ("pdf/a-3b"; pd4ml.PdfSpec.listKeywords() lists them); unknown keywords raise ValueError. PDF/A embeds all fonts; PDF/UA needs well-structured HTML (real headings, alt texts, lang, table headers) — combine with pd.setErrorPolicy(pd4ml.ErrorPolicy.PEDANTIC) to be told when the source does not satisfy the standard. Details: PDF/A, PDF/UA and e-invoicing.

5.9 ZUGFeRD / Factur-X e-invoices

PdfSpec.ZUGFeRD / PdfSpec.FacturX select the PDF/A-3 container the standards require; the invoice XML is attached from the HTML with PD4ML's attachment tag. The complete recipe, with the required attachment names, is on the ZUGFeRD and Factur-X page.

import pd4ml

pd = pd4ml.PD4ML()
pd.readHTML("invoice.html")        # contains the <pd4ml:attachment> of factur-x.xml
pd.writePDF("invoice.pdf", pd4ml.PdfSpec.FacturX)

5.10 Passwords and permissions

import pd4ml

C = pd4ml.Constants
pd = pd4ml.PD4ML()
pd.setPermissions("owner-secret", C.AllowPrint | C.AllowCopy, C.AES_256BIT)
pd.read_html_string("<p>Confidential</p>")
pd.writePDF("protected.pdf")

Combine the Allow* flags with |, or start from C.DefaultPermissions and remove flags with ^. Needs the Bouncy Castle jars (installed by pd4ml install). See permissions and encryption.

5.11 Word (DOCX, RTF) and images

import pd4ml

pd = pd4ml.PD4ML()
pd.read_html_string("<h1>Letter</h1><p>Dear customer, ...</p>")
pd.writeDOCX("letter.docx")
pd.writeRTF("letter.rtf")
pages = pd.render_png()                                  # PNG bytes per page
tiffs = pd.renderAsImages(pd4ml.Constants.TIFF)          # one TIFF per page
pd.renderAsImages(".", "page", pd4ml.Constants.PNG24)    # page files in a directory

Image formats: PNG8, PNG24, TIFF, TIFF_COMPRESSED (Constants); the multi-page TIFF formats are not supported by the v42 engine yet — use engine="v40" for them.

With the v42 engine, DOCX/RTF output can be made editable — text boxes and form fields instead of pictures for positioned content — with the pd4ml.word.editable parameter: pd.setParam(pd4ml.Constants.PD4ML_WORD_EDITABLE, "true").

5.12 Interactive forms and multi-column layout

import pd4ml

pd = pd4ml.PD4ML()
pd.generateForms(True, None)          # <input>/<select>/<textarea> become PDF form fields
pd.read_html_string("<form>Name: <input name='name' size='30'></form>")
pd.writePDF("form.pdf")

news = pd4ml.PD4ML()
news.generateMulticolumn(2, 20, True) # two columns, 20 mm apart
news.read_html_string("<h1>Newsletter</h1>" + "<p>Lorem ipsum dolor sit amet.</p>" * 200)
news.writePDF("newsletter.pdf")

See forms and multi-column pages (PD4ML 4.0.24 / 4.1.2 and later).

5.13 Adding CSS and HTML programmatically

import pd4ml

pd = pd4ml.PD4ML()
pd.addStyle("body { font-family: sans-serif } h1 { color: #036 }", True)   # True: after the document's styles
pd.injectHtml("<div style='font-size:8pt'>Generated by the reporting service</div>", False)  # before </body>
pd.read_html_string("<h1>Report</h1>")
pd.writePDF("report-styled.pdf")

addStyle also takes the URL of a stylesheet (pd.addStyle(pd4ml.to_url("print.css"), True)). See injecting HTML and adding styles.

5.14 Progress, diagnostics and metadata

import pd4ml

def progress(message_id, percent, note, msec):
    print(f"{percent:3d}% {note or ''} ({msec} ms)")

pd = pd4ml.PD4ML(engine="v40")
pd.monitorProgressWith(progress)              # a plain Python function
pd.setErrorPolicy(pd4ml.ErrorPolicy.NORMAL)   # NONE, RELAXED, NORMAL, PEDANTIC, STRICT
pd.setLogLevel(1)                             # 0 (silent) .. 5
pd.setAuthorName("Reporting service")
pd.setDocumentTitle("Monthly statement")
pd.addMetadata("StatementId", "2026-10", False)
pd.read_html_string("<h1>Statement</h1>")
pd.writePDF("statement.pdf")
pages = int(pd.getLastRenderInfo(pd4ml.Constants.PD4ML_TOTAL_PAGES))

The v42 engine currently reports fewer progress stages than v40. More in extensibility and diagnostics.

5.15 Working with existing PDFs

import pd4ml

cover = pd4ml.html_to_pdf(b"<h1 style='margin-top:300px; text-align:center'>Annual Report</h1>")
body = pd4ml.html_to_pdf(b"<h2>Chapter 1</h2><p>...</p>")

doc = pd4ml.open_pdf(cover)               # path, URL, bytes or file object (+ password)
doc.append(pd4ml.open_pdf(body))          # pages of another document after these
doc.setTitle("Annual Report")
pd4ml.write_pdf(doc, "annual-report.pdf")

stamp = pd4ml.html_to_pdf(b"<p style='color:#c00; font-size:40pt'>COPY</p>")
original = pd4ml.open_pdf("annual-report.pdf")
original.overlay(pd4ml.open_pdf(stamp), 128)   # draw over every page, half opaque (0..255)
pd4ml.write_pdf(original, "annual-report-copy.pdf", password="secret")

print(pd4ml.open_pdf("annual-report.pdf").getNumberOfPages())

underlay draws another document under the pages (a letterhead), pd4ml.merge_pdfs(a, b) appends two documents in one call, deletePage, getPageSize, getMetadata, dumpOutlines inspect and edit a document. PD4ML's PDF toolkit (signing, optimisation, XFDF) is described in the PDF post-processing toolkit and reachable through pd4ml.java_class(...).

5.16 Web applications

A converter per request; the JVM is shared by all of them.

# Flask
from flask import Flask, Response, render_template
import pd4ml

app = Flask(__name__)

@app.route("/invoice/<int:number>.pdf")
def invoice(number):
    pd = pd4ml.PD4ML()
    pd.read_html_string(render_template("invoice.html", number=number), base_url=app.static_folder)
    return Response(pd.writePDF(), mimetype="application/pdf",
                    headers={"Content-Disposition": f"inline; filename=invoice-{number}.pdf"})
# FastAPI
from fastapi import FastAPI
from fastapi.responses import Response
import pd4ml

app = FastAPI()

@app.get("/report.pdf")
def report():                       # a plain def: FastAPI runs it in a worker thread
    pdf = pd4ml.html_to_pdf(b"<h1>Report</h1>")
    return Response(pdf, media_type="application/pdf")

With Django, return HttpResponse(pd.writePDF(), content_type="application/pdf") from the view. Under Gunicorn/uWSGI each worker process starts its own JVM; --preload does not share it — start PD4ML after the fork (in the worker), not in the master.

5.17 Batch conversion

import pathlib
from concurrent.futures import ThreadPoolExecutor
import pd4ml

def convert(src: pathlib.Path) -> pathlib.Path:
    target = src.with_suffix(".pdf")
    pd = pd4ml.PD4ML()                 # one converter per document and thread
    pd.readHTML(src)
    pd.writePDF(target)
    return target

sources = sorted(pathlib.Path(".").glob("*.html"))
with ThreadPoolExecutor(max_workers=4) as pool:
    for done in pool.map(convert, sources):
        print("written", done)

5.18 Licensing

import pd4ml

pd = pd4ml.PD4ML("YOUR-SERIAL-NUMBER")         # or a license file URL; or pd4ml.lic in the working dir / classpath
if pd.isDemoMode():
    print("unlicensed: output is watermarked")
print("PD4ML", pd4ml.version(), "pro" if pd.isPro() else "")

Without a license PD4ML is fully functional but marks its output. See licensing.


6. Tips and hints

  • Reuse the process, not the converter. Create a new PD4ML() for every document (they are cheap); keep the Python process — and with it the JVM — alive between conversions.
  • One converter per thread. A PD4ML object is not thread-safe; separate objects can convert in parallel threads.
  • Set base_url for HTML from strings. Missing images and unstyled output are almost always unresolved relative links (details).
  • Settings before readHTML. Page size, headers, fonts and styles apply to the layout readHTML makes; writes afterwards only choose the output format.
  • Write several formats from one read. PDF, DOCX and page images from a single readHTML cost one layout.
  • Print CSS works. @page { size: A4 landscape; margin: 15mm }, @media print, and page-break-* / break-* are honoured — often simpler than API calls.
  • Control the scale with the HTML width. If text is too small or a layout wraps differently than in the browser, set setHtmlWidth to the width the page was designed for.
  • Index fonts once with -configure.fonts instead of pointing useTTF at a large unindexed directory on every conversion.
  • Restrict what documents can load when converting untrusted HTML: see Constants.PD4ML_ALLOWED_RESOURCE_LOCATION and PD4ML_DISABLE_EXTERNAL_ATTACHMENTS (via setParam).
  • Memory: very large documents need JVM heap — pd4ml.start(jvm_options=["-Xmx4g"]) or PD4ML_JVM_OPTIONS=-Xmx4g.
  • IDE support: the module ships type stubs for the whole API, so completion and type checking work for the Java methods too.
  • Choose the build for production: pin it with pd4ml install --version X (or a PD4ML_CLASSPATH to a jar you keep) rather than following the latest release.

7. Troubleshooting

Symptom Cause and remedy
ClasspathError: PD4ML not found No build configured and the automatic download is off (PD4ML_AUTO_INSTALL=0) or failed: run pd4ml install (the error tells why the download failed; proxies are taken from HTTPS_PROXY), or set PD4ML_CLASSPATH.
A pause and a "downloading" notice on the first conversion The automatic first-use download (2.2). Run pd4ml install at deployment instead.
JVMNotFoundException / "No JVM shared library" Java is not installed or not found: install Java and set JAVA_HOME, or set PD4ML_JVM to jvm.dll / libjvm.so.
OSError: [WinError 193] or similar on JVM start Python and Java differ in bitness (32/64-bit): use a 64-bit JDK with 64-bit Python.
"JVM cannot be restarted" The JVM was shut down; a process can run it only once. Start a new process.
JVM options have no effect They were given after the JVM started: call pd4ml.start(...) before the first conversion.
Images or styles missing Relative links without base_url, or resources the server cannot reach (proxies, authentication): see setCookie, setHttpOptions.
Output is watermarked No license installed (licensing).
Squares instead of characters The fonts lack those glyphs: provide fonts with useTTF (fonts).
NoClassDefFoundError: org/bouncycastle/... with setPermissions The Bouncy Castle jars are missing: pd4ml install (without --no-bouncycastle), or add them to the classpath.
RuntimeWarning: ... has no v42 layout engine The PD4ML build predates 4.2: the conversion uses its own engine. Use a 4.2 build for v42.
A conversion runs too long pd.terminate() from another thread aborts it (ForcedTerminationException).

8. API reference

Module functions

Function
PD4ML(*args, engine="v42", html_width=None) a converter (see below)
html_to_pdf(source, target=None, *, base_url, page_size, page_margins, spec, engine, html_width) one-call conversion
open_pdf(source, password=None) a PdfDocument from a path, URL, bytes or file object
write_pdf(document, target=None, password=None, permissions=None) writes a PdfDocument
merge_pdfs(first, second, target=None) two PDFs appended
pdf_spec(keyword) the PdfSpec for a keyword ("pdf/a-3b")
progress_listener(callback) a Java ProgressListener for a Python callable
install(version="latest", *, snapshots, jakarta, home, force, bouncycastle) downloads a PD4ML build
installed_jar(), installed_classpath(), installed_version() the installed build
start(classpath=None, jvm_path=None, jvm_options=()) starts the JVM explicitly
is_started(), has_layout_engines(), version() state of the runtime
java_class(name) any Java class by name
to_url(location), to_input_stream(source) Python values as Java URL / InputStream

PD4ML methods with Python additions

Method
readHTML(source, base_url=None, encoding=None) source: path, URL string, bytes, file object, Java stream/URL
read_html_string(html, base_url=None) the document as a string
writePDF(target=None, spec=None) to a path / file object, or bytes; spec: PdfSpec or keyword
writeDOCX(target=None), writeRTF(target=None, rtf_wmf=False) as writePDF
renderAsImages(format) / render_png() pages as encoded images (bytes)
monitorProgressWith(listener) a ProgressListener or a Python callable
useHtmlRenderer(engine, html_width=None, params=None) the layout engine by name

All other methods are those of the Java class com.pd4ml.PD4ML, documented in the Java API section and the PD4ML Javadoc; static members (PD4ML.getVersion(), PD4ML.disableBatik) are on the class.

Java classes

Constants (with LayoutEngine, ErrorPolicy), PageSize, PageMargins, Dimensions (with Units), PdfSpec, PdfDocument, ProgressListener, ResourceProvider, CustomTag, StatusMessage, the exceptions, and JPD4ML (the unwrapped com.pd4ml.PD4ML).

ResourceProvider and CustomTag are extended in Java (Python can implement Java interfaces, not extend Java classes); register yours with addCustomResourceProvider / addCustomTagHandler and put its classes on the classpath. Examples: resource loaders, custom tags.

9. Further reading