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

PD4ML for .NET

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

PD4ML is a Java library. pd4ml-dotnet runs it in a small Java bridge process that .NET talks to over a pipe (or a local TCP port). Every PD4ML class has a typed .NET wrapper (PD4ML, PageSize, PdfSpec, PdfDocument...) with its methods in PascalCase. The library adds .NET I/O on top: file paths, byte[], strings and Streams instead of Java streams.

An alternative to the native port. A native .NET port of PD4ML is pending. pd4ml-dotnet lets .NET applications use PD4ML now. It runs the Java PD4ML itself, so you get the same features, output and release cadence as the Java library, at the cost of needing a Java runtime next to .NET (see 4.2).

The PD4ML manual, which this guide refers to throughout, is at https://pd4ml.com/docs/. The generated reference of every class and method is docs/api.md in the source archive from pd4ml.com/net.

Contents


1. Prerequisites

Requirement Notes
.NET 6 or later, or .NET Framework 4.6.1+ The library targets .NET Standard 2.0, so it runs on .NET 6, 7, 8 and later, and on .NET Framework 4.6.1 and later. The pd4ml command-line tool needs .NET 6 or later.
Java 8 or later A JDK or JRE. An LTS release (17, 21) is recommended. The library finds Java through PD4ML_JAVA, then JAVA_HOME, then java on the PATH.
PD4ML Downloaded by pd4ml install or Installer.Install() (see below), or any PD4ML build you point the library at.
Network access Needed only for the download. Conversions need no network access unless the HTML references remote resources.
A PD4ML license Optional for evaluation. Without one, PD4ML runs in evaluation mode and watermarks its output. See Licensing and 5.18.

Check with dotnet --info and java -version. On a server without Java, install a JRE package (for example apt install openjdk-21-jre-headless). If it is not on the PATH, set JAVA_HOME or PD4ML_JAVA. In containers, use an image that has both runtimes, or add a JRE to the .NET image.

2. Installation

2.1 Add the library

pd4ml-dotnet is available for download from https://pd4ml.com/net/:

Download
Pd4ml.Net.1.0.0.nupkg the library, a NuGet package
Pd4ml.Net.Tool.1.0.0.nupkg the pd4ml command-line tool, a .NET tool package
source archive the projects, tests and this guide, to build or reference yourself

Save the packages in a folder, for example C:\pd4ml-net (any folder works; a team can use a shared folder or its own NuGet feed), and add the library to your project from there:

dotnet add package Pd4ml.Net --source C:\pd4ml-net

To restore from that folder later without --source, register it once: dotnet nuget add source C:\pd4ml-net --name pd4ml. New versions are published on the same page; dotnet add package Pd4ml.Net --source C:\pd4ml-net again picks up the most recent one.

With the source archive instead, reference the project directly:

dotnet add reference pd4ml-dotnet/src/Pd4ml.Net/Pd4ml.Net.csproj

Everything is in the Pd4mlNet namespace: using Pd4mlNet;.

2.2 Install PD4ML

With the pd4ml command-line tool, installed from the downloaded tool package:

dotnet tool install --global Pd4ml.Net.Tool --add-source C:\pd4ml-net
pd4ml install

(From the source archive, without installing the tool: dotnet run --project pd4ml-dotnet/src/Pd4ml.Cli -- install.)

pd4ml install does the following:

  • downloads the most recent PD4ML 4.x release from the PD4ML Maven repository (https://pd4ml.com/maven2);
  • 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-dotnet on Windows, ~/.cache/pd4ml-dotnet elsewhere.

The library then uses that build automatically. Run the command again to update.

Option
(none) the most recent 4.x release
--snapshots the most recent 4.x build, including development snapshots
pd4ml install 4.0.23fx2 an exact version
--jakarta the -jakarta variant, for Jakarta EE / Servlet 6 environments
--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)
pd4ml list list the available versions

You can do the same from code, for example at application start:

using Pd4mlNet;

Installer.Install();                          // downloads only when that build is not there yet
Console.WriteLine(Installer.InstalledVersion());

To check the installation, run pd4ml version.

Services and web servers: the account the application runs as must be able to see the installed build. Either install it as that account, or install into a shared directory with --home and set PD4ML_HOME for the application too. On IIS, the application pool identity has its own %LOCALAPPDATA%.

2.3 Other ways to provide PD4ML

The library looks for PD4ML in this order:

  1. Bridge.Configure(o => o.Classpath = new[] { ... }), called before the first conversion;
  2. the PD4ML_CLASSPATH environment variable: jars and class directories separated by ; (Windows) or : (elsewhere), with dir/* for all jars of a directory;
  3. the PD4ML_CLASSPATH_FILE environment variable: a file with one entry per line;
  4. the build that pd4ml install downloaded.

Typical reasons to use them:

  • Offline or air-gapped servers: run pd4ml install --home ./pd4ml-libs on a machine with network access, copy the directory, and set PD4ML_HOME on the target. Alternatively, ship the directory with your application and point PD4ML_HOME at it.
  • A licensed or specific jar: PD4ML_CLASSPATH=/opt/pd4ml/pd4ml.jar:/opt/pd4ml/lib/*.
  • A PD4ML development build (for the v42 layout engine, see 4.3): put a classpath file listing the build's classes and its Maven dependencies in PD4ML_CLASSPATH_FILE.

3. Quick start

using Pd4mlNet;

var pd = new PD4ML();
pd.SetPageSize(PageSize.A4);
pd.ReadHtmlString("<h1>Hello, PDF</h1><p>Generated with <b>PD4ML</b>.</p>");
pd.WritePDF("hello.pdf");

Or in one call:

using System.Text;
using Pd4mlNet;

byte[] pdf = Pdf.HtmlToPdf(Encoding.UTF8.GetBytes("<h1>Invoice 42</h1>"));
Console.WriteLine(Encoding.ASCII.GetString(pdf, 0, 5));     // %PDF-

4. How it works

4.1 The two-phase model

A document is read once and written any number of times:

  • ReadHTML parses the document and lays it out;
  • each WritePDF, WriteDOCX, WriteRTF or RenderAsImages call then produces one output from that same layout.

Settings (page size, headers, fonts...) go before ReadHTML. See the two-phase model.

using Pd4mlNet;

var pd = new PD4ML();
pd.ReadHtmlString("<h1>Quarterly report</h1>");
pd.WritePDF("report.pdf");
pd.WriteDOCX("report.docx");
byte[][] thumbnails = pd.RenderPng();     // PNG images, one per page

4.2 The bridge

  • One bridge per application (default). The first use of PD4ML starts a Java bridge process. It serves every conversion of the .NET process, from any thread, and ends with it. Starting Java takes roughly a second. After that, converting an ordinary document takes milliseconds.

  • Thread safety. Calls to the bridge are serialized. Use one PD4ML object per document (each thread its own) and convert from as many threads as you like. The conversions run one at a time, so for parallel throughput use several processes or several bridges (see below).

  • Shared server. Several processes, or short-lived ones (Azure Functions, CLI invocations), can use one long-running bridge instead of each starting Java:

    pd4ml serve --port 8723          # keep it running (a Windows service, systemd, a sidecar container)
    

    Then set PD4ML_BRIDGE=tcp://127.0.0.1:8723 for the application, or call Bridge.Configure(o => o.Server = "tcp://127.0.0.1:8723"). The server listens on the loopback interface only, and each connection has its own objects.

  • JVM options (memory, system properties): set PD4ML_JVM_OPTIONS="-Xmx2g", or call Bridge.Configure(o => o.JvmOptions.Add("-Xmx2g")) before the first use. Java runs headless.

  • The bridge's console output is discarded by default. To keep it, set PD4ML_BRIDGE_LOG=path or o.LogFile.

  • Bridge.Shutdown() stops the bridge; the next use starts it again. A new Bridge(options) creates a separate bridge, with its own Java process and classpath, for low-level use.

4.3 Layout engines

PD4ML 4.2 introduces a second HTML layout engine, v42, written to match current browsers closely (flexbox, grid, modern CSS). new PD4ML() uses it by default:

using Pd4mlNet;

if (Runtime.HasLayoutEngines)
{
    var pd = new PD4ML();                          // v42
    var legacy = new PD4ML(LayoutEngine.v40);      // the previous engine
    Console.WriteLine($"{pd.Engine} {legacy.Engine}");
}

Builds before 4.2 have one engine only; this includes the current public release, 4.1.x. With such a build:

  • the library uses that build's engine and traces a warning once (System.Diagnostics.Trace);
  • Runtime.HasLayoutEngines returns false;
  • asking for an engine explicitly throws InvalidOperationException.

4.4 .NET and Java values

  • Java methods appear in PascalCase: setPageSize becomes SetPageSize, getVersion becomes GetVersion. Static Java fields keep their names (PageSize.A4, Constants.AllowPrint, LayoutEngine.v42); instance fields become properties (PageSize.A4.Width).

  • Java types map to .NET types:

    Java .NET
    String string
    int / long / float / boolean int / long / float / bool
    byte[] byte[]
    InputStream Stream
    OutputStream any Stream
    URL Uri
    File FileInfo
    Map IDictionary<string, string>
    PD4ML classes the wrappers
    any other object JavaObject
  • Java objects that have no wrapper are JavaObjects. Call them with obj.Call("method", args) or obj.Call<T>(...), or through dynamic:

    using Pd4mlNet;
    
    dynamic locale = Java.Class("java.util.Locale").Call("getDefault");
    Console.WriteLine((string)locale.getDisplayName());
    
  • Java.Class("name") gives you any Java class: .New(...) calls a constructor, .Call(...) a static method, and .Get(...) reads a static field. With dynamic, write Java's names as they are (map.put(...)).

  • ToString(), Equals() and GetHashCode() call the Java methods of the same name. Java objects are released when their .NET wrappers are garbage-collected.

4.5 Exceptions

Java exceptions are thrown as JavaException, which carries the Java class and its superclasses:

using Pd4mlNet;

try
{
    var pd = new PD4ML();
    pd.ReadHTML("file:///no/such/document.html");
    pd.WritePDF();
}
catch (JavaException e) when (e.IsInstanceOf("java.io.IOException"))
{
    Console.WriteLine($"cannot read the document: {e.JavaMessage}");
}

e.JavaClass, e.Causes and e.JavaTrace hold the details. Other exception types:

Exception Thrown when
BridgeException the bridge itself fails: no Java, no PD4ML build, or the process ended
CallbackException a .NET callback throws, such as a progress listener; the original exception is the InnerException
InstallException downloads fail

5. Use cases

The snippets below are complete and runnable as C# scripts or in a Main method. Files they read (report.html...) are assumed to exist in the working directory.

5.1 HTML string to PDF

This is the common case for web applications: render HTML with your template engine (Razor, Scriban, a StringBuilder) and convert it in memory.

using Pd4mlNet;

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

var pd = new PD4ML();
pd.ReadHtmlString(html);
byte[] pdf = pd.WritePDF();               // the PDF as bytes

WritePDF, WriteDOCX and WriteRTF have three forms:

  • with no argument, they return the document as byte[];
  • given a file path, they write the document to that file;
  • given a Stream, they write into it (a FileStream, MemoryStream or Response.Body).

5.2 Files and URLs

using Pd4mlNet;

var pd = new PD4ML();
pd.ReadHTML("report.html");               // also: "https://example.com/report", a Uri, a FileInfo
pd.WritePDF("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, byte[] or a stream, pass the base location. Otherwise images and stylesheets go missing without an error (see reading the source):

using Pd4mlNet;

var html = "<link rel='stylesheet' href='style.css'><p class='note'>Styled</p>";
var pd = new PD4ML();
pd.ReadHtmlString(html, ".");             // a directory, file path or URL
pd.WritePDF("styled.pdf");

For a document in a known encoding other than UTF-8, call pd.ReadHTML(bytes, baseUrl, "windows-1251"). ReadHTML(Stream, baseUrl, encoding) works the same way for streams.

5.3 Page size, orientation, margins and scaling

using Pd4mlNet;

var pd = new PD4ML();
pd.SetPageSize(PageSize.LETTER.Rotate());                       // landscape
pd.SetPageMargins(new PageMargins(15, 20, 15, 20, Units.MM));   // left, top, right, bottom
pd.SetHtmlWidth(1000);              // the virtual browser width the page is laid out in
pd.ReadHtmlString("<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, so a smaller HTML width makes everything larger. Related controls:

  • AdjustHtmlWidth(true) lays the page out at the document's own natural width.
  • FitPageVertically(Constants.CENTER_ALIGN) shrinks the whole document onto a single page.
  • SetPageSize and SetPageMargins accept a page scope ("1", "2+", "odd"...) as a second argument.

More in page layout.

For a custom size, use new PageSize(100f, 150f, Units.MM). For a named size, use PageSize.GetInstance("a5") (the name in lower case).

5.4 Headers, footers and page numbers

Headers and footers are HTML fragments. PD4ML fills in $[page], $[total] and $[title] on each page, and SetDynamicData adds placeholders of your own. In C#, write them in a verbatim string, or in an ordinary (not interpolated) string:

using System.Linq;
using Pd4mlNet;

var pd = new PD4ML();
pd.SetDynamicData(new Dictionary<string, string> { ["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.ReadHtmlString("<title>Invoice</title><h1>Invoice</h1>" + string.Concat(Enumerable.Repeat("<p>Line item</p>", 80)));
pd.WritePDF("invoice.pdf");

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

5.5 Watermarks and page backgrounds

using Pd4mlNet;

var pd = new PD4ML();
pd.SetWatermark("<span style='font-size:72pt; color:#c00'>DRAFT</span>",
    120f, 350f,     // left, top (px)
    0.15f,          // opacity
    45f,            // rotation, degrees
    1f,             // scale
    true, true,     // visible on screen, in print
    "1+");          // pages
pd.SetPageBackground("<div style='background:#f7f7f2; height:100%'></div>", "1+");
pd.ReadHtmlString("<h1>Proposal</h1>");
pd.WritePDF("draft.pdf");

5.6 Fonts

PD4ML's built-in fonts cover Unicode broadly. For a specific typeface (a corporate font, CJK coverage), give it a font directory:

using Pd4mlNet;

var pd = new PD4ML();
pd.UseTTF(@"C:\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 the fonts on every conversion (font indexing):

java -jar <pd4ml jar> -configure.fonts C:\fonts

Installer.InstalledJar() returns the jar's path. CSS @font-face web fonts in the document work as well.

using Pd4mlNet;

var pd = new PD4ML();
pd.GenerateBookmarksFromHeadings(true);    // one PDF bookmark per <h1>..<h6>
pd.EnableHyperlinks(true);                 // <a href> become clickable links (the default)
pd.ReadHtmlString("<h1>Introduction</h1><p>...</p><h1 style='page-break-before:always'>Details</h1>");
pd.WritePDF("bookmarked.pdf");

PD4ML's <pd4ml:toc> tag generates a table of contents with page numbers from the headings; see the usage examples. For explicit page breaks, use CSS page-break-before / break-before, or <pd4ml:page.break>.

5.8 PDF/A and PDF/UA

using Pd4mlNet;

var pd = new PD4ML();
pd.ReadHtmlString("<html lang='en'><title>Annual report</title><h1>Annual report</h1>");
pd.WritePDF("archive.pdf", PdfSpec.PDFA_3B);                          // archival
pd.WritePDF("accessible.pdf", PdfSpec.PDFUA_1);                       // accessible
pd.WritePDF("both.pdf", PdfSpec.PDF_1_7.Combine(PdfSpec.PDFA_2A));

var status = pd.GetLastRenderInfo(Constants.PD4ML_PDFA_STATUS);      // after writing

You can also name the spec with a keyword: Runtime.PdfSpecFor("pdf/a-3b"). PdfSpec.ListKeywords() lists the keywords. An unknown keyword throws ArgumentException; PdfSpec.GetInstance alone would fall back to PDF 1.7 without an error.

  • PDF/A embeds all fonts.
  • PDF/UA needs well-structured HTML: real headings, alt texts, lang, table headers. Combine it with pd.SetErrorPolicy(ErrorPolicy.PEDANTIC) to be told when the source does not satisfy the standard.

Details: PDF/A, PDF/UA and e-invoicing.

5.9 ZUGFeRD / Factur-X e-invoices

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

using Pd4mlNet;

var pd = new PD4ML();
pd.ReadHTML("invoice.html");          // contains the <pd4ml:attachment> of factur-x.xml
pd.WritePDF("invoice.pdf", PdfSpec.FacturX);

5.10 Passwords and permissions

using Pd4mlNet;

var pd = new PD4ML();
pd.SetPermissions("owner-secret", Constants.AllowPrint | Constants.AllowCopy, Constants.AES_256BIT);
pd.ReadHtmlString("<p>Confidential</p>");
pd.WritePDF("protected.pdf");

Combine the Allow* flags with |, or start from Constants.DefaultPermissions and remove flags with & ~. Encryption needs the Bouncy Castle jars, which pd4ml install installs. See permissions and encryption.

5.11 Word (DOCX, RTF) and images

using Pd4mlNet;

var pd = new PD4ML();
pd.ReadHtmlString("<h1>Letter</h1><p>Dear customer, ...</p>");
pd.WriteDOCX("letter.docx");
pd.WriteRTF("letter.rtf");
byte[][] pages = pd.RenderPng();                               // PNG per page
byte[][] tiffs = pd.RenderAsImages(Constants.TIFF);            // one TIFF per page
pd.RenderAsImages(".", "page", Constants.PNG24);               // page files in a directory

The image formats are PNG8, PNG24, TIFF and TIFF_COMPRESSED (constants in Constants). The v42 engine does not support the multi-page TIFF formats yet; use the v40 engine for them.

With the v42 engine, DOCX/RTF output can be made editable: positioned content becomes text boxes and form fields instead of pictures. To enable it, call pd.SetParam(Constants.PD4ML_WORD_EDITABLE, "true").

5.12 Interactive forms and multi-column layout

using System.Linq;
using Pd4mlNet;

var pd = new PD4ML();
pd.GenerateForms(true, null);         // <input>/<select>/<textarea> become PDF form fields
pd.ReadHtmlString("<form>Name: <input name='name' size='30'></form>");
pd.WritePDF("form.pdf");

var news = new PD4ML();
news.GenerateMulticolumn(2, 20, true); // two columns, 20 mm apart
news.ReadHtmlString("<h1>Newsletter</h1>" + string.Concat(Enumerable.Repeat("<p>Lorem ipsum dolor sit amet.</p>", 200)));
news.WritePDF("newsletter.pdf");

See forms and multi-column pages. Multi-column pages need PD4ML 4.0.24 / 4.1.2 or later.

5.13 Adding CSS and HTML programmatically

using Pd4mlNet;

var pd = new 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.ReadHtmlString("<h1>Report</h1>");
pd.WritePDF("report-styled.pdf");

AddStyle also takes the URL of a stylesheet: pd.AddStyle(new Uri(Path.GetFullPath("print.css")), true). See injecting HTML and adding styles.

5.14 Progress, diagnostics and metadata

using Pd4mlNet;

var pd = new PD4ML();
pd.MonitorProgressWith((messageId, percent, note, msec) =>
    Console.WriteLine($"{percent,3}% {note} ({msec} ms)"));
pd.SetErrorPolicy(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.ReadHtmlString("<h1>Statement</h1>");
pd.WritePDF("statement.pdf");
int pages = Convert.ToInt32(pd.GetLastRenderInfo(Constants.PD4ML_TOTAL_PAGES));

The callback runs in .NET while the conversion runs in Java. It may call PD4ML itself. An exception it throws comes out of the call that caused it, wrapped in a CallbackException. The v42 engine currently reports fewer progress stages than v40. More in extensibility and diagnostics.

5.15 Working with existing PDFs

using System.Text;
using Pd4mlNet;

byte[] cover = Pdf.HtmlToPdf(Encoding.UTF8.GetBytes("<h1 style='margin-top:300px; text-align:center'>Annual Report</h1>"));
byte[] body = Pdf.HtmlToPdf(Encoding.UTF8.GetBytes("<h2>Chapter 1</h2><p>...</p>"));

var doc = Pdf.Open(cover);                // byte[], a Stream, a path or a URL (+ password)
doc.Append(Pdf.Open(body));               // pages of another document after these
doc.SetTitle("Annual Report");
Pdf.Save(doc, "annual-report.pdf");

byte[] stamp = Pdf.HtmlToPdf(Encoding.UTF8.GetBytes("<p style='color:#c00; font-size:40pt'>COPY</p>"));
var original = Pdf.Open("annual-report.pdf");
original.Overlay(Pdf.Open(stamp), 128);   // draw over every page, half opaque (0..255)
Pdf.Save(original, "annual-report-copy.pdf", "secret");

Console.WriteLine(Pdf.Open("annual-report.pdf").GetNumberOfPages());

Pdf.Write(doc) returns the document as byte[]. Other operations:

  • Underlay draws another document under the pages (a letterhead, for example).
  • Pdf.Merge(a, b) appends two documents in one call.
  • DeletePage, GetPageSize, GetMetadata and DumpOutlines inspect and edit a document.

PD4ML's PDF toolkit (signing, optimisation, XFDF) is described in the PDF post-processing toolkit and is reachable through Java.Class(...).

5.16 Web applications

ASP.NET Core minimal API:

using Pd4mlNet;

var app = WebApplication.Create(args);

app.MapGet("/invoice/{number:int}.pdf", (int number, IWebHostEnvironment env) =>
{
    var pd = new PD4ML();
    pd.ReadHtmlString(RenderInvoiceHtml(number), env.WebRootPath);   // base: where images/CSS live
    return Results.File(pd.WritePDF(), "application/pdf", $"invoice-{number}.pdf");
});

app.Run();

An MVC controller, with the HTML from a Razor view rendered to a string (for example with IRazorViewEngine, or a library such as RazorLight):

public async Task<IActionResult> Invoice(int number)
{
    string html = await _viewRenderer.RenderAsync("Invoice", await _invoices.GetAsync(number));
    var pd = new PD4ML();
    pd.ReadHtmlString(html, _env.WebRootPath);
    return File(pd.WritePDF(), "application/pdf");
}

Conversions are synchronous and serialized through the bridge. In ASP.NET Core you can run them on the request thread, or with await Task.Run(...) for long documents. The bridge starts with the first request. To pay that cost at startup instead, touch PD4ML when the application starts (_ = Runtime.Version;).

5.17 Batch conversion

using Pd4mlNet;

foreach (var source in Directory.GetFiles(".", "*.html"))
{
    var pd = new PD4ML();                    // one converter per document
    pd.ReadHTML(source);
    pd.WritePDF(Path.ChangeExtension(source, ".pdf"));
    Console.WriteLine($"written {source}");
}

One bridge serves the whole loop. To convert truly in parallel, run several worker processes or give workers their own Bridge instances (a low-level API).

5.18 Licensing

using Pd4mlNet;

var pd = new PD4ML("YOUR-SERIAL-NUMBER");   // or a license file URL; or pd4ml.lic in the working dir / classpath
if (pd.IsDemoMode())
    Console.WriteLine("unlicensed: output is watermarked");
Console.WriteLine($"PD4ML {Runtime.Version}{(pd.IsPro() ? " pro" : "")}");

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


6. Tips and hints

  • Start the bridge early. The first use starts Java, which takes about a second. Touch PD4ML at application start, or use a bridge server for short-lived processes.
  • One converter per document. new PD4ML() is cheap; reuse the bridge, not the object.
  • Set the base location for HTML from strings. Missing images and unstyled output are almost always caused by unresolved relative links (details).
  • Settings go 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. They are 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. Use SetParam with Constants.PD4ML_ALLOWED_RESOURCE_LOCATION and PD4ML_DISABLE_EXTERNAL_ATTACHMENTS.
  • Pin the build in production. Use pd4ml install <version> or a PD4ML_CLASSPATH to a jar you keep, rather than following the latest release.
  • Containers: install the build when you build the image (pd4ml install --home /opt/pd4ml), and set PD4ML_HOME=/opt/pd4ml.

7. Troubleshooting

Symptom Cause and remedy
BridgeException: PD4ML not found No build is configured. Run pd4ml install as the account the application runs as, or set PD4ML_CLASSPATH.
BridgeException: cannot start java / "closed the connection" Java is missing or failed. Check java -version, and set JAVA_HOME or PD4ML_JAVA. PD4ML_BRIDGE_LOG shows Java's output.
BridgeException: cannot connect The bridge server is not running, or PD4ML_BRIDGE names another port.
BridgeException: ... Configure() before the first use Bridge.Configure was called after PD4ML had been used. Call it first, or call Bridge.Shutdown() first.
Images or styles missing Relative links have no base location, or the server cannot reach the resources (proxies, authentication). See SetCookie and SetHttpOptions.
Output is watermarked No license is installed (licensing).
Squares instead of characters The fonts lack those glyphs. Provide fonts with UseTTF (fonts).
NoClassDefFoundError: org/bouncycastle/... with SetPermissions The Bouncy Castle jars are missing. Install without --no-bouncycastle, or add them to the classpath.
Warning "has no v42 layout engine" The PD4ML build predates 4.2, so the conversion uses its own engine. Use a 4.2 build for v42.
JavaException: ... NoSuchMethodException Only with JavaObject.Call / dynamic: no Java method of that name takes those argument types. Pass floats as 10f.

8. API reference

.NET classes

Class
PD4ML the converter: new PD4ML(), new PD4ML(license, htmlWidth, engine), new PD4ML(LayoutEngine); every Java method of com.pd4ml.PD4ML
PageSize, PageMargins, Units, Dimensions, Constants, LayoutEngine, ErrorPolicy, PdfSpec, PdfDocument, ProgressListener, StatusMessage, ResourceProvider, CustomTag typed wrappers of the PD4ML Java classes (docs/api.md in the source archive from pd4ml.com/net)
Pdf HtmlToPdf(source, pageSize, margins, spec), Open(source, password), Write(doc, password, permissions), Save(doc, path, ...), Merge(a, b)
Runtime Version, Install(), Configure(), HasLayoutEngines, ToUrl(), PdfSpecFor()
Installer Install(version, options), InstalledJar(), InstalledClasspath(), InstalledVersion(), AvailableVersions(), SelectVersion()
Bridge, BridgeOptions Configure(), Default, Shutdown(), Command(); low-level New, Call, CallStatic, GetField, Proxy
Java, JavaClass, JavaObject any Java class or object, with dynamic support; Java.Proxy(interface, callback)
JavaException, BridgeException, CallbackException, InstallException errors

PD4ML methods with .NET additions

Method
ReadHTML(string pathOrUrl, encoding), ReadHTML(Uri), ReadHTML(FileInfo) a document from a path or URL
ReadHTML(byte[] html, baseUrl, encoding), ReadHTML(Stream html, baseUrl, encoding) a document from memory
ReadHtmlString(string html, baseUrl) the document as a string
WritePDF() → byte[], WritePDF(PdfSpec), WritePDF(string path, PdfSpec?), WritePDF(Stream, PdfSpec?) PDF output
WriteDOCX(...), WriteRTF(..., bool rtfWmf) the same forms as WritePDF
RenderAsImages(format) → byte[][], RenderPng(), RenderAsImages(dir, name, format) page images
MonitorProgressWith(Action<int, int, string?, long>) progress callback
UseHtmlRenderer(string or LayoutEngine, htmlWidth, parameters), Engine the layout engine

All other methods are those of the Java class com.pd4ml.PD4ML (docs/api.md in the source archive from pd4ml.com/net, Java API).

ResourceProvider and CustomTag are extended in Java: register yours with AddCustomResourceProvider / AddCustomTagHandler, and put its classes on the classpath. Java interfaces (such as ProgressListener) can be implemented in .NET with Java.Proxy(). Examples: resource loaders, custom tags.

9. Further reading