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-pythonon Windows,~/.cache/pd4ml-pythonelsewhere, orPD4ML_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_HOMEfor 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:
pd4ml.start(classpath=[...]), called before the first conversion;- the
PD4ML_CLASSPATHenvironment variable — jars and class directories separated by;(Windows) or:(elsewhere),dir/*for all jars of a directory; - the
PD4ML_CLASSPATH_FILEenvironment variable — a file with one entry per line; - the build
pd4ml install(or the first use) downloaded.
Typical reasons to use them:
- Offline / air-gapped servers: run
pd4ml install --home ./pd4ml-libson a machine with network access, copy the directory, and setPD4ML_HOME(orPD4ML_CLASSPATH) andPD4ML_AUTO_INSTALL=0on 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>writespd4ml.classpathwith the build's classes and its Maven dependencies; setPD4ML_CLASSPATH_FILEto 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(...)orPD4ML_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,bytesforbyte[],dictforMap,Nonefornull. - Java strings come back as
strandnullasNone; numbers may come back as Java boxed numbers — wrap them inint(...)/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
PD4MLobject wraps the Java one, available aspd.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] — $[customer] — 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.
5.7 Bookmarks, links and tables of contents
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
PD4MLobject is not thread-safe; separate objects can convert in parallel threads. - Set
base_urlfor 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 layoutreadHTMLmakes; writes afterwards only choose the output format. - Write several formats from one read. PDF, DOCX and page images from a single
readHTMLcost one layout. - Print CSS works.
@page { size: A4 landscape; margin: 15mm },@media print, andpage-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
setHtmlWidthto the width the page was designed for. - Index fonts once with
-configure.fontsinstead of pointinguseTTFat a large unindexed directory on every conversion. - Restrict what documents can load when converting untrusted HTML: see
Constants.PD4ML_ALLOWED_RESOURCE_LOCATIONandPD4ML_DISABLE_EXTERNAL_ATTACHMENTS(viasetParam). - Memory: very large documents need JVM heap —
pd4ml.start(jvm_options=["-Xmx4g"])orPD4ML_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 aPD4ML_CLASSPATHto 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
- PD4ML documentation and the full programmer's manual
