# 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 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:

```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 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:

```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 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

```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.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](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).

---

## 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: 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](#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 |
| `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`).

`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)
- [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. -->
