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-dotnetlets .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-dotneton Windows,~/.cache/pd4ml-dotnetelsewhere.
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
--homeand setPD4ML_HOMEfor 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:
Bridge.Configure(o => o.Classpath = new[] { ... }), called before the first conversion;- the
PD4ML_CLASSPATHenvironment variable: jars and class directories separated by;(Windows) or:(elsewhere), withdir/*for all jars of a directory; - the
PD4ML_CLASSPATH_FILEenvironment variable: a file with one entry per line; - the build that
pd4ml installdownloaded.
Typical reasons to use them:
- Offline or air-gapped servers: run
pd4ml install --home ./pd4ml-libson a machine with network access, copy the directory, and setPD4ML_HOMEon the target. Alternatively, ship the directory with your application and pointPD4ML_HOMEat 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:
ReadHTMLparses the document and lays it out;- each
WritePDF,WriteDOCX,WriteRTForRenderAsImagescall 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
PD4MLobject 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:8723for the application, or callBridge.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 callBridge.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=pathoro.LogFile.Bridge.Shutdown()stops the bridge; the next use starts it again. Anew 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.HasLayoutEnginesreturns false;- asking for an engine explicitly throws
InvalidOperationException.
4.4 .NET and Java values
Java methods appear in PascalCase:
setPageSizebecomesSetPageSize,getVersionbecomesGetVersion. 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 Stringstringint/long/float/booleanint/long/float/boolbyte[]byte[]InputStreamStreamOutputStreamany StreamURLUriFileFileInfoMapIDictionary<string, string>PD4ML classes the wrappers any other object JavaObjectJava objects that have no wrapper are
JavaObjects. Call them withobj.Call("method", args)orobj.Call<T>(...), or throughdynamic: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. Withdynamic, write Java's names as they are (map.put(...)).ToString(),Equals()andGetHashCode()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 (aFileStream,MemoryStreamorResponse.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.SetPageSizeandSetPageMarginsaccept 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] — $[customer] — 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.
5.7 Bookmarks, links and tables of contents
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,
alttexts,lang, table headers. Combine it withpd.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:
Underlaydraws another document under the pages (a letterhead, for example).Pdf.Merge(a, b)appends two documents in one call.DeletePage,GetPageSize,GetMetadataandDumpOutlinesinspect 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 layoutReadHTMLmakes; writes afterwards only choose the output format. - Write several formats from one read. PDF, DOCX and page images from a single
ReadHTMLcost one layout. - Print CSS works.
@page { size: A4 landscape; margin: 15mm },@media printandpage-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
SetHtmlWidthto the width the page was designed for. - Index fonts once with
-configure.fonts, instead of pointingUseTTFat a large unindexed directory on every conversion. - Restrict what documents can load when converting untrusted HTML. Use
SetParamwithConstants.PD4ML_ALLOWED_RESOURCE_LOCATIONandPD4ML_DISABLE_EXTERNAL_ATTACHMENTS. - Pin the build in production. Use
pd4ml install <version>or aPD4ML_CLASSPATHto 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 setPD4ML_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
- PD4ML documentation and the full programmer's manual
