Documentation page: PD4ML Programmer's Manual. This page is also available as Markdown at pd4ml-manual.md.

PD4ML Programmer's Manual

This document applies to PD4ML v4.1.1

PD4ML is a pure-Java document generation engine that turns HTML and CSS into production-quality PDF, DOCX, and RTF — no browser, no native dependencies, no external rendering service. It runs anywhere a JVM does: inside a web application, a batch job, a desktop tool, or straight from the command line.

Beyond the core HTML-to-PDF conversion covered in this manual, PD4ML also ships a small suite of PDF post-processing packages -- signing, merging, optimizing, COS-level editing, and XFDF annotation import/export -- each with its own dedicated manual; see §15.

Every class, method and constant this manual mentions is described in full in the PD4ML API reference (Javadoc); the method names below link to their entries there.

1. What PD4ML does

At its core, PD4ML is a layout engine: it parses HTML/CSS (or arbitrary XML plus an XSL stylesheet) the same way a browser would, paginates the result, and writes it out in one of several formats. What sets it apart from "print to PDF" approaches built on a headless browser:

  • Deterministic, print-focused pagination -- explicit page breaks, repeating headers/footers, running page numbers, widow/orphan control, and multi-column layout, all driven by ordinary CSS plus a handful of PD4ML-specific extensions.
  • Standards-aware output -- PDF/A-1/2/3/4 (long-term archival), PDF/UA (accessible, tagged PDF), and ZUGFeRD/Factur-X (machine-readable e-invoices embedded in a human-readable PDF) are first-class output targets, not an afterthought (§11).
  • Full Unicode and font control -- TrueType/OpenType embedding, glyph subsetting, kerning, and ligature substitution, with correct shaping for Latin, Cyrillic, CJK, Arabic, and Hebrew scripts.
  • Multiple output formats from one conversion -- the same parsed document can be written as PDF, DOCX, RTF, or rendered to raster images, without re-parsing the source (§6.3).
  • Deployment flexibility -- a plain Java API, a self-contained command-line tool (§4), a GUI previewer (§5), a JSP tag library for web applications (§13), and integrations that bring the same API to Python, PHP, .NET, Node.js, and Ruby.

PD4ML is commercially licensed, with a free evaluation mode (watermarked output, no restriction on functionality) so an integration can be built and tested before a license is purchased.

2. Obtaining PD4ML

PD4ML is distributed as a single, self-contained jar plus a required Bouncy Castle companion (com.pd4ml.pdf.encryption -- standard PDF permissions/ protection, §11.1 -- and com.pd4ml.pdf.sign, §15.2, both depend on it; as of v4.1.0 it is no longer an optional, signing-only add-on) through PD4ML's own Maven repository:

<dependency>
    <groupId>com.pd4ml</groupId>
    <artifactId>pd4ml</artifactId>
    <version>4.1.1</version>
</dependency>

<!-- Required as of v4.1.0 -- com.pd4ml.pdf.encryption (document protection,
     &sect;11.1) and com.pd4ml.pdf.sign (&sect;15.2) both depend on it. A Maven
     build already pulls these in transitively via the dependency above;
     declare them explicitly only to pin a specific version. -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.84</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.84</version>
</dependency>

<repository>
    <id>pd4ml</id>
    <url>https://pd4ml.com/maven2/</url>
</repository>

A separate, credentialed repository (https://pd4ml.com/maven2-src/) also publishes source jars, for stepping through PD4ML's own code in a debugger during integration work.

Without Maven, the plain jar plus its bouncycastle-runtime.jar companion (a single jar merging the Bouncy Castle artifacts above -- see the note in §15.2) can be downloaded directly and placed on the classpath; nothing else needs to be installed to run it. PD4ML runs on Java 8 or later; TIFF image output (§6.3) needs Java 9 or later, whose built-in TIFF writer it uses.

From other languages, PD4ML's integrations download the jar from this same repository, and its companion jars, themselves, and expose the complete Java API to the host language (a Java runtime is still needed alongside):

Language Install Guide
Python pip install pd4ml PD4ML for Python
PHP composer require pd4ml/pd4ml-php PD4ML for PHP
.NET packages from pd4ml.com/net PD4ML for .NET
Node.js npm install pd4ml PD4ML for Node.js
Ruby gem install pd4ml PD4ML for Ruby

3. Licensing

PD4ML pd4ml = new PD4ML();                 // looks for pd4ml.lic on the classpath / working directory
PD4ML pd4ml = new PD4ML("SERIAL-NUMBER");   // the serial number from the license email
PD4ML pd4ml = new PD4ML("https://internal.example.com/pd4ml.lic");  // license file URI, instead of a serial number

With no license installed, PD4ML runs in evaluation mode: every feature is available, but output carries a watermark. This is normal and expected during development -- there is nothing to configure to unlock full, unwatermarked output beyond installing a valid license file or serial number.

What a license covers depends on its edition (STD, PRO, DMS, UA) -- see PD4ML editions and prices. The DOM API (§6.5: getDocument(), layout() and the com.pd4ml.dom package) is part of the DMS and UA editions. With an STD or PRO license it still works, but the output of a document it was used on carries the "PD4ML EVALUATION MODE" banners. The same holds for e-mail (.eml) and saved web page (.mht, .mhtml) input -- readEML(), readMHT(), and readHTML() for input it recognizes as a message or a saved page (see Converting E-mail Messages and Saved Web Pages).

if (pd4ml.isDemoMode()) {
    log.warn("PD4ML is running unlicensed -- output will be watermarked");
}
System.out.println("PD4ML " + PD4ML.getVersion() + " built " + PD4ML.getVersionBuildDate());

4. Command-line tool: Pd4Cmd

com.pd4ml.tools.Pd4Cmd is pd4ml.jar's own Main-Class -- run it directly with java -jar, no classpath assembly required:

java -Djava.awt.headless=true -Xmx512m -jar pd4ml.jar "https://example.com" 1200

The two required positional arguments are the source (a URL, or a local file: path) and the htmlWidth in pixels -- the virtual browser viewport width PD4ML lays the page out against before scaling it to the target paper size (see §7 for the scaling math). Everything else is an optional flag.

4.1 Common flags

Flag Purpose
-out <file> Output file (default: stdout)
LETTER, A4, LEGAL, ... (positional) Target page format
-orientation PORTRAIT|LANDSCAPE Page orientation
-insets <l,t,r,b[,mm|points]> Page margins
-bookmarks HEADINGS|ANCHORS Generate a bookmark tree
-header "<html>", -footer "<html>" Repeating page header/footer HTML
-watermark "<url,left,top,width,height,angle>" Image watermark
-bgcolor <#rrggbb>, -bgimage <url> Page background
-pdfforms Convert HTML <form> controls to real PDF form fields
-outformat pdf|pdfa|pdfua|... PDF/A / PDF/UA / ZUGFeRD conformance (§11.2)
-password <pw> Owner password + default permissions
-permissions <n> Explicit permission bitmask (§11.1)
-ttf <dir> TrueType/OpenType font directory to embed from
-kerning, -ligatures Font shaping refinements (§9)
-threads <n> Async resource-loader thread pool size
-nohyperlinks Disable hyperlink conversion
-adjustwidth Auto-fit htmlWidth to the document's natural width
-fitapage Downscale the whole layout to fit one page vertically
-multicolumn <columns,gap[,mm]> Multi-column page layout
-debug <level> Verbosity (0-5)

Run java -jar pd4ml.jar with no arguments for the complete, always-current flag reference.

4.2 Examples

Convert a live page to a bookmarked, form-enabled Letter PDF:

java -Djava.awt.headless=true -Xmx512m -jar pd4ml.jar \
    "https://example.com/report.html" 1200 LETTER \
    -bookmarks HEADINGS -pdfforms -out report.pdf

Render a local file with a running header/footer and page numbers:

java -jar pd4ml.jar "file:/data/invoice.html" 900 A4 \
    -header "<div style='text-align:right'>$[title]</div>" \
    -footer "<div style='text-align:center'>Page $[page] of $[total]</div>" \
    -out invoice.pdf

Pd4Cmd also carries a handful of built-in, single-document PDF post-processing flags for quick one-off tasks -- -tools <file> ... switches it from HTML conversion into edit mode:

# Keep only pages 2-3 and everything from page 5 onward
java -jar pd4ml.jar -tools file:/docs/source.pdf -pagerange 2-3,5+ -out subset.pdf

# Append another PDF after the source document
java -jar pd4ml.jar -tools file:/docs/source.pdf -merge file:/docs/appendix.pdf after -out combined.pdf

# Read back permission flags, author, title and page count
java -jar pd4ml.jar -tools file:/docs/source.pdf -printpermissions -printauthor -printtitle -printpagenum

For anything beyond these simple cases -- selecting page ranges from two independent source documents with accessibility-tag reconciliation, reachability-based cleanup and duplicate-content dedup, or arbitrary COS-level structural edits -- reach for the dedicated pd4mergecli, pd4optimizecli and pd4coscli tools instead (§15).

4.3 Font indexing

java -Xmx512m -jar pd4ml.jar -configure.fonts /path/to/fonts [pd4fonts.properties]

Scans a directory of TrueType/OpenType font files and writes (or updates) a font-mapping properties file PD4ML uses to resolve CSS font-family names to actual font files at conversion time -- run once per font directory, ahead of time, rather than paying the directory scan cost on every conversion.

5. GUI application: PD4Browser

A lightweight preview tool bundled in the same jar -- useful for iterating on CSS/layout without a full round trip through your application:

java -jar pd4ml.jar -gui

It shows the document page by page as PD4ML lays it out -- the window width is the HTML width, so resizing the window shows the effect of setHtmlWidth() directly -- and converts it to PDF, DOCX or RTF with Render. Its Fonts Tool builds font mapping files and font JARs for useTTF(). Its settings (page format, margins, fonts, headers and footers, bookmarks, proxy, ...) live in a pd4browser.properties file in the working directory -- the same settings a Java integration would set via the API in §6-9, as plain properties for quick experimentation. PD4Browser: Interactive Converter and Fonts Tool describes the window, the Fonts Tool and every setting of the file.

6. The Java API

6.1 The two-phase model

Every conversion is read once, then written one or more times: a readHTML(...) call parses and lays out the source; any number of subsequent write*(...)/renderAsImages() calls then serialize that same parsed document to different formats or destinations, without re-parsing.

import com.pd4ml.PD4ML;
import java.io.*;

PD4ML pd4ml = new PD4ML();
String html = "<html><body><h1>Hello, World!</h1></body></html>";
pd4ml.readHTML(new ByteArrayInputStream(html.getBytes("UTF-8")), null, "UTF-8");

try (FileOutputStream out = new FileOutputStream("hello.pdf")) {
    pd4ml.writePDF(out);
}

6.2 Reading the source

pd4ml.readHTML(inputStream);                          // encoding auto-detected
pd4ml.readHTML(inputStream, baseUrl);                  // relative links/images resolve against baseUrl
pd4ml.readHTML(inputStream, baseUrl, "windows-1251");  // explicit source encoding
pd4ml.readHTML(new URL("https://example.com/report"));  // fetch directly

A relative-URL baseUrl matters whenever the source HTML references external images, stylesheets, or fonts by relative path -- omitting it (or getting it wrong) is the most common cause of a converted document missing images that render fine in a browser.

Runnable example: A003ReadHtmlFromClasspath.java -- reading the HTML source from the classpath via java: URLs.

6.3 Writing the output

pd4ml.writePDF(pdfOutputStream);
pd4ml.writeRTF(rtfOutputStream, false);   // false: keep raster images as PNG/JPEG rather than converting to WMF
pd4ml.writeDOCX(docxOutputStream);

BufferedImage[] pages = pd4ml.renderAsImages();                     // one image per page, in memory
byte[][] pngBytes     = pd4ml.renderAsImages(Constants.PNG24);      // ... or already PNG-encoded
File dir              = pd4ml.renderAsImages(outDir, "page", Constants.PNG24);  // ... or written straight to disk

The encoded formats are Constants.PNG24, PNG8, TIFF, TIFF_COMPRESSED (PackBits), MULTIPAGE_TIFF and MULTIPAGE_TIFF_COMPRESSED (one file with all pages); "png" means PNG24, letter case does not matter, and any other name throws an InvokeException. PNG works on any Java version; the TIFF formats need Java 9 or later, and throw an IOException on Java 8.

Because reading happens once, converting the same source to PDF and a page-image thumbnail set costs one HTML parse, not two:

pd4ml.readHTML(new URL("https://example.com/report"));
pd4ml.writePDF(new FileOutputStream("report.pdf"));
BufferedImage[] thumbnails = pd4ml.renderAsImages();

Runnable example: E001GettingStarted.java

6.4 Choosing the layout engine

From PD4ML v4.2.0. PD4ML has two HTML layout engines, named by Constants.LayoutEngine: v40, the engine of PD4ML v4.0 and v4.1 and the default, and v42, with browser-compatible layout, CSS @page rules beyond the page size and margins, and the DOM API (§6.5). Select the engine with useHtmlRenderer(...) before readHTML(...):

pd4ml.useHtmlRenderer(Constants.LayoutEngine.v42);
pd4ml.useHtmlRenderer(Constants.LayoutEngine.v42, 1024);             // ... and the HTML width, as setHtmlWidth() sets it
pd4ml.useHtmlRenderer(Constants.LayoutEngine.v42, 1024, settings);   // ... and v42 settings, a Map<String, String>

The v42 settings are "media" (the CSS media type to lay out for, "screen" by default), "dark" ("true" matches @media (prefers-color-scheme: dark)), "externalResources" ("false" loads no images or style sheets), "webFonts" ("false" loads no @font-face fonts), "rejectUnitlessLengths" ("true" drops lengths without a unit, such as width: 600, outside quirks mode, as browsers do) and "bevelColorScheme" ("chrome" draws inset/outset/groove/ridge borders in Chrome's colours). Unknown keys are ignored.

6.5 The DOM API: reading and changing the document

From PD4ML v4.2.0, with the v42 layout engine. DMS and UA editions. Between readHTML(...) and the output, getDocument() returns the parsed HTML document, to read, query with CSS selectors and change; 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:

import com.pd4ml.dom.Document;
import com.pd4ml.dom.Element;
import com.pd4ml.dom.Layout;

pd4ml.useHtmlRenderer(Constants.LayoutEngine.v42);
pd4ml.readHTML(new URL("https://example.com/invoice.html"));

Document doc = pd4ml.getDocument();
doc.getElementById("total").setTextContent("1 250,00 EUR");
for (Element img : doc.querySelectorAll("img:not([alt])")) {
    img.setAttribute("alt", "");
}

Layout layout = pd4ml.layout();
if (layout.getPages(doc.getElementById("totals")).size() > 1) {   // split over two pages?
    doc.getElementById("totals").setAttribute("style", "page-break-before: always");
}
pd4ml.writePDF(out);

The classes, in the package com.pd4ml.dom:

Class What it gives
Node the tree: getParentNode(), getChildNodes(), siblings, appendChild(), insertBefore(), replaceChild(), removeChild(), remove(), cloneNode(), getTextContent()/setTextContent(), toHtml()
Document getElementById(), querySelector()/querySelectorAll(), getElementsByTagName()/getElementsByClassName(), getHead(), getBody(), getTitle(), createElement(), createTextNode(), createComment()
Element attributes (getAttribute(), setAttribute(), removeAttribute(), getAttributes()), classes (addClass(), removeClass(), toggleClass(), hasClass()), getInnerHTML()/setInnerHTML(), getOuterHTML()/setOuterHTML(), matches(), closest(), child and sibling elements
Text, Comment, DocumentType text (getData(), setData(), isWhitespace()), comments and the doctype
Layout from layout(): getPageCount(), page sizes and numbers, getBoxes(element), getPages(element), elementAt(page, x, y), getComputedStyle(element, property)
LayoutBox one box of an element: its page, getX(), getY(), getWidth(), getHeight() in points from the top left of the page

Make the changes before the first output: writing a document moves footnotes and endnotes into place in the tree. layout() is a snapshot -- after changing the document, call it again. With the v40 engine, or before readHTML(...), both methods throw an IllegalStateException.

The full guide, with more examples: DOM API: Modifying the HTML Document Before Conversion. Runnable examples: A009ModifyDocumentWithDom.java, A010ReadLayout.java.

7. Page layout: size, margins, and scaling

Three settings govern how source content maps onto printed pages:

Setting Meaning Default
pageSize Target paper format A4, portrait
pageMargins Blank border reserved on every page 10 mm all sides
htmlWidth Virtual viewport width (px) the source is laid out against 727 px
import com.pd4ml.PageSize;
import com.pd4ml.PageMargins;
import com.pd4ml.Dimensions.Units;

pd4ml.setPageSize(PageSize.LETTER);
pd4ml.setPageMargins(new PageMargins(15, 20, 15, 20, Units.MM));
pd4ml.setHtmlWidth(1000);

Content is scaled uniformly to fit the printable area:

scale = (pageWidth - marginLeft - marginRight) / htmlWidth

A narrower htmlWidth means a larger scale factor -- text and images appear bigger on the page, at the cost of how much content fits horizontally before wrapping. pd4ml.adjustHtmlWidth(true) (or the CLI's -adjustwidth) instead measures the document's own natural width and uses that, useful for source HTML that isn't already responsive-width-aware.

The scale applies to every size, including those given in physical units: with an htmlWidth of 1600 px on A4, font-size: 12pt prints at about 5.5pt. protectPhysicalUnitDimensions(true) keeps sizes in mm, cm, in, pt and pc at their true size while pixel-sized content still scales (v40 engine; the v42 engine always converts physical units at 96 dpi, as browsers do). From PD4ML v4.2.0 it works for DOCX and RTF output too - before, it made physical sizes there about four times too small.

setPageSize(...)/setPageMargins(...) also accept an optional scope string ("1", "2+", "3-7,odd", ...) to vary page size or margins across the document -- the same scope grammar used by headers/footers (§8) and by Pd4Cmd -pagerange.

fitPageVertically(Constants.CENTER_ALIGN) downscales the entire layout to guarantee it fits one single page, rather than paginating -- handy for "print this as a single-page summary" use cases; it's mutually exclusive with multi-column mode (§10.1).

Runnable example: E002SetPageFormat.java

8. Headers, footers, watermarks, and backgrounds

Repeating page decorations are themselves HTML/CSS fragments, not a separate mini-format to learn:

pd4ml.setPageHeader("<b>$[title]</b>", 30, "1");             // first page only
pd4ml.setPageHeader("<b>$[title]</b> &mdash; $[page]/$[total]", 30, "2+");  // every page after
pd4ml.setPageFooter(
    "<div style='width:100%; text-align:right; font-size:8pt'>Page $[page] of $[total]</div>",
    24, "1+");

Three placeholders are always available: $[page] (current page number), $[total] (total page count), and $[title] (document title). Arbitrary additional placeholders come from setDynamicData(...):

Map<String, String> data = new HashMap<>();
data.put("customer", "Acme Corp.");
data.put("invoiceNo", "INV-2026-0031");
pd4ml.setDynamicData(data);
pd4ml.setPageFooter("<div>Invoice $[invoiceNo] &mdash; $[customer]</div>", 24, "1+");

A watermark, and a full-bleed page background rendered underneath every page's content, follow the same HTML-fragment-plus-scope model -- the watermark content is itself HTML (an <img>, styled text, or anything else CSS can lay out), not a bare image URL:

pd4ml.setWatermark("<img src='https://example.com/draft-stamp.png'/>",
        50, 50,      // left, top
        0.15f,       // opacity
        30,          // rotation angle, degrees
        1.0f,        // scale
        true, true,  // visible on screen and in print
        "1+");

pd4ml.setPageBackground("<div style='background:#f7f7f7; height:100%'></div>", "1+");

Watermarks also appear in RTF and DOCX output (from v4.2.0): with the v40 engine as a Word text watermark, with v42 as a picture of the watermark - see RTF and DOCX Output: Watermarks.

Runnable examples: E003SetPageHeaderFooter.java, E004SetPageHeaderFooterInline.java (headers/footers), E005SetPageBackground.java, E006SetPageBackgroundInline.java (backgrounds), E007SetPageWatermark.java, E008SetPageWatermarkInline.java (watermarks) -- each via the API and inline tags respectively.

CSS gradient backgrounds. From PD4ML v4.2.0, both layout engines. linear-gradient(), radial-gradient(), conic-gradient() and their repeating- forms work as backgrounds, in PDF and image output:

.banner  { background: linear-gradient(to right, #f80, #08f); border-radius: 12px; }
.badge   { background: radial-gradient(circle at 30% 30%, #fff, #fc0 40%, #c60); }
.chart   { background: conic-gradient(#36c 0 40%, #f80 0 75%, #ccc 0); border-radius: 50%; }
.stripes { background: repeating-linear-gradient(45deg, #eee 0 10px, #fff 10px 20px); }
.fade    { background: linear-gradient(transparent, rgba(0,0,0,.6)), url(photo.jpg); }
  • The full CSS syntax: angles and to <side> / to <corner>; radial shapes, sizes (closest-side, farthest-corner, ...) and at <position>; conic from <angle> and at <position>; color stops anywhere along the gradient, hard stops and double-position stops. Transparent stops fade without turning grey.
  • Sized and tiled like images: background-size, background-position and background-repeat apply, so linear-gradient(...) 0 0 / 20px 20px tiles a pattern.
  • Boxes: gradients follow rounded corners, and show through the gaps of dashed and dotted borders. On a box that breaks across pages, the gradient continues from one page to the next.
  • In PDF, gradients are vector shadings, not images: sharp at any zoom, and small. Transparent stops become a soft mask. PDF/A-1 does not allow transparency, so there the colors are blended over white instead.

The v40 layout engine draws one background layer per box (the first gradient or image), reads background-size only from the property itself (not from the / size part of the background shorthand), and draws a border-radius: 50% box as a pill rather than an ellipse.

A background image or gradient on <body> -- or on <html>, when <body> has no background -- fills every page of the v42 layout engine's output, as in v40.

pd4ml.useTTF("/opt/fonts");                 // TrueType/OpenType directory to embed glyphs from
pd4ml.embedTTFs(true, false);               // embed only the glyphs actually used, not the full font
pd4ml.applyKerning(true);                   // honor the font's own kerning pairs
pd4ml.enableLigatures(true);                // build compound glyphs (fi, fl, ...) where the font defines them

pd4ml.generateBookmarksFromHeadings(true);  // one bookmark per <h1>-<h6>
pd4ml.generateBookmarksFromAnchors(true);   // ... or from named <a name="..."> anchors instead
pd4ml.enableHyperlinks(true);               // convert <a href> into live PDF link annotations (default: on)

PD4ML ships with broad Unicode coverage out of the box; useTTF(...) matters when the source document needs a specific font family (a corporate typeface, a CJK font with particular glyph coverage, ...) rather than PD4ML's own font substitution.

Emoji and color fonts. From PD4ML v4.2.0, both layout engines. Emoji are drawn in color, in PDF and image output, from a color font:

Color font format Example fonts Support
COLR (version 0) + CPAL: layered vector glyphs Twemoji Mozilla, Segoe UI Emoji supported
CBDT/CBLC: PNG bitmap glyphs Noto Color Emoji supported
sbix Apple Color Emoji not supported
OpenType-SVG, COLR version 1 not supported

Make an emoji font available like any other font -- with useTTF(...) or @font-face:

pd4ml.useTTF("/opt/fonts");   // a directory that also holds NotoColorEmoji.ttf
@font-face { font-family: 'Noto Color Emoji'; src: url(fonts/NotoColorEmoji.ttf); }

No font-family is needed in the document: an emoji the text font lacks comes from a color emoji font of the font set (Twemoji Mozilla, Noto Color Emoji, Segoe UI Emoji, ...). Host fonts are never used, so the output does not depend on the machine it is made on.

Alternatively, put the optional pd4ml-emoji add-on (Twemoji Mozilla, about 0.7 MB) on the class path next to pd4ml.jar: emoji then work with no setup. It is in PD4ML's Maven repository (§2), with the same version as PD4ML, and its jar is part of the distribution zip:

<dependency>
    <groupId>com.pd4ml</groupId>
    <artifactId>pd4ml-emoji</artifactId>
    <version>4.2.0</version>
</dependency>

Its emoji graphics are licensed under CC-BY 4.0 (attribution in the jar's META-INF/NOTICE.txt). A font of the same family given with useTTF(...) or @font-face takes precedence. Any jar can add fonts this way: a META-INF/pd4ml-fonts.properties file in it lists family=/resource/path lines.

  • Emoji sequences stay one glyph: families and professions joined with zero-width joiners, skin tones, flags, keycaps -- by the font's own ligatures.
  • Text stays text: in PDF, the emoji are drawn as vector shapes or images, with invisible text underneath, so they can be selected, copied, pasted and searched. The color font itself is not embedded.
  • DOCX and RTF: emoji are written as text; Word draws them with its own emoji font.
  • Known gap: Twemoji's keycap emoji (1️⃣, #️⃣) are not formed and are left blank; Noto Color Emoji's keycaps work.

Runnable examples: N001TtfFonts.java, N002TtfFonts.java (TTF fonts), H003CreateBookmarks.java (bookmarks from headings).

10. Forms, multi-column layout, and dynamic content

10.1 Interactive PDF forms

pd4ml.generateForms(true, null);   // convert <input>/<select>/<textarea> to real PDF form fields

The resulting fields are ordinary AcroForm widgets -- readable and fillable in any PDF viewer, and, downstream, exactly what com.pd4ml.pdf.xfdf (§15.5) reads values out of and writes values back into via XFDF.

The second argument names the font of the field values: a TrueType font made available with useTTF(...) or @font-face, or null for the default.

Field appearances. From PD4ML v4.0.25 and v4.2.0. Every field carries its own appearance stream -- what the field shows before anyone edits it:

Field Shows
<input type="text"> the value, aligned as its text-align says, in the field's text color
<input type="password"> one bullet per character -- never the value itself
<textarea> the value, wrapped at the field width, from the top
<select> (drop-down) the selected option
<select size="...">, <select multiple> (list box) the options, the selected ones highlighted
<input type="checkbox">, <input type="radio"> the check mark or dot when checked

So the values are visible in every viewer, including those that do not draw form fields themselves: printing, flattening, browser previews, PDF libraries. And since the document no longer asks the viewer to regenerate the fields (NeedAppearances), Acrobat no longer offers to save a document that was only read. The text is laid out the way Acrobat lays out a field while it is edited, so it does not move when a field gets the focus.

Editing. While a user edits a field, the viewer draws the text itself with the field's font, referenced by name and not embedded -- as Acrobat does for its own forms: the viewer uses the installed font, or a similar one where it is missing. Users can type Latin-1 (WinAnsi) characters. The appearances PD4ML writes use the embedded form font, so a value in any script is shown exactly until it is edited.

10.2 Multi-column pages

Available in PD4ML v4.0.24 and v4.1.2 and newer (PDF output).

pd4ml.generateMulticolumn(2, 20, true);   // 2 columns, 20mm gap

The document is laid out to the width of a column; every page break moves on to the next column, and a new page starts after the last one. <pd4ml:page.break multicolumn="true"/> starts a new page right away. Headers, footers, backgrounds and watermarks span the page, page numbers count pages, and footnotes go to the bottom of their column.

Not combinable with fitPageVertically(...) (§7) or adjustHtmlWidth(...) -- they compete to control the overall page layout; with either of them the document is output in a single column.

Runnable examples: H008MulticolumnNewsletter.java (two columns, header/footer, footnote), H009MulticolumnGlossary.java (three columns, landscape).

10.3 Injecting HTML and custom resource providers

pd4ml.injectHtml("<div class='cover-note'>Generated " + LocalDate.now() + "</div>", true);

injectHtml splices a fragment right after the opening <body> (or, with prependBodyContent=false, right before the closing </body>) -- useful for adding a fixed cover note or trailer without having to reassemble the whole source document string.

A custom ResourceProvider lets PD4ML resolve resources through a scheme of your own choosing -- a CMS attachment ID, a database BLOB, a signed internal URL -- rather than only http(s):///file::

public class DatabaseImageProvider extends ResourceProvider {

    public static final String PROTOCOL = "db-image";

    @Override
    public boolean canLoad(String resource, FileCache cache) {
        return resource.toLowerCase().startsWith(PROTOCOL);
    }

    @Override
    public BufferedInputStream getResourceAsStream(String resource, FileCache cache) throws IOException {
        String imageId = resource.substring(PROTOCOL.length() + 1);
        byte[] bytes = imageRepository.loadBytes(imageId);   // your own lookup
        return new BufferedInputStream(new ByteArrayInputStream(bytes));
    }
}
pd4ml.addCustomResourceProvider("com.example.DatabaseImageProvider");
<img src="db-image:41208" alt="Signature" />

Runnable examples: E010InjectHtml.java, A005AddCustomResourceLoader.java, A005AddImageConvertingResourceLoader.java

11. Document security, PDF/A, PDF/UA, and ZUGFeRD/Factur-X

11.1 Permissions and encryption

setPermissions(...) is implemented by com.pd4ml.pdf.encryption, which depends on Bouncy Castle -- see §2 for the required Maven artifacts (or bouncycastle-runtime.jar alongside the plain jar).

import com.pd4ml.Constants;

// Owner password "secret"; readers can view and print, nothing else
pd4ml.setPermissions("secret", Constants.AllowPrint);

// Everything allowed except modifying the document
pd4ml.setPermissions(null, Constants.DefaultPermissions ^ Constants.AllowModify);

// AES-256 instead of the default encryption method (AES-128)
pd4ml.setPermissions("secret", Constants.DefaultPermissions, Constants.AES_256BIT);

Constants defines one bit flag per PDF permission (AllowPrint, AllowModify, AllowCopy, AllowAnnotate, AllowFillingForms, AllowContentExtraction, AllowAssembly, AllowDegradedPrint, ...) -- combine with |, or start from DefaultPermissions (everything allowed) and subtract with ^. The optional third argument to setPermissions picks the encryption method: RC4_40BIT, RC4_128BIT, AES_128BIT (the default), or AES_256BIT.

Runnable examples: E009SetDocumentPassword.java, E009SetDocumentPermissions.java

11.2 PDF/A, PDF/UA, and e-invoicing formats

pd4ml.writePDF(out, Constants.PDFA);    // PDF/A-1b: long-term archival, all fonts embedded
pd4ml.writePDF(out, Constants.PDFUA);   // PDF/UA + PDF/A-2a: accessible, tagged PDF

As of v4.1.0, the Constants shortcuts above are just two fixed points on a much larger surface exposed by writePDF(OutputStream, PdfSpec). A PdfSpec names one target specification -- a PDF version, a PDF/A or PDF/UA conformance level, or one of the e-invoicing presets below -- and combine() layers any number of them onto a single call:

import com.pd4ml.PdfSpec;

pd4ml.writePDF(out, PdfSpec.PDFA_3B);
pd4ml.writePDF(out, PdfSpec.PDF_1_7_8.combine(PdfSpec.PDFA_3B));

PDF version. PdfSpec names every PDF version from 1.4 through 2.0, including the numbered ISO extension levels that Acrobat itself has historically used to track incremental additions to the 1.7 line:

PdfSpec.PDF_1_4
PdfSpec.PDF_1_5
PdfSpec.PDF_1_6
PdfSpec.PDF_1_7
PdfSpec.PDF_1_7_1   // through
PdfSpec.PDF_1_7_11  // ISO 32000-1 extension levels 1-11
PdfSpec.PDF_2_0     // ISO 32000-2

From PD4ML v4.2.0: pd4ml.setParam(Constants.PD4ML_PDF_BROTLI, "true") compresses the streams of PDF 2.0 output with Brotli (/Filter /BrotliDecode) instead of Flate -- smaller files, but BrotliDecode is a recent PDF Association extension, not part of ISO 32000-2:2020, and many viewers cannot read it yet. It is ignored below PDF 2.0 and for PDF/A and PDF/UA output, which must use standard filters only.

PDF/A (long-term archival). Ten constants cover PDF/A-1 through PDF/A-4, each named for its conformance level: a (Accessible -- adds tagged-PDF structure on top of b), b (Basic -- visual reproducibility only), and, from PDF/A-2 onward, u (Unicode -- guarantees text is extractable/searchable without necessarily being fully tagged). PDF/A-4 drops the letter scheme in favor of a plain baseline plus an e (Embedded-files) variant, continuing PDF/A-3's relaxation of the earlier prohibition on arbitrary embedded files:

PdfSpec.PDFA_1A
PdfSpec.PDFA_1B
PdfSpec.PDFA_2A
PdfSpec.PDFA_2B
PdfSpec.PDFA_2U
PdfSpec.PDFA_3A
PdfSpec.PDFA_3B
PdfSpec.PDFA_3U
PdfSpec.PDFA_4
PdfSpec.PDFA_4E

PDF/UA (accessibility). Two levels are available, tied to the two ISO base specifications: PDFUA_1 (built on PDF 1.7 / ISO 32000-1) and PDFUA_2 (built on PDF 2.0 / ISO 32000-2 -- it can only be combined with PdfSpec.PDF_2_0, not an earlier version):

PdfSpec.PDFUA_1
PdfSpec.PDFUA_2   // requires PdfSpec.PDF_2_0

Any of these can be layered together in a single call, as long as the combination is one the PDF/A and PDF/UA specifications themselves permit -- PDF version, archival level, and accessibility level are independent axes:

pd4ml.writePDF(out, PdfSpec.PDFUA_1);
pd4ml.writePDF(out, PdfSpec.PDF_1_7_8.combine(PdfSpec.PDFUA_1));
pd4ml.writePDF(out, PdfSpec.PDF_1_7_11.combine(PdfSpec.PDFA_3A).combine(PdfSpec.PDFUA_1));

ZUGFeRD and Factur-X. Four further constants bundle the exact PDF version and PDF/A level each e-invoicing standard requires into one name, so the underlying combine() call never needs to be written out by hand. The plain and _a forms differ only in PDF/A conformance level -- _a produces a PDF/A-3a (accessible, tagged) container instead of the baseline PDF/A-3b:

Constant Equivalent to
PdfSpec.ZUGFeRD PdfSpec.PDF_1_7.combine(PdfSpec.PDFA_3B)
PdfSpec.ZUGFeRD_a PdfSpec.PDF_1_7.combine(PdfSpec.PDFA_3A)
PdfSpec.FacturX PdfSpec.PDF_1_7.combine(PdfSpec.PDFA_3B)
PdfSpec.FacturX_a PdfSpec.PDF_1_7.combine(PdfSpec.PDFA_3A)
pd4ml.writePDF(os, PdfSpec.ZUGFeRD);
pd4ml.writePDF(os, PdfSpec.FacturX);

These presets only select the container's PDF/A conformance level -- they don't attach the invoice XML itself, which is a separate step covered in full, including the required attachment filenames and a working end-to-end example, on the ZUGFeRD and Factur-X PDF Invoices page in the Features section of this site.

Not every combination is meaningful -- pairing PDFUA_2 with a pre-2.0 PDF version is the clearest example -- and combine() rejects an invalid pairing by throwing PdfSpecViolationException or PdfSpecUnsupportedException rather than silently producing a non-conformant file; catch (or let propagate) whichever of the two applies while the exact target spec for a given document is still being worked out.

PDF/UA conformance depends on the source HTML being well-structured (real headings, <table> markup for tabular data, alt text on images, ...) as much as on PD4ML's own output -- pairing any of the targets above with setErrorPolicy(Constants.ErrorPolicy.PEDANTIC) surfaces a source document that doesn't actually satisfy the requested standard, rather than silently emitting non-conformant output.

Runnable example: Z001ConvertHtml.java with Z001.htm -- a PDF/A-3b ZUGFeRD/Factur-X invoice.

11.3 Restricting where resources are loaded from

PD4ML loads every stylesheet, image, web font and attachment the HTML refers to, with the network access and file permissions of your server. When the HTML comes from a source you do not fully control, limit loading to locations you trust with addAllowedResourceLocation(...) (from v4.2.0) - otherwise the HTML can make your server read local files or call internal services:

pd4ml.addAllowedResourceLocation("https://cdn.example.com/assets/, /opt/app/templates/");
pd4ml.addAllowedResourceLocation("https://*.example.com/");   // adds to the locations above

Resources outside the listed URL prefixes are not loaded; * matches any characters except /. The same restriction can be set with setParam(Constants.PD4ML_ALLOWED_RESOURCE_LOCATION, ...). Details, what is checked and recommendations: Restricting Resource Loading for Untrusted HTML.

E-mail messages and saved web pages load only from https:// unless you list locations this way: Converting E-mail Messages and Saved Web Pages.

12. Extensibility and diagnostics

pd4ml.setErrorPolicy(Constants.ErrorPolicy.PEDANTIC);  // fail fast on malformed source / non-conformant output
pd4ml.setLogLevel(3);                                   // 0 (silent) .. 5 (very verbose)

// ProgressListener callback: messageID identifies the conversion phase (see
// ProgressListener's own constants), progress is a 0-50 estimate, msec is
// elapsed time since conversion start
pd4ml.monitorProgressWith((messageID, progress, message, msec) ->
        System.out.println(progress + "% - " + message));

// Fetch images/attachments with up to 8 concurrent threads (avoid in a
// Java EE container that restricts application-managed thread creation)
pd4ml.asyncResourceLoader(8);

pd4ml.addMetadata("InvoiceNumber", "INV-2026-0031", false);            // custom PDF document-info entry
pd4ml.addDocumentActionHandler("OpenAction", "app.alert('Welcome');"); // PDF-embedded JavaScript

getLastRenderInfo(Constants.PD4ML_TOTAL_PAGES) and getLastRenderInfo(Constants.PD4ML_PDFA_STATUS) retrieve information about the just-completed render -- how many pages it produced, or whether the requested PDF/A conformance was actually achieved -- after writePDF(...) returns.

Runnable examples: A004AddProgressListener.java, A007RenderingStatusInfo.java, A008AddingCustomTagRenderer.java

13. Web integration: the JSP tag library

Copy pd4ml*.jar into WEB-INF/lib and the tag library is available to any JSP page, no further deployment step needed. Two taglib URIs are published, for a classic javax.servlet.jsp container and a Jakarta EE 9+/jakarta.servlet.jsp one respectively -- pick the one matching your servlet container:

<%@ taglib uri="http://pd4ml.com/tlds/4.2" prefix="pd4tl" %>      <%-- javax: Tomcat 9 and earlier --%>
<%@ taglib uri="http://pd4ml.com/tlds/4.2-jk" prefix="pd4tl" %>   <%-- jakarta: Tomcat 10 and later --%>
<%@ taglib uri="http://pd4ml.com/tlds/4.2" prefix="pd4tl" %>
<pd4tl:transform
    screenWidth="1000"
    pageFormat="A4"
    pageInsets="15,20,15,20,mm">
<html>
<head><style>body { font-family: "Helvetica", sans-serif; }</style></head>
<body>
    <h1>${invoice.number}</h1>
    <pd4ml:page.footer height="24">Page $[page] of $[total]</pd4ml:page.footer>
    <!-- ... document body ... -->
</body>
</html>
</pd4tl:transform>

The tag resets the response when it starts, so output before it (white space included) is discarded, and it sets the document's content type itself; the page after </pd4tl:transform> is not processed. The response must not be committed before the tag: frameworks that wrap every JSP in a shared layout (Struts, JSF, ...) should render the PD4ML page from its own, undecorated JSP, or use the tag's forward mode. The pd4tl prefix keeps pd4ml: free for PD4ML's own tags in the HTML, such as <pd4ml:page.footer> above.

To save the generated file server-side instead of streaming it to the client, nest savefile in the transform tag (the directory must exist):

<pd4tl:transform screenWidth="1000">
    <pd4tl:savefile
        dir="/var/spool/generated-pdfs"
        redirect="/app/download.jsp" />
    ...
</pd4tl:transform>

redirect receives a filename request parameter naming the file just written under dir -- download.jsp (or a plain servlet) then streams, emails, or archives it as your application requires. A redirect path starting with / is relative to the server root, so it includes the application's context path.

The complete reference of the tags - headers and footers, permissions, fonts, metadata, output formats, forward mode, troubleshooting - is the PD4ML JSP Tag Library guide.

14. IBM Notes/Domino integration

A Notes/Domino document can be converted the same way any other HTML source is -- either fetched live over HTTP:

http://<host>/<database>/<view>/<documentUNID>?OpenDocument

or exported to DXL (Domino's XML document representation) and converted through an XSL stylesheet PD4ML ships for that purpose:

java -Djava.awt.headless=true -Xmx512m -jar pd4ml.jar document.dxl 1200 -xsl notesdefault

Either approach works equally well from a scheduled Domino agent (Java or LotusScript, shelling out to Pd4Cmd) or an external batch process with network access to the Domino server. The URL patterns, the DXL export, and both deployment options are described in PD4ML and IBM Notes/Domino.

15. The PDF post-processing toolkit

Alongside HTML-to-PDF conversion, PD4ML ships five focused packages for working with existing PDFs -- each is a complete, independently-documented feature with its own manual, runnable examples, and command-line tool; this section only orients you to which one covers which job.

15.1 com.pd4ml.pdf.cos -- the PDF object model

The low-level PDF object model, parser, COS-path query language, and incremental-update writer everything below is built on. Reach for it directly for structural edits nothing else here covers -- rotating a page, patching an arbitrary dictionary key, auditing a document's object graph. From v4.2.0 it also reads streams compressed with Brotli (BrotliDecodeFilter, the PDF 2.0 extension /BrotliDecode), and COSDocumentWriter.isPdfA1(document) tells whether a document declares PDF/A-1 conformance in its XMP metadata. See the PD4ML COS API Manual and pd4coscli Reference.

Runnable examples: com.pd4ml.pdf.cos.example.usecases

15.2 com.pd4ml.pdf.sign -- digital signatures

PKCS#12/PKCS#11/HSM/cloud-KMS signing, RFC 3161 timestamping, DocMDP certification, and PAdES-LT/LTV long-term validation. The only one of the five post-processing packages that needs Bouncy Castle on the classpath -- already required regardless by the core engine's own document protection, §11.1 -- see the PD4ML Signing Manual and pdfsigncli Reference.

Runnable examples: com.pd4ml.pdf.sign.example.usecases

15.3 com.pd4ml.pdf.merge -- combining PDFs

Selects independent page ranges from two or more source PDFs and merges them into one structurally-correct document, reconciling tagged (accessibility) structure across sources. See the PD4ML PDF Merge Manual and pd4mergecli Reference.

Runnable examples: com.pd4ml.pdf.merge.example.usecases

15.4 com.pd4ml.pdf.optimizer -- cleanup and size reduction

Drops unreferenced objects, deduplicates byte-identical content, and collapses incremental-update history into one fresh, compact file -- optionally linearized for Fast Web View (from v4.2.0: PdfOptimizer.Options.linearize(true); a viewer reading the file over a network shows the first page before the whole file has arrived; not with encrypted output). See the PD4ML PDF Optimizer Manual and pd4optimizecli Reference.

Runnable examples: com.pd4ml.pdf.optimizer.example.usecases

15.5 com.pd4ml.pdf.xfdf -- annotation & form-value exchange

Exports a PDF's annotations and AcroForm field values to XFDF (ISO 19444-1), or imports an XFDF file's back in -- the same format most PDF viewers use for "export/import comments", including tagging newly-added annotations into an already-accessible target. See the PD4ML XFDF Manual and pd4xfdfcli Reference.

Runnable examples: com.pd4ml.pdf.xfdf.example.usecases

16. See also