# PD4ML for Python — User Guide

`pd4ml` brings [PD4ML](https://pd4ml.com) 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](https://jpype.readthedocs.io)), 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](#1.-prerequisites)
2. [Installation](#2.-installation)
3. [Quick start](#3.-quick-start)
4. [How it works](#4.-how-it-works)
5. [Use cases](#5.-use-cases)
6. [Tips and hints](#6.-tips-and-hints)
7. [Troubleshooting](#7.-troubleshooting)
8. [API reference](#8.-api-reference)
9. [Further reading](#9.-further-reading)

---

## 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](https://pd4ml.com/docs/#3.-licensing) and [5.18](#5.18-licensing). |

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](#2.3-other-ways-to-provide-pd4ml)), the first use of PD4ML in a process downloads the default
PD4ML build (see below) before starting Java. A notice on standard error reports it. So after
`pip install pd4ml`, this is all it takes:

```python
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 default PD4ML build: the most recent release of at least 4.2.0, or, until
  4.2.0 is released, the most recent 4.2.x snapshot;
- 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.

**Why the default is a snapshot for now.** This module wraps the PD4ML 4.2 API: `readEML` /
`readMHT`, `addAllowedResourceLocation`, XMP `addMetadata`, the DOM API
([5.21](#5.21-changing-the-document-before-conversion-dom-api), DMS and UA editions) and the v42
layout engine. They need PD4ML 4.2, which is published so far only as `4.2.0-SNAPSHOT`; the most
recent release, 4.1.2, lacks them. So the default is the most recent 4.2.x snapshot until 4.2.0 is released, and from then on the most
recent release of at least 4.2.0, with no change to the module. Later snapshots (4.3.0-SNAPSHOT)
are installed only with `--snapshots`. To keep an earlier release, pin it:
`pd4ml install --version 4.1.2` (the 4.2 API is then not available).

| Option | |
|---|---|
| *(none)* | the default build (the most recent release of at least 4.2.0, until then the most recent 4.2.x snapshot), or an update to it |
| `--snapshots` | the most recent 4.x build, including development snapshots |
| `--version 4.1.2` | an exact version |
| `--jakarta` | the `-jakarta` variant (for Jakarta EE / Servlet 6 web applications); PD4ML 4.2 has none, as its jar includes the Jakarta tag library, so the default build is installed |
| `--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:

```python
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](#2.3-other-ways-to-provide-pd4ml).

> **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](#4.3-layout-engines)):
  `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

```python
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:

```python
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](https://pd4ml.com/docs/#6.1-the-two-phase-model).

```python
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.

```python
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:

```python
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:

```python
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.

```python
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

```python
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](https://pd4ml.com/docs/#6.2-reading-the-source)):

```python
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

```python
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](https://pd4ml.com/docs/#7.-page-layout-size-margins-and-scaling).

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:

```python
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](https://pd4ml.com/docs/#8.-headers-footers-watermarks-and-backgrounds)
and the [usage examples](https://pd4ml.com/docs/usage-examples.html#set-page-header-and-footer).

### 5.5 Watermarks and page backgrounds

```python
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:

```python
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](https://pd4ml.com/docs/#4.3-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

```python
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](https://pd4ml.com/docs/usage-examples.html#add-toc).
Explicit page breaks: CSS `page-break-before` / `break-before`, or `<pd4ml:page.break>`.

### 5.8 PDF/A and PDF/UA

```python
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](https://pd4ml.com/docs/#11.2-pdfa-pdfua-and-e-invoicing-formats).

### 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](https://pd4ml.com/docs/features/zugferd-and-factur-x-pdf-invoices.html).

```python
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

```python
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](https://pd4ml.com/docs/#11.1-permissions-and-encryption).

### 5.11 Word (DOCX, RTF) and images

```python
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 is *editable*: positioned content becomes text boxes
and form fields, not pictures.

### 5.12 Interactive forms and multi-column layout

```python
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](https://pd4ml.com/docs/#10.1-interactive-pdf-forms) and
[multi-column pages](https://pd4ml.com/docs/#10.2-multi-column-pages) (PD4ML 4.0.24 / 4.1.2
and later).

### 5.13 Adding CSS and HTML programmatically

```python
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](https://pd4ml.com/docs/#10.3-injecting-html-and-custom-resource-providers)
and [adding styles](https://pd4ml.com/docs/usage-examples.html#add-style-programmatically).

### 5.14 Progress, diagnostics and metadata

```python
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.addMetadata("http://www.w3.org/ns/tdmrep/", "tdm", "reservation", "1")   # XMP, a namespace of your own
pd.read_html_string("<h1>Statement</h1>")
pd.writePDF("statement.pdf")
pages = int(pd.getLastRenderInfo(pd4ml.Constants.PD4ML_TOTAL_PAGES))
```

`addMetadata(namespace_uri, prefix, key, value)` writes a property in an XMP namespace of your
own (for PDF/A, declared in an extension schema), such as the TDM Reservation Protocol's
`tdm:reservation`. The v42 engine currently reports fewer progress stages than v40. More in
[extensibility and diagnostics](https://pd4ml.com/docs/#12.-extensibility-and-diagnostics).

### 5.15 Working with existing PDFs

```python
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](https://pd4ml.com/docs/#15.-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.

```python
# 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"})
```

```python
# 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

```python
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

```python
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](https://pd4ml.com/docs/#3.-licensing).

### 5.19 E-mail messages and saved web pages

> **Editions.** E-mail and saved web page input — `readEML`/`readMHT`, and `readHTML` for input it
> recognizes as one of them — is part of the PD4ML **DMS and UA editions**. With an STD or PRO license it
> works, but the output carries the "PD4ML EVALUATION MODE" banners.

PD4ML reads e-mail messages (`.eml`) and web pages saved as a single file (`.mht`, `.mhtml`)
and writes them to any output format. `readHTML` recognizes them by the file name or by how
they start; `readEML` and `readMHT` read the input as one whatever it looks like, which is the
way to go for mail from outside your control. Both take what `readHTML` takes: a path, URL
string, bytes, a binary file object or a Java stream.

```python
from email.message import EmailMessage
import pd4ml

msg = EmailMessage()
msg["From"], msg["To"], msg["Subject"] = "billing@example.com", "customer@example.com", "Your invoice"
msg.set_content("Please find your invoice attached.")
msg.add_alternative("<h1>Invoice 1001</h1><p>Please find your invoice attached.</p>", subtype="html")
msg.add_attachment(b"%PDF-1.7 ...", maintype="application", subtype="pdf", filename="invoice.pdf")

pd = pd4ml.PD4ML()
pd.readEML(msg.as_bytes())          # or a path: pd.readEML("message.eml"); pd.readMHT("page.mht")
pd.writePDF("message.pdf")
```

The document of a message is a block with From, Date, To, Cc, Subject and the names of the
attachments, followed by the body (HTML if the message has one) with the images it embeds. The
attachments are embedded in the PDF. A saved page shows with the images, style sheets and
fonts saved in it. Of the resources such a file refers to but does not contain, only `https://`
locations are loaded; `addAllowedResourceLocation` (5.20) replaces that default. Available from
PD4ML 4.2.0, with both layout engines. See
[converting e-mail and saved web pages](https://pd4ml.com/docs/features/converting-e-mail-and-saved-web-pages.html).

### 5.20 Untrusted HTML: restricting resource loading

HTML written by users or received from other systems decides what PD4ML loads: local files,
internal services, slow servers. `addAllowedResourceLocation` limits loading of style sheets,
images, fonts and attachments to locations you trust; everything else is skipped:

```python
import pathlib
import pd4ml

user_html = '<link rel="stylesheet" href="style.css"><img src="file:/etc/passwd"><p class="note">Hi</p>'

pd = pd4ml.PD4ML()
pd.addAllowedResourceLocation([pathlib.Path("."), "https://cdn.example.com/assets/"])
pd.addAllowedResourceLocation("https://*.example.org/")      # calls add up; also a comma-separated string
pd.read_html_string(user_html, base_url=".")
pd.writePDF("user-content.pdf")                              # style.css applied, the file: image skipped
```

A location is a URL prefix or a local path (a `pathlib` path stands for its `file:` URL); `*`
matches any characters except `/`, so `https://*.example.com/` covers the subdomains. A path
containing `..` is always rejected. As long as no location is added, resources load from
anywhere. With `setErrorPolicy(pd4ml.ErrorPolicy.PEDANTIC)` a rejected image fails the
conversion with an `IOException` instead of being left out. Call it before `readHTML`.
Available from PD4ML 4.2.0. See
[restricting resource loading](https://pd4ml.com/docs/features/restricting-resource-loading-for-untrusted-html.html).

### 5.21 Changing the document before conversion (DOM API)

With the v42 layout engine, `getDocument()` returns the document `readHTML` parsed, to read,
query with CSS selectors and change before the output; the output is laid out as if the HTML
had been written that way. `layout()` lays the document out into pages without writing it and
tells where each element ended up.

> The DOM API (`getDocument()`, `layout()` and the `com.pd4ml.dom` classes) is part of the
> **DMS and UA editions** of PD4ML 4.2.0 and later. Without a license it works, with the demo
> watermark; with an STD or PRO license the output of a document it was used on carries
> "PD4ML EVALUATION MODE" banners.

```python
import pd4ml

pd = pd4ml.PD4ML()                                  # v42, the default engine
pd.read_html_string("""
  <h1 id="number">Invoice 0000</h1>
  <table><tbody id="items"><tr><td>sample</td></tr></tbody></table>
  <p class="draft-only">DRAFT</p>
  <div id="payment">Payment details</div>""")
doc = pd.getDocument()

doc.getElementById("number").setTextContent("Invoice 1001")
rows = doc.querySelector("#items")
rows.setInnerHTML("")                               # drop the sample rows
for name, amount in [("Consulting", "800.00"), ("Travel", "120.00")]:
    tr = doc.createElement("tr")
    for value in (name, amount):
        td = doc.createElement("td")
        td.setTextContent(value)
        tr.appendChild(td)
    rows.appendChild(tr)
for e in doc.querySelectorAll(".draft-only"):      # a Python list
    e.remove()

layout = pd.layout()
payment = doc.getElementById("payment")
if len(layout.getPages(payment)) > 1:               # split over two pages: start it on a new one
    payment.setAttribute("style", "page-break-before: always")
box = layout.getBoxes(payment)[0]
print(layout.getPageCount(), "page(s); payment on page", box.getPage(), "at", box.getX(), box.getY())

pd.writePDF("invoice-1001.pdf")
```

The classes are exported: `pd4ml.Document`, `pd4ml.Element`, `pd4ml.Text`, `pd4ml.Comment`,
`pd4ml.DocumentType` (all `pd4ml.Node`), `pd4ml.Layout` and `pd4ml.LayoutBox`. Their methods
keep the Java names:

- **Document**: `getElementById`, `querySelector`, `querySelectorAll`, `getElementsByTagName`,
  `getElementsByClassName`, `getHead`, `getBody`, `getDocumentElement`, `getTitle`,
  `getDoctype`, `getBaseURI`, `createElement`, `createTextNode`, `createComment`, `toHtml`.
- **Node** (every node): `getParentNode`, `getParentElement`, `getChildNodes`,
  `getFirstChild`/`getLastChild`, `getNextSibling`/`getPreviousSibling`, `appendChild`,
  `insertBefore`, `replaceChild`, `removeChild`, `remove`, `cloneNode`, `contains`,
  `getTextContent`/`setTextContent`, `toHtml`.
- **Element**: `getTagName`, `getAttribute`/`setAttribute`/`hasAttribute`/`removeAttribute`,
  `getAttributes`, `getId`/`setId`, `getClassName`/`setClassName`, `getClassList`,
  `hasClass`/`addClass`/`removeClass`/`toggleClass`, `getChildren`,
  `getFirstElementChild`/`getLastElementChild`, `getNextElementSibling`/`getPreviousElementSibling`,
  `querySelector`/`querySelectorAll`, `matches`, `closest`,
  `getInnerHTML`/`setInnerHTML`, `getOuterHTML`/`setOuterHTML`.
- **Text**, **Comment**: `getData`/`setData` (`Text.isWhitespace()`); **DocumentType**:
  `getName`, `getPublicId`, `getSystemId`.
- **Layout**: `getPageCount`, `getPageWidth(page)`/`getPageHeight(page)` in points,
  `getPageNumber(page)`, `getBoxes(element)`, `getPages(element)`,
  `getComputedStyle(element, property)`, `elementAt(page, x, y)`.
- **LayoutBox**: `getPage`, `getX`, `getY`, `getWidth`, `getHeight`: in points from the top
  left corner of the page.

Where Java returns a list, the module returns a Python `list` (`querySelectorAll`,
`getChildNodes`, `getChildren`, `getElementsBy...`, `getBoxes`, `getPages` as a list of `int`);
`getAttributes` returns a `dict` and `getClassList` a `list` of `str`. Nodes compare with `==`
(two objects for one node are equal) and can be dict keys. Pages are numbered from 1.

`getDocument()` and `layout()` raise `java.lang.IllegalStateException` before `readHTML` and
with the v40 engine. A `Layout` is a snapshot: call `layout()` again after changing the
document. Make the changes before the first output. See
[modifying the HTML document before conversion](https://pd4ml.com/docs/features/modifying-the-html-document-before-conversion.html).

---

## 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](https://pd4ml.com/docs/#6.2-reading-the-source)).
- **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:
  `addAllowedResourceLocation` ([5.20](#5.20-untrusted-html-restricting-resource-loading)), 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 default build, which is a
  development snapshot until PD4ML 4.2.0 is released.

## 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](#2.2-install-pd4ml)). 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](https://pd4ml.com/docs/#3.-licensing)). |
| Squares instead of characters | The fonts lack those glyphs: provide fonts with `useTTF` ([fonts](https://pd4ml.com/docs/#9.-fonts-bookmarks-and-hyperlinks)). |
| `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 |
| `readEML(source)`, `readMHT(source)` | an e-mail message / a saved web page; source as `readHTML`; DMS and UA editions |
| `addAllowedResourceLocation(locations)` | a comma-separated string, or a sequence of strings and paths |
| `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](https://pd4ml.com/docs/#6.-the-java-api) 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`).

The DOM API (v42 engine; DMS and UA editions): `Document`, `Node`, `Element`, `Text`,
`Comment`, `DocumentType`, `Layout`, `LayoutBox` of `com.pd4ml.dom`, returned by
`getDocument()` and `layout()`, with Python lists and dicts for Java collections
([5.21](#5.21-changing-the-document-before-conversion-dom-api)).

`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](https://github.com/zxfr/pd4ml-examples/blob/master/src/main/java/advanced/A005AddCustomResourceLoader.java),
[custom tags](https://github.com/zxfr/pd4ml-examples/blob/master/src/main/java/advanced/A008AddingCustomTagRenderer.java).

## 9. Further reading

- [PD4ML documentation](https://pd4ml.com/docs/) and the full [programmer's manual](https://pd4ml.com/docs/pd4ml-manual.html)
- [Usage examples](https://pd4ml.com/docs/usage-examples.html)
- [HTML5 element support matrix](https://pd4ml.com/docs/html5-element-support-matrix.html)
- [PD4ML for Python compared with WeasyPrint, xhtml2pdf and Playwright](https://pd4ml.com/docs/integrations/pd4ml-python/python-html-to-pdf-library-comparison.html)
- [Command-line tool reference](https://pd4ml.com/docs/pd4cmd-command-line-reference.html)
- [ZUGFeRD and Factur-X invoices](https://pd4ml.com/docs/features/zugferd-and-factur-x-pdf-invoices.html)
- [PDF post-processing toolkit](https://pd4ml.com/docs/#15.-the-pdf-post-processing-toolkit)
- [Java examples on GitHub](https://github.com/zxfr/pd4ml-examples)

<!-- Copied from pd4ml-python/docs/guide.md by docs-generator/sync-integrations.js: edit it there. -->
