pd4ml 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/docs/guide.md CHANGED
@@ -1,782 +1,937 @@
1
- # PD4ML for Ruby — User Guide
2
-
3
- `pd4ml` brings [PD4ML](https://pd4ml.com) to Ruby: it converts HTML and CSS into **PDF**,
4
- **PDF/A**, **PDF/UA**, **ZUGFeRD/Factur-X** invoices, **DOCX**, **RTF** and **page images**, and
5
- gives Ruby applications the complete public PD4ML API.
6
-
7
- PD4ML is a Java library. The gem runs it in a small Java *bridge* process that Ruby talks to.
8
- Every PD4ML class and method can be called from Ruby, by its Java name or Ruby-style
9
- (`set_page_size`), and the gem adds Ruby I/O on top: paths, binary strings and IO objects
10
- instead of Java streams.
11
-
12
- The PD4ML manual, which this guide refers to throughout, is at <https://pd4ml.com/docs/>.
13
- The generated reference of every class and method is [api.md](api.md).
14
-
15
- **Contents**
16
-
17
- 1. [Prerequisites](#1-prerequisites)
18
- 2. [Installation](#2-installation)
19
- 3. [Quick start](#3-quick-start)
20
- 4. [How it works](#4-how-it-works)
21
- 5. [Use cases](#5-use-cases)
22
- 6. [Tips and hints](#6-tips-and-hints)
23
- 7. [Troubleshooting](#7-troubleshooting)
24
- 8. [API reference](#8-api-reference)
25
- 9. [Further reading](#9-further-reading)
26
-
27
- ---
28
-
29
- ## 1. Prerequisites
30
-
31
- | Requirement | Notes |
32
- |---|---|
33
- | **Ruby 3.1 or later** | MRI on any platform Ruby and Java run on. |
34
- | **Java 8 or later** | A JDK or JRE; an LTS release (17, 21) is recommended. The gem finds it through `PD4ML_JAVA`, then `JAVA_HOME`, then `java` on the `PATH`. |
35
- | **PD4ML** | Downloaded automatically on first use, or ahead of time with `pd4ml install` (see below); or any PD4ML build you point the gem at. |
36
- | **Network access** | Only for downloading PD4ML. Conversions need none, unless the HTML references remote resources. |
37
- | **A PD4ML license** | Optional for evaluation. Without one, PD4ML runs in evaluation mode and watermarks its output. See [Licensing](https://pd4ml.com/docs/#3.-licensing) and [5.18](#518-licensing). |
38
-
39
- Check Java with `java -version`. On a server without Java, install a JRE package (for example
40
- `apt install openjdk-21-jre-headless`) and, if it is not on the `PATH`, set `JAVA_HOME`. In
41
- containers, use an image with both Ruby and Java, or add a JRE to the Ruby image.
42
-
43
- ## 2. Installation
44
-
45
- ### 2.1 Install the gem
46
-
47
- With Bundler, add the gem to the `Gemfile` and run `bundle install`:
48
-
49
- <!-- no-test -->
50
- ```ruby
51
- gem 'pd4ml', git: 'https://github.com/zxfr/pd4ml-ruby.git'
52
- ```
53
-
54
- Without Bundler, download the gem from <https://github.com/zxfr/pd4ml-ruby/releases> and
55
- install it:
56
-
57
- ```
58
- gem install pd4ml-1.0.0.gem
59
- ```
60
-
61
- The gem has no gem dependencies and installs the `pd4ml` command.
62
-
63
- ### 2.2 Install PD4ML
64
-
65
- The gem contains the Ruby library and the bridge only. The PD4ML library itself comes from the
66
- PD4ML Maven repository (<https://pd4ml.com/maven2>), in one of two ways.
67
-
68
- **Automatically, on first use.** If no PD4ML build is installed or configured
69
- ([2.3](#23-other-ways-to-provide-pd4ml)), the first use of PD4ML in a process downloads the most
70
- recent PD4ML 4.x release before starting Java. A warning on standard error reports it. So once the gem
71
- is installed, this is all it takes:
72
-
73
- <!-- no-test -->
74
- ```ruby
75
- require 'pd4ml'
76
-
77
- pdf = Pd4ml.html_to_pdf('<h1>Hello</h1>'.b) # the first call downloads PD4ML (once)
78
- ```
79
-
80
- **Ahead of time, with the `pd4ml` command.** This is better for servers, CI and container
81
- images, where a download in the middle of a request is unwelcome:
82
-
83
- ```
84
- pd4ml install # with Bundler: bundle exec pd4ml install
85
- ```
86
-
87
- Either way, the gem:
88
-
89
- - downloads the most recent PD4ML 4.x release;
90
- - downloads the Bouncy Castle jars PD4ML needs for encryption and signing, from Maven Central;
91
- - checks every file against its published SHA-1 checksum;
92
- - stores them in a per-user cache directory: `%LOCALAPPDATA%\pd4ml-ruby` on Windows,
93
- `~/.cache/pd4ml-ruby` elsewhere, or `PD4ML_HOME`.
94
-
95
- The gem then uses that build. Later runs do not check for newer PD4ML releases: run
96
- `pd4ml install` again to update. Updating the gem does not change the PD4ML build.
97
-
98
- | Option | |
99
- |---|---|
100
- | *(none)* | the most recent 4.x release, or an update to it |
101
- | `--snapshots` | the most recent 4.x build, including development snapshots |
102
- | `pd4ml install 4.0.23fx2` | an exact version |
103
- | `--jakarta` | the `-jakarta` variant (for Jakarta EE / Servlet 6 environments) |
104
- | `--home DIR` | install into `DIR` (default: `PD4ML_HOME`, else the cache directory) |
105
- | `--force` | download again |
106
- | `--no-bouncycastle` | skip the Bouncy Castle jars (encryption and signing will not work) |
107
- | `pd4ml list` | list the available versions |
108
-
109
- The same is available from Ruby, which suits an application's boot (a Rails initializer):
110
-
111
- <!-- no-test -->
112
- ```ruby
113
- require 'pd4ml'
114
-
115
- Pd4ml.install # downloads only when that build is not there yet
116
- puts Pd4ml.installed_version, Pd4ml.installed_classpath
117
- ```
118
-
119
- To check the installation, run `pd4ml version`.
120
-
121
- **Turning the automatic download off.** Set `PD4ML_AUTO_INSTALL=0`, for example on servers that
122
- must not reach the internet, or where the build is provisioned separately. The gem then raises
123
- `Pd4ml::BridgeError` until a build is installed or configured. The automatic download never runs
124
- when a build is configured through [2.3](#23-other-ways-to-provide-pd4ml).
125
-
126
- > **Services and containers**: the account the application runs as must see the installed
127
- > build. Install it as that account, or install into a shared directory and set `PD4ML_HOME`
128
- > for the application too. In a Dockerfile:
129
- >
130
- > ```
131
- > RUN bundle install && PD4ML_HOME=/opt/pd4ml bundle exec pd4ml install
132
- > ENV PD4ML_HOME=/opt/pd4ml PD4ML_AUTO_INSTALL=0
133
- > ```
134
-
135
- ### 2.3 Other ways to provide PD4ML
136
-
137
- The gem looks for PD4ML in this order:
138
-
139
- 1. `Pd4ml.configure(classpath: [...])`, called before the first conversion;
140
- 2. the `PD4ML_CLASSPATH` environment variable: jars and class directories separated by `;`
141
- (Windows) or `:` (elsewhere), with `dir/*` for all jars of a directory;
142
- 3. the `PD4ML_CLASSPATH_FILE` environment variable: a file with one entry per line;
143
- 4. the build `pd4ml install` (or the first use) downloaded.
144
-
145
- Typical reasons to use them:
146
-
147
- - **Offline or air-gapped servers**: run `pd4ml install --home ./pd4ml-libs` on a machine with
148
- network access, copy the directory, and set `PD4ML_HOME` and `PD4ML_AUTO_INSTALL=0` on the
149
- target.
150
- - **A licensed or specific jar**: `PD4ML_CLASSPATH=/opt/pd4ml/pd4ml.jar:/opt/pd4ml/lib/*`.
151
- - **A PD4ML development build** (for the v42 layout engine, see [4.3](#43-layout-engines)): a
152
- classpath file listing the build's classes and its Maven dependencies, in
153
- `PD4ML_CLASSPATH_FILE`.
154
-
155
- ## 3. Quick start
156
-
157
- ```ruby
158
- require 'pd4ml'
159
-
160
- pd = Pd4ml::PD4ML.new
161
- pd.set_page_size(Pd4ml::PageSize::A4)
162
- pd.read_html_string('<h1>Hello, PDF</h1><p>Generated with <b>PD4ML</b>.</p>')
163
- pd.write_pdf('hello.pdf')
164
- ```
165
-
166
- Or in one call:
167
-
168
- ```ruby
169
- require 'pd4ml'
170
-
171
- pdf = Pd4ml.html_to_pdf('<h1>Invoice 42</h1>'.b)
172
- puts pdf[0, 5] # %PDF-
173
- ```
174
-
175
- ## 4. How it works
176
-
177
- ### 4.1 The two-phase model
178
-
179
- A document is **read once and written any number of times**:
180
-
181
- - `read_html` parses the document and lays it out;
182
- - each `write_pdf`, `write_docx`, `write_rtf` or `render_as_images` call then produces one output
183
- from that same layout.
184
-
185
- Settings (page size, headers, fonts...) go before `read_html`. See
186
- [the two-phase model](https://pd4ml.com/docs/#6.1-the-two-phase-model).
187
-
188
- ```ruby
189
- require 'pd4ml'
190
-
191
- pd = Pd4ml::PD4ML.new
192
- pd.read_html_string('<h1>Quarterly report</h1>')
193
- pd.write_pdf('report.pdf')
194
- pd.write_docx('report.docx')
195
- thumbnails = pd.render_png # PNG images, one binary String per page
196
- ```
197
-
198
- ### 4.2 The bridge and threads
199
-
200
- - **One bridge per process.** The first use of PD4ML starts a Java bridge process. It serves
201
- every conversion of the Ruby process, from any thread, and ends with it. Starting Java takes a
202
- fraction of a second; after that, converting an ordinary document takes milliseconds.
203
- - **Threads.** The bridge is thread-safe: use one `Pd4ml::PD4ML` per document (each thread its
204
- own) and convert from as many threads as you like (Puma, Sidekiq). The conversions themselves
205
- run one at a time, so for parallel throughput use several processes, or point them all at one
206
- bridge server (next item). Each Puma or Unicorn worker process starts its own bridge on first
207
- use; there is nothing to set up before forking.
208
- - **Shared bridge server.** Several processes, or short-lived ones (rake tasks, CLI tools), can
209
- use one long-running bridge instead of each starting Java:
210
-
211
- ```
212
- pd4ml serve --port 8723 # keep it running (systemd, a sidecar container, foreman)
213
- ```
214
-
215
- Then set `PD4ML_BRIDGE=tcp://127.0.0.1:8723`, or call
216
- `Pd4ml.configure(server: 'tcp://127.0.0.1:8723')`. The server listens on the loopback
217
- interface only, and each connection has its own objects.
218
- - **JVM options** (memory, system properties): set `PD4ML_JVM_OPTIONS="-Xmx2g"`, or call
219
- `Pd4ml.configure(jvm_options: ['-Xmx2g'])` before the first use. Java runs headless.
220
- - **The bridge's console output** is discarded unless `PD4ML_BRIDGE_LOG=path` (or
221
- `Pd4ml.configure(log_file: ...)`) keeps it. `Pd4ml.shutdown` stops the bridge; the next use
222
- starts it again.
223
-
224
- ### 4.3 Layout engines
225
-
226
- PD4ML 4.2 introduces a second HTML layout engine, **v42**, written to match current browsers
227
- closely (flexbox, grid, modern CSS). `Pd4ml::PD4ML.new` uses it by default:
228
-
229
- ```ruby
230
- require 'pd4ml'
231
-
232
- if Pd4ml.layout_engines?
233
- pd = Pd4ml::PD4ML.new # v42
234
- legacy = Pd4ml::PD4ML.new(engine: 'v40') # the previous engine
235
- puts pd.engine, legacy.engine
236
- end
237
- ```
238
-
239
- Builds before 4.2 have one engine only; this includes the current public release, 4.1.x. With
240
- such a build:
241
-
242
- - the gem uses that build's engine and warns once;
243
- - `Pd4ml.layout_engines?` returns false;
244
- - asking for an engine explicitly raises.
245
-
246
- ### 4.4 Ruby and Java values
247
-
248
- - **Method names**: Java methods can be called by their Java names (`setPageSize`, `readHTML`)
249
- or Ruby-style: `set_page_size`, `use_ttf` (for `useTTF`), `demo_mode?` (for `isDemoMode`).
250
- Ruby-style names are matched to the real Java names, acronyms included.
251
- - **Classes**: the PD4ML classes are Ruby classes in the `Pd4ml` module. Constants are Ruby
252
- constants (`Pd4ml::PageSize::A4`, `Pd4ml::Constants::AllowPrint`); lower-case static fields and
253
- static methods are class methods (`Pd4ml::LayoutEngine.v42`,
254
- `Pd4ml::PdfSpec.get_instance('pdf/a-2b')`); constructors use `new`. Any other Java class:
255
- `Pd4ml::Java.type('java.util.Locale')`.
256
- - **Objects**: a Java object comes back as a `Pd4ml::JavaObject` with all its Java methods.
257
- Public fields: `obj[:width]` and `obj[:width] = value`. `to_s` is Java's `toString()`,
258
- `enum_name` an enum constant's name, `java_class` the class name.
259
- - **Values**: Strings, Integers, Floats (`100.0` goes to Java as a float), `true`/`false`, `nil`,
260
- Symbols (as strings: `engine: :v40`), Arrays, Hashes (as Java `Map`s).
261
- - **Binary data**: a binary String (`File.binread`, `"...".b`, encoding ASCII-8BIT) goes to Java
262
- as `byte[]`; an IO is read. Java's `byte[]` comes back as a binary String. Text Strings (UTF-8)
263
- go to Java as text.
264
- - Java objects are released when Ruby garbage-collects them.
265
-
266
- ### 4.5 Exceptions
267
-
268
- Java exceptions are raised as `Pd4ml::JavaError`, which knows the Java class and its superclasses:
269
-
270
- ```ruby
271
- require 'pd4ml'
272
-
273
- begin
274
- pd = Pd4ml::PD4ML.new
275
- pd.read_html('file:///no/such/document.html')
276
- pd.write_pdf
277
- rescue Pd4ml::JavaError => e
278
- raise unless e.instance_of_java?('java.io.IOException')
279
-
280
- puts "cannot read the document: #{e.java_message}"
281
- end
282
- ```
283
-
284
- `e.java_class`, `e.causes` and `e.java_trace` hold the details. Other errors:
285
-
286
- | Error | Raised when |
287
- |---|---|
288
- | `Pd4ml::BridgeError` | the bridge itself fails: no Java, no PD4ML build, or the process ended |
289
- | `Pd4ml::CallbackError` | a Ruby block raised, such as a progress listener; the original error is `cause` |
290
- | `Pd4ml::InstallError` | downloading PD4ML fails |
291
-
292
- ---
293
-
294
- ## 5. Use cases
295
-
296
- The snippets below are complete and runnable. Files they read (`report.html`...) are assumed to
297
- exist in the working directory.
298
-
299
- ### 5.1 HTML string to PDF
300
-
301
- This is the common case for web applications: render HTML with your template engine (ERB, Haml,
302
- Slim, a Rails view) and convert it in memory.
303
-
304
- ```ruby
305
- require 'pd4ml'
306
-
307
- html = '<html><body><h1>Order 1001</h1><p>Thank you for your order.</p></body></html>'
308
-
309
- pd = Pd4ml::PD4ML.new
310
- pd.read_html_string(html)
311
- pdf = pd.write_pdf # the PDF as a binary String
312
- ```
313
-
314
- `write_pdf`, `write_docx` and `write_rtf` return the document as a binary String when given no
315
- target. Given a path (a String or Pathname) they write the file, given an IO (a `File`, a
316
- `StringIO`, a Rack body) they write into it.
317
-
318
- ### 5.2 Files and URLs
319
-
320
- ```ruby
321
- require 'pd4ml'
322
-
323
- pd = Pd4ml::PD4ML.new
324
- pd.read_html('report.html') # also: 'https://example.com/report', a Pathname, a URI
325
- pd.write_pdf('report.pdf')
326
- ```
327
-
328
- **Relative links** (`<img src="logo.png">`, `<link href="style.css">`) resolve against the
329
- document's own location when it is read from a path or URL. When the HTML comes from a string,
330
- binary String or IO, pass the base location. Otherwise images and stylesheets go missing without
331
- an error (see [reading the source](https://pd4ml.com/docs/#6.2-reading-the-source)):
332
-
333
- ```ruby
334
- require 'pd4ml'
335
-
336
- html = '<link rel="stylesheet" href="style.css"><p class="note">Styled</p>'
337
- pd = Pd4ml::PD4ML.new
338
- pd.read_html_string(html, '.') # a directory, file path or URL
339
- pd.write_pdf('styled.pdf')
340
- ```
341
-
342
- A text String that does not start with `<` is taken as a path or URL; content always goes in as a
343
- binary String (`File.binread`), an IO, or through `read_html_string`. For a document in a known
344
- encoding other than UTF-8, call `pd.read_html(File.binread(path), base, 'windows-1251')`.
345
-
346
- ### 5.3 Page size, orientation, margins and scaling
347
-
348
- ```ruby
349
- require 'pd4ml'
350
-
351
- pd = Pd4ml::PD4ML.new
352
- pd.set_page_size(Pd4ml::PageSize::LETTER.rotate) # landscape
353
- pd.set_page_margins(Pd4ml::PageMargins.new(15, 20, 15, 20, Pd4ml::Units::MM)) # left, top, right, bottom
354
- pd.set_html_width(1000) # the virtual browser width the page is laid out in
355
- pd.read_html_string("<table width='100%'><tr><td>wide content</td></tr></table>")
356
- pd.write_pdf('landscape.pdf')
357
- ```
358
-
359
- The layout is scaled to the page: `scale = printable width / html width`, so a smaller HTML width
360
- makes everything larger. Related controls:
361
-
362
- - `adjust_html_width(true)` lays the page out at the document's own natural width.
363
- - `fit_page_vertically(Pd4ml::Constants::CENTER_ALIGN)` shrinks the whole document onto a
364
- single page.
365
- - `set_page_size` and `set_page_margins` accept a page *scope* (`'1'`, `'2+'`, `'odd'`...) as a
366
- second argument.
367
-
368
- More in [page layout](https://pd4ml.com/docs/#7.-page-layout-size-margins-and-scaling).
369
-
370
- For a custom size, use `Pd4ml::PageSize.new(100.0, 150.0, Pd4ml::Units::MM)`. For a named size,
371
- use `Pd4ml::PageSize.get_instance('a5')` (the name in lower case).
372
-
373
- ### 5.4 Headers, footers and page numbers
374
-
375
- Headers and footers are HTML fragments. PD4ML fills in `$[page]`, `$[total]` and `$[title]` on
376
- each page, and `set_dynamic_data` adds placeholders of your own. `$[...]` is safe in both single-
377
- and double-quoted Ruby strings (only `#{...}` is interpolated).
378
-
379
- ```ruby
380
- require 'pd4ml'
381
-
382
- pd = Pd4ml::PD4ML.new
383
- pd.set_dynamic_data({ 'customer' => 'Acme Corp.', 'invoice' => 'INV-2026-0031' })
384
- pd.set_page_header("<div style='font-size:9pt'><b>$[title]</b></div>", 30, '1+')
385
- pd.set_page_footer(
386
- "<div style='font-size:8pt; text-align:right'>" \
387
- 'Invoice $[invoice] &mdash; $[customer] &mdash; page $[page] of $[total]</div>',
388
- 24, '1+'
389
- )
390
- pd.read_html_string("<title>Invoice</title><h1>Invoice</h1>#{'<p>Line item</p>' * 80}")
391
- pd.write_pdf('invoice.pdf')
392
- ```
393
-
394
- The second argument is the area height in pixels. The third is the page scope: `'1'` (first
395
- page), `'2+'` (all but the first), `'odd'`, `'even'`, `'3-7'`... You can also write headers and
396
- footers inline in the HTML with PD4ML's extension tags. See
397
- [headers, footers, watermarks and backgrounds](https://pd4ml.com/docs/#8.-headers-footers-watermarks-and-backgrounds)
398
- and the [usage examples](https://pd4ml.com/docs/usage-examples.html#set-page-header-and-footer).
399
-
400
- ### 5.5 Watermarks and page backgrounds
401
-
402
- ```ruby
403
- require 'pd4ml'
404
-
405
- pd = Pd4ml::PD4ML.new
406
- pd.set_watermark("<span style='font-size:72pt; color:#c00'>DRAFT</span>",
407
- 120.0, 350.0, # left, top (px)
408
- 0.15, # opacity
409
- 45.0, # rotation, degrees
410
- 1.0, # scale
411
- true, true, # visible on screen, in print
412
- '1+') # pages
413
- pd.set_page_background("<div style='background:#f7f7f2; height:100%'></div>", '1+')
414
- pd.read_html_string('<h1>Proposal</h1>')
415
- pd.write_pdf('draft.pdf')
416
- ```
417
-
418
- ### 5.6 Fonts
419
-
420
- PD4ML's built-in fonts cover Unicode broadly. For a specific typeface (a corporate font, CJK
421
- coverage), give it a font directory:
422
-
423
- <!-- no-test -->
424
- ```ruby
425
- require 'pd4ml'
426
-
427
- pd = Pd4ml::PD4ML.new
428
- pd.use_ttf('/opt/fonts', true) # a directory of TTF/OTF fonts (indexed: see below)
429
- pd.embed_ttfs(true, false) # embed only the glyphs used
430
- pd.apply_kerning(true)
431
- pd.enable_ligatures(true)
432
- ```
433
-
434
- Index a font directory once, ahead of time. PD4ML then reads the index (`pd4fonts.properties`)
435
- instead of scanning all the fonts on every conversion
436
- ([font indexing](https://pd4ml.com/docs/#4.3-font-indexing)):
437
-
438
- ```
439
- java -jar <pd4ml jar> -configure.fonts /opt/fonts
440
- ```
441
-
442
- `ruby -rpd4ml -e 'puts Pd4ml.installed_jar'` prints the jar's path. CSS `@font-face` web fonts in
443
- the document work as well.
444
-
445
- ### 5.7 Bookmarks, links and tables of contents
446
-
447
- ```ruby
448
- require 'pd4ml'
449
-
450
- pd = Pd4ml::PD4ML.new
451
- pd.generate_bookmarks_from_headings(true) # one PDF bookmark per <h1>..<h6>
452
- pd.enable_hyperlinks(true) # <a href> become clickable links (the default)
453
- pd.read_html_string("<h1>Introduction</h1><p>...</p><h1 style='page-break-before:always'>Details</h1>")
454
- pd.write_pdf('bookmarked.pdf')
455
- ```
456
-
457
- PD4ML's `<pd4ml:toc>` tag generates a table of contents with page numbers from the headings; see
458
- the [usage examples](https://pd4ml.com/docs/usage-examples.html#add-toc). For explicit page
459
- breaks, use CSS `page-break-before` / `break-before`, or `<pd4ml:page.break>`.
460
-
461
- ### 5.8 PDF/A and PDF/UA
462
-
463
- ```ruby
464
- require 'pd4ml'
465
-
466
- pd = Pd4ml::PD4ML.new
467
- pd.read_html_string("<html lang='en'><title>Annual report</title><h1>Annual report</h1>")
468
- pd.write_pdf('archive.pdf', spec: Pd4ml::PdfSpec::PDFA_3B) # archival
469
- pd.write_pdf('accessible.pdf', spec: Pd4ml::PdfSpec::PDFUA_1) # accessible
470
- pd.write_pdf('both.pdf', spec: Pd4ml::PdfSpec::PDF_1_7.combine(Pd4ml::PdfSpec::PDFA_2A))
471
- pd.write_pdf('keyword.pdf', spec: 'pdf/a-2b') # by keyword
472
-
473
- status = pd.get_last_render_info(Pd4ml::Constants::PD4ML_PDFA_STATUS) # after writing
474
- ```
475
-
476
- `Pd4ml::PdfSpec.list_keywords` lists the keywords; an unknown one raises `ArgumentError`
477
- (`PdfSpec.get_instance` alone would fall back to PDF 1.7 without an error).
478
-
479
- - PDF/A embeds all fonts.
480
- - PDF/UA needs well-structured HTML: real headings, `alt` texts, `lang`, table headers.
481
- Combine it with `pd.set_error_policy(Pd4ml::ErrorPolicy::PEDANTIC)` to be told when the source
482
- does not satisfy the standard.
483
-
484
- Details: [PDF/A, PDF/UA and e-invoicing](https://pd4ml.com/docs/#11.2-pdfa-pdfua-and-e-invoicing-formats).
485
-
486
- ### 5.9 ZUGFeRD / Factur-X e-invoices
487
-
488
- `PdfSpec::ZUGFeRD` and `PdfSpec::FacturX` select the PDF/A-3 container the standards require.
489
- The HTML attaches the invoice XML with PD4ML's attachment tag. The
490
- [ZUGFeRD and Factur-X page](https://pd4ml.com/docs/features/zugferd-and-factur-x-pdf-invoices.html)
491
- has the complete recipe, including the required attachment names.
492
-
493
- <!-- no-test -->
494
- ```ruby
495
- require 'pd4ml'
496
-
497
- pd = Pd4ml::PD4ML.new
498
- pd.read_html('invoice.html') # contains the <pd4ml:attachment> of factur-x.xml
499
- pd.write_pdf('invoice.pdf', spec: Pd4ml::PdfSpec::FacturX)
500
- ```
501
-
502
- ### 5.10 Passwords and permissions
503
-
504
- ```ruby
505
- require 'pd4ml'
506
-
507
- c = Pd4ml::Constants
508
- pd = Pd4ml::PD4ML.new
509
- pd.set_permissions('owner-secret', c::AllowPrint | c::AllowCopy, c::AES_256BIT)
510
- pd.read_html_string('<p>Confidential</p>')
511
- pd.write_pdf('protected.pdf')
512
- ```
513
-
514
- Combine the `Allow*` flags with `|`, or start from `Constants::DefaultPermissions` and remove
515
- flags with `& ~`. Encryption needs the Bouncy Castle jars, which the installation downloads. See
516
- [permissions and encryption](https://pd4ml.com/docs/#11.1-permissions-and-encryption).
517
-
518
- ### 5.11 Word (DOCX, RTF) and images
519
-
520
- ```ruby
521
- require 'pd4ml'
522
-
523
- pd = Pd4ml::PD4ML.new
524
- pd.read_html_string('<h1>Letter</h1><p>Dear customer, ...</p>')
525
- pd.write_docx('letter.docx')
526
- pd.write_rtf('letter.rtf')
527
- pages = pd.render_png # a PNG per page
528
- tiffs = pd.render_as_images(Pd4ml::Constants::TIFF) # one TIFF per page
529
- pd.render_as_images('pages', 'page', Pd4ml::Constants::PNG24) # page files in a directory
530
- ```
531
-
532
- The image formats are `PNG8`, `PNG24`, `TIFF` and `TIFF_COMPRESSED` (constants in `Constants`).
533
- The v42 engine does not support the multi-page TIFF formats yet; use the v40 engine for them.
534
-
535
- With the v42 engine, DOCX/RTF output can be made *editable*: positioned content becomes text
536
- boxes and form fields instead of pictures. To enable it, call
537
- `pd.set_param(Pd4ml::Constants::PD4ML_WORD_EDITABLE, 'true')`.
538
-
539
- ### 5.12 Interactive forms and multi-column layout
540
-
541
- ```ruby
542
- require 'pd4ml'
543
-
544
- pd = Pd4ml::PD4ML.new
545
- pd.generate_forms(true, nil) # <input>/<select>/<textarea> become PDF form fields
546
- pd.read_html_string("<form>Name: <input name='name' size='30'></form>")
547
- pd.write_pdf('form.pdf')
548
-
549
- news = Pd4ml::PD4ML.new
550
- news.generate_multicolumn(2, 20, true) # two columns, 20 mm apart
551
- news.read_html_string("<h1>Newsletter</h1>#{'<p>Lorem ipsum dolor sit amet.</p>' * 200}")
552
- news.write_pdf('newsletter.pdf')
553
- ```
554
-
555
- See [forms](https://pd4ml.com/docs/#10.1-interactive-pdf-forms) and
556
- [multi-column pages](https://pd4ml.com/docs/#10.2-multi-column-pages). Multi-column pages need
557
- PD4ML 4.0.24 / 4.1.2 or later.
558
-
559
- ### 5.13 Adding CSS and HTML programmatically
560
-
561
- ```ruby
562
- require 'pd4ml'
563
-
564
- pd = Pd4ml::PD4ML.new
565
- pd.add_style('body { font-family: sans-serif } h1 { color: #036 }', true) # true: after the document's styles
566
- pd.inject_html("<div style='font-size:8pt'>Generated by the reporting service</div>", false) # before </body>
567
- pd.read_html_string('<h1>Report</h1>')
568
- pd.write_pdf('report-styled.pdf')
569
- ```
570
-
571
- `add_style` also takes the URL of a stylesheet: `pd.add_style(Pd4ml.to_url('print.css'), true)`.
572
- See [injecting HTML](https://pd4ml.com/docs/#10.3-injecting-html-and-custom-resource-providers)
573
- and [adding styles](https://pd4ml.com/docs/usage-examples.html#add-style-programmatically).
574
-
575
- ### 5.14 Progress, diagnostics and metadata
576
-
577
- ```ruby
578
- require 'pd4ml'
579
-
580
- pd = Pd4ml::PD4ML.new
581
- pd.monitor_progress_with do |message_id, percent, note, msec|
582
- puts format('%3d%% %s (%d ms)', percent, note, msec)
583
- end
584
- pd.set_error_policy(Pd4ml::ErrorPolicy::NORMAL) # NONE, RELAXED, NORMAL, PEDANTIC, STRICT
585
- pd.set_log_level(1) # 0 (silent) .. 5
586
- pd.set_author_name('Reporting service')
587
- pd.set_document_title('Monthly statement')
588
- pd.add_metadata('StatementId', '2026-10', false)
589
- pd.read_html_string('<h1>Statement</h1>')
590
- pd.write_pdf('statement.pdf')
591
- pages = pd.get_last_render_info(Pd4ml::Constants::PD4ML_TOTAL_PAGES).to_i
592
- ```
593
-
594
- The block runs on the calling thread while the conversion runs in Java, and it may call PD4ML
595
- itself. An error it raises comes out of the call that caused it, as a `Pd4ml::CallbackError`.
596
- The v42 engine currently reports fewer progress stages than v40. More in
597
- [extensibility and diagnostics](https://pd4ml.com/docs/#12.-extensibility-and-diagnostics).
598
-
599
- ### 5.15 Working with existing PDFs
600
-
601
- ```ruby
602
- require 'pd4ml'
603
-
604
- cover = Pd4ml.html_to_pdf("<h1 style='margin-top:300px; text-align:center'>Annual Report</h1>".b)
605
- body = Pd4ml.html_to_pdf('<h2>Chapter 1</h2><p>...</p>'.b)
606
-
607
- doc = Pd4ml.open_pdf(cover) # a binary String, IO, path or URL (+ password)
608
- doc.append(Pd4ml.open_pdf(body)) # pages of another document after these
609
- doc.set_title('Annual Report')
610
- Pd4ml.write_pdf(doc, 'annual-report.pdf')
611
-
612
- stamp = Pd4ml.html_to_pdf("<p style='color:#c00; font-size:40pt'>COPY</p>".b)
613
- original = Pd4ml.open_pdf('annual-report.pdf')
614
- original.overlay(Pd4ml.open_pdf(stamp), 128) # draw over every page, half opaque (0..255)
615
- Pd4ml.write_pdf(original, 'annual-report-copy.pdf', password: 'secret')
616
-
617
- puts Pd4ml.open_pdf('annual-report.pdf').get_number_of_pages
618
- both = Pd4ml.merge_pdfs(cover, body) # two documents appended in one call
619
- ```
620
-
621
- `underlay` draws another document *under* the pages (a letterhead, for example);
622
- `delete_page`, `get_page_size`, `get_metadata` and `dump_outlines` inspect and edit a document.
623
- PD4ML's PDF toolkit (signing, optimisation, XFDF) is described in
624
- [the PDF post-processing toolkit](https://pd4ml.com/docs/#15.-the-pdf-post-processing-toolkit)
625
- and is reachable through `Pd4ml::Java.type(...)`.
626
-
627
- ### 5.16 Web applications
628
-
629
- Rails controller, with the HTML from a view:
630
-
631
- <!-- no-test -->
632
- ```ruby
633
- class InvoicesController < ApplicationController
634
- def show
635
- invoice = Invoice.find(params[:id])
636
- html = render_to_string(:show, layout: 'pdf', locals: { invoice: invoice })
637
- pd = Pd4ml::PD4ML.new
638
- pd.read_html_string(html, Rails.public_path.to_s) # base: where images/CSS live
639
- send_data pd.write_pdf, type: 'application/pdf', disposition: 'inline',
640
- filename: "invoice-#{invoice.number}.pdf"
641
- end
642
- end
643
- ```
644
-
645
- Sinatra:
646
-
647
- <!-- no-test -->
648
- ```ruby
649
- require 'sinatra'
650
- require 'pd4ml'
651
-
652
- get '/report.pdf' do
653
- pd = Pd4ml::PD4ML.new
654
- pd.read_html_string(erb(:report), settings.public_folder)
655
- content_type 'application/pdf'
656
- pd.write_pdf
657
- end
658
- ```
659
-
660
- To pay the cost of starting Java at boot instead of on the first request, touch PD4ML in an
661
- initializer (`Pd4ml.version`). Each Puma worker process starts its own bridge; set
662
- `PD4ML_BRIDGE` to share one bridge server between them.
663
-
664
- ### 5.17 Batch conversion
665
-
666
- ```ruby
667
- require 'pd4ml'
668
-
669
- Dir.glob('*.html').each do |source|
670
- pd = Pd4ml::PD4ML.new # one converter per document
671
- pd.read_html(source)
672
- pd.write_pdf(source.sub(/\.html\z/, '.pdf'))
673
- puts "written #{source}"
674
- end
675
- ```
676
-
677
- One bridge serves the whole loop. To convert in parallel, run several processes (each with its
678
- own bridge), or point them all at one bridge server.
679
-
680
- ### 5.18 Licensing
681
-
682
- <!-- no-test -->
683
- ```ruby
684
- require 'pd4ml'
685
-
686
- pd = Pd4ml::PD4ML.new(license: 'YOUR-SERIAL-NUMBER') # or a license file URL; or pd4ml.lic in the working dir / classpath
687
- puts 'unlicensed: output is watermarked' if pd.demo_mode?
688
- puts "PD4ML #{Pd4ml.version}#{pd.pro? ? ' pro' : ''}"
689
- ```
690
-
691
- Without a license, PD4ML is fully functional but marks its output. See
692
- [licensing](https://pd4ml.com/docs/#3.-licensing).
693
-
694
- ---
695
-
696
- ## 6. Tips and hints
697
-
698
- - **Start the bridge early.** The first use starts Java. Touch PD4ML when the application
699
- boots, or use a bridge server for short-lived processes.
700
- - **One converter per document.** `Pd4ml::PD4ML.new` is cheap; reuse the bridge, not the object.
701
- - **Set the base location for HTML from strings.** Missing images and unstyled output are almost
702
- always caused by unresolved relative links ([details](https://pd4ml.com/docs/#6.2-reading-the-source)).
703
- - **Settings go before `read_html`.** Page size, headers, fonts and styles apply to the layout
704
- `read_html` makes; writes afterwards only choose the output format.
705
- - **Write several formats from one read.** PDF, DOCX and page images from a single `read_html`
706
- cost one layout.
707
- - **Binary in, binary out.** Pass content as a binary String (`File.binread`, `.b`) or an IO;
708
- results are binary Strings, ready for `File.binwrite` or `send_data`.
709
- - **Print CSS works.** `@page { size: A4 landscape; margin: 15mm }`, `@media print` and
710
- `page-break-*` / `break-*` are honoured. They are often simpler than API calls.
711
- - **Control the scale with the HTML width.** If text is too small, or a layout wraps
712
- differently than in the browser, set `set_html_width` to the width the page was designed for.
713
- - **Index fonts once** with `-configure.fonts`, instead of pointing `use_ttf` at a large
714
- unindexed directory on every conversion.
715
- - **Restrict what documents can load** when converting untrusted HTML. Use `set_param` with
716
- `Constants::PD4ML_ALLOWED_RESOURCE_LOCATION` and `PD4ML_DISABLE_EXTERNAL_ATTACHMENTS`.
717
- - **Pin the build in production.** Use `pd4ml install <version>` or a `PD4ML_CLASSPATH` to a
718
- jar you keep, rather than following the latest release.
719
-
720
- ## 7. Troubleshooting
721
-
722
- | Symptom | Cause and remedy |
723
- |---|---|
724
- | `Pd4ml::BridgeError: PD4ML not found` | No build is configured and the automatic download is off (`PD4ML_AUTO_INSTALL=0`) or failed: run `pd4ml install`, or set `PD4ML_CLASSPATH`. |
725
- | `Pd4ml::BridgeError: cannot start java` / "closed the connection" | Java is missing or failed. Check `java -version`, set `JAVA_HOME` or `PD4ML_JAVA`; `PD4ML_BRIDGE_LOG=file` keeps Java's output. |
726
- | `Pd4ml::BridgeError: cannot connect to the PD4ML bridge` | The bridge server is not running, or `PD4ML_BRIDGE` names another port. |
727
- | A pause and a "downloading" warning on the first conversion | The automatic first-use download ([2.2](#22-install-pd4ml)). Run `pd4ml install` at deployment instead. |
728
- | `Errno::ENOENT` reading the document | A text String is taken as a path: pass HTML content with `read_html_string`, or as a binary String (`.b`). |
729
- | Images or styles missing | Relative links have no base location, or the server cannot reach the resources (proxies, authentication). See `set_cookie` and `set_http_options`. |
730
- | Output is watermarked | No license is installed ([licensing](https://pd4ml.com/docs/#3.-licensing)). |
731
- | Squares instead of characters | The fonts lack those glyphs. Provide fonts with `use_ttf` ([fonts](https://pd4ml.com/docs/#9.-fonts-bookmarks-and-hyperlinks)). |
732
- | `NoClassDefFoundError: org/bouncycastle/...` with `set_permissions` | The Bouncy Castle jars are missing. Install without `--no-bouncycastle`, or add them to the classpath. |
733
- | 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. |
734
- | `Pd4ml::JavaError: ... NoSuchMethodException` | No Java method of that name takes those argument types. Check [api.md](api.md); pass floats as `10.0`, binary data as a binary String. |
735
-
736
- ## 8. API reference
737
-
738
- ### Module `Pd4ml`
739
-
740
- | | |
741
- |---|---|
742
- | `Pd4ml::PD4ML` | the converter: `Pd4ml::PD4ML.new(license:, html_width:, engine:)`; every Java method of `com.pd4ml.PD4ML` |
743
- | `PageSize`, `PageMargins`, `Units`, `Dimensions`, `Constants`, `LayoutEngine`, `ErrorPolicy`, `PdfSpec`, `PdfDocument`, `ProgressListener`, `StatusMessage`, `ResourceProvider`, `CustomTag` | the PD4ML Java classes ([api.md](api.md)) |
744
- | `html_to_pdf(source, target, base_url:, page_size:, page_margins:, spec:, engine:, html_width:)`, `open_pdf(source, password)`, `write_pdf(doc, target, password:, permissions:)`, `merge_pdfs(a, b, target)` | one-call conversion and PDF documents |
745
- | `version`, `layout_engines?`, `to_url(location)`, `pdf_spec(keyword)` | runtime helpers |
746
- | `configure(**options)`, `shutdown` | the bridge: `classpath:`, `java:`, `jvm_options:`, `server:`, `log_file:` |
747
- | `install(version, **options)`, `installed_jar`, `installed_classpath`, `installed_version`, `available_versions` | PD4ML builds |
748
- | `Java.type(name)`, `Java.proxy(iface) { \|method, args\| ... }` | any Java class, Java interfaces implemented by Ruby blocks |
749
- | `JavaError`, `BridgeError`, `CallbackError`, `InstallError` | errors |
750
-
751
- ### `Pd4ml::PD4ML` methods with Ruby additions
752
-
753
- | Method | |
754
- |---|---|
755
- | `read_html(source, base_url = nil, encoding = nil)` | `source`: a path or URL (String, Pathname, URI), a binary String or IO, or a Java `URL`/`InputStream` |
756
- | `read_html_string(html, base_url = nil)` | the document as a String |
757
- | `write_pdf(target = nil, spec: nil)` | `target`: a path or IO; none: returns a binary String. `spec`: a `PdfSpec`, a keyword (`'pdf/a-2b'`) or `'PDF'`/`'PDFA'`/`'PDFUA'` |
758
- | `write_docx(target = nil)`, `write_rtf(target = nil, rtf_wmf: false)` | as `write_pdf` |
759
- | `render_as_images(format)`, `render_png` | pages as encoded images (binary Strings); `render_as_images(dir, name, format)` writes files |
760
- | `monitor_progress_with { \|message_id, progress, note, msec\| ... }` | a block, or a Java `ProgressListener` |
761
- | `use_html_renderer(engine, html_width: nil, params: nil)` | the layout engine by name; `pd.engine` tells which one is in use |
762
-
763
- The Java names of these methods (`readHTML`, `writePDF`...) work too. All other methods are those
764
- of the Java class `com.pd4ml.PD4ML` ([api.md](api.md#pd4mlpd4ml),
765
- [Java API](https://pd4ml.com/docs/#6.-the-java-api)); static ones are called on the class
766
- (`Pd4ml::PD4ML.get_version`). The Java object itself is `pd.java`.
767
-
768
- `ResourceProvider` and `CustomTag` are extended in **Java**: register yours with
769
- `add_custom_resource_provider` / `add_custom_tag_handler`, and put its classes on the classpath.
770
- Java *interfaces* (such as `ProgressListener`) can be implemented in Ruby with
771
- `Pd4ml::Java.proxy`. Examples: [resource loaders](https://github.com/zxfr/pd4ml-examples/blob/master/src/main/java/advanced/A005AddCustomResourceLoader.java),
772
- [custom tags](https://github.com/zxfr/pd4ml-examples/blob/master/src/main/java/advanced/A008AddingCustomTagRenderer.java).
773
-
774
- ## 9. Further reading
775
-
776
- - [PD4ML documentation](https://pd4ml.com/docs/) and the full [programmer's manual](https://pd4ml.com/docs/pd4ml-manual.html)
777
- - [Usage examples](https://pd4ml.com/docs/usage-examples.html)
778
- - [HTML5 element support matrix](https://pd4ml.com/docs/html5-element-support-matrix.html)
779
- - [Command-line tool reference](https://pd4ml.com/docs/pd4cmd-command-line-reference.html)
780
- - [ZUGFeRD and Factur-X invoices](https://pd4ml.com/docs/features/zugferd-and-factur-x-pdf-invoices.html)
781
- - [PDF post-processing toolkit](https://pd4ml.com/docs/#15.-the-pdf-post-processing-toolkit)
782
- - [Java examples on GitHub](https://github.com/zxfr/pd4ml-examples)
1
+ # PD4ML for Ruby — User Guide
2
+
3
+ `pd4ml` brings [PD4ML](https://pd4ml.com) to Ruby: it converts HTML and CSS into **PDF**,
4
+ **PDF/A**, **PDF/UA**, **ZUGFeRD/Factur-X** invoices, **DOCX**, **RTF** and **page images**, and
5
+ gives Ruby applications the complete public PD4ML API.
6
+
7
+ PD4ML is a Java library. The gem runs it in a small Java *bridge* process that Ruby talks to.
8
+ Every PD4ML class and method can be called from Ruby, by its Java name or Ruby-style
9
+ (`set_page_size`), and the gem adds Ruby I/O on top: paths, binary strings and IO objects
10
+ instead of Java streams.
11
+
12
+ The PD4ML manual, which this guide refers to throughout, is at <https://pd4ml.com/docs/>.
13
+ The generated reference of every class and method is [api.md](api.md).
14
+
15
+ **Contents**
16
+
17
+ 1. [Prerequisites](#1-prerequisites)
18
+ 2. [Installation](#2-installation)
19
+ 3. [Quick start](#3-quick-start)
20
+ 4. [How it works](#4-how-it-works)
21
+ 5. [Use cases](#5-use-cases)
22
+ 6. [Tips and hints](#6-tips-and-hints)
23
+ 7. [Troubleshooting](#7-troubleshooting)
24
+ 8. [API reference](#8-api-reference)
25
+ 9. [Further reading](#9-further-reading)
26
+
27
+ ---
28
+
29
+ ## 1. Prerequisites
30
+
31
+ | Requirement | Notes |
32
+ |---|---|
33
+ | **Ruby 3.1 or later** | MRI on any platform Ruby and Java run on. |
34
+ | **Java 8 or later** | A JDK or JRE; an LTS release (17, 21) is recommended. The gem finds it through `PD4ML_JAVA`, then `JAVA_HOME`, then `java` on the `PATH`. |
35
+ | **PD4ML** | Downloaded automatically on first use, or ahead of time with `pd4ml install` (see below); or any PD4ML build you point the gem at. |
36
+ | **Network access** | Only for downloading PD4ML. Conversions need none, unless the HTML references remote resources. |
37
+ | **A PD4ML license** | Optional for evaluation. Without one, PD4ML runs in evaluation mode and watermarks its output. See [Licensing](https://pd4ml.com/docs/#3.-licensing) and [5.21](#521-licensing). |
38
+
39
+ Check Java with `java -version`. On a server without Java, install a JRE package (for example
40
+ `apt install openjdk-21-jre-headless`) and, if it is not on the `PATH`, set `JAVA_HOME`. In
41
+ containers, use an image with both Ruby and Java, or add a JRE to the Ruby image.
42
+
43
+ ## 2. Installation
44
+
45
+ ### 2.1 Install the gem
46
+
47
+ ```
48
+ gem install pd4ml
49
+ ```
50
+
51
+ or, with Bundler, `gem 'pd4ml'` in the `Gemfile` and `bundle install`. The gem has no gem
52
+ dependencies and installs the `pd4ml` command.
53
+
54
+ The gem is published on RubyGems as [`pd4ml`](https://rubygems.org/gems/pd4ml). The same files
55
+ are on GitHub, <https://github.com/zxfr/pd4ml-ruby>, with each version's `.gem` attached to its
56
+ [release](https://github.com/zxfr/pd4ml-ruby/releases); Bundler can also take it from there
57
+ (`gem 'pd4ml', git: 'https://github.com/zxfr/pd4ml-ruby.git'`).
58
+
59
+ ### 2.2 Install PD4ML
60
+
61
+ The gem contains the Ruby library and the bridge only. The PD4ML library itself comes from the
62
+ PD4ML Maven repository (<https://pd4ml.com/maven2>), in one of two ways.
63
+
64
+ **Automatically, on first use.** If no PD4ML build is installed or configured
65
+ ([2.3](#23-other-ways-to-provide-pd4ml)), the first use of PD4ML in a process downloads PD4ML 4.2
66
+ ([which build](#which-build)) before starting Java. A warning on standard error reports it. So
67
+ once the gem is installed, this is all it takes:
68
+
69
+ <!-- no-test -->
70
+ ```ruby
71
+ require 'pd4ml'
72
+
73
+ pdf = Pd4ml.html_to_pdf('<h1>Hello</h1>'.b) # the first call downloads PD4ML (once)
74
+ ```
75
+
76
+ **Ahead of time, with the `pd4ml` command.** This is better for servers, CI and container
77
+ images, where a download in the middle of a request is unwelcome:
78
+
79
+ ```
80
+ pd4ml install # with Bundler: bundle exec pd4ml install
81
+ ```
82
+
83
+ Either way, the gem:
84
+
85
+ - downloads PD4ML 4.2 ([which build](#which-build));
86
+ - downloads the Bouncy Castle jars PD4ML needs for encryption and signing, from Maven Central;
87
+ - checks every file against its published SHA-1 checksum;
88
+ - stores them in a per-user cache directory: `%LOCALAPPDATA%\pd4ml-ruby` on Windows,
89
+ `~/.cache/pd4ml-ruby` elsewhere, or `PD4ML_HOME`.
90
+
91
+ The gem then uses that build. Later runs do not check for newer PD4ML releases: run
92
+ `pd4ml install` again to update. Updating the gem does not change the PD4ML build.
93
+
94
+ <a id="which-build"></a>**Which build.** The gem needs PD4ML 4.2 for its 4.2 API: `read_eml` and
95
+ `read_mht`, `add_allowed_resource_location`, `add_metadata` (XMP), and the DOM API
96
+ (`get_document`, `layout`), as well as for the v42 layout engine ([4.3](#43-layout-engines)). So a
97
+ plain `pd4ml install` (and the first use) takes:
98
+
99
+ - the most recent PD4ML release from 4.2.0 on;
100
+ - while 4.2.0 is not released yet, the most recent 4.2.x **development snapshot** (today
101
+ 4.2.0-SNAPSHOT), because no release has the 4.2 API yet.
102
+
103
+ Once 4.2.0 is released, new installs take it by themselves, and `pd4ml install` updates an
104
+ existing snapshot install to it. Snapshots of later versions (4.3, 5.0) are never taken by
105
+ default; `--snapshots` asks for them.
106
+
107
+ The rest of the API works with older builds too: to stay on the current 4.1 release, install it
108
+ explicitly (`pd4ml install 4.1.2`); the 4.2 methods then raise `Pd4ml::JavaError`
109
+ (`NoSuchMethodException`).
110
+
111
+ | Option | |
112
+ |---|---|
113
+ | *(none)* | the most recent release from 4.2.0 on, else the most recent 4.2.x snapshot ([which build](#which-build)), or an update to it |
114
+ | `--snapshots` | the most recent 4.x build, including development snapshots |
115
+ | `pd4ml install 4.1.2` | an exact version (any release or snapshot the repository lists) |
116
+ | `--jakarta` | for Jakarta EE / Servlet 6 environments: a `-jakarta` variant if one fits; PD4ML 4.2 has none (its builds contain the Jakarta tag library), so the same 4.2 build (an older `-jakarta` build by version, e.g. `pd4ml install 4.0.20fx3-jakarta`) |
117
+ | `--home DIR` | install into `DIR` (default: `PD4ML_HOME`, else the cache directory) |
118
+ | `--force` | download again |
119
+ | `--no-bouncycastle` | skip the Bouncy Castle jars (encryption and signing will not work) |
120
+ | `pd4ml list` | list the available versions |
121
+
122
+ The same is available from Ruby, which suits an application's boot (a Rails initializer):
123
+
124
+ <!-- no-test -->
125
+ ```ruby
126
+ require 'pd4ml'
127
+
128
+ Pd4ml.install # downloads only when that build is not there yet
129
+ puts Pd4ml.installed_version, Pd4ml.installed_classpath
130
+ ```
131
+
132
+ To check the installation, run `pd4ml version`.
133
+
134
+ **Turning the automatic download off.** Set `PD4ML_AUTO_INSTALL=0`, for example on servers that
135
+ must not reach the internet, or where the build is provisioned separately. The gem then raises
136
+ `Pd4ml::BridgeError` until a build is installed or configured. The automatic download never runs
137
+ when a build is configured through [2.3](#23-other-ways-to-provide-pd4ml).
138
+
139
+ > **Services and containers**: the account the application runs as must see the installed
140
+ > build. Install it as that account, or install into a shared directory and set `PD4ML_HOME`
141
+ > for the application too. In a Dockerfile:
142
+ >
143
+ > ```
144
+ > RUN bundle install && PD4ML_HOME=/opt/pd4ml bundle exec pd4ml install
145
+ > ENV PD4ML_HOME=/opt/pd4ml PD4ML_AUTO_INSTALL=0
146
+ > ```
147
+
148
+ ### 2.3 Other ways to provide PD4ML
149
+
150
+ The gem looks for PD4ML in this order:
151
+
152
+ 1. `Pd4ml.configure(classpath: [...])`, called before the first conversion;
153
+ 2. the `PD4ML_CLASSPATH` environment variable: jars and class directories separated by `;`
154
+ (Windows) or `:` (elsewhere), with `dir/*` for all jars of a directory;
155
+ 3. the `PD4ML_CLASSPATH_FILE` environment variable: a file with one entry per line;
156
+ 4. the build `pd4ml install` (or the first use) downloaded.
157
+
158
+ Typical reasons to use them:
159
+
160
+ - **Offline or air-gapped servers**: run `pd4ml install --home ./pd4ml-libs` on a machine with
161
+ network access, copy the directory, and set `PD4ML_HOME` and `PD4ML_AUTO_INSTALL=0` on the
162
+ target.
163
+ - **A licensed or specific jar**: `PD4ML_CLASSPATH=/opt/pd4ml/pd4ml.jar:/opt/pd4ml/lib/*`.
164
+ - **A PD4ML development build** (for the v42 layout engine, see [4.3](#43-layout-engines)): a
165
+ classpath file listing the build's classes and its Maven dependencies, in
166
+ `PD4ML_CLASSPATH_FILE`.
167
+
168
+ ## 3. Quick start
169
+
170
+ ```ruby
171
+ require 'pd4ml'
172
+
173
+ pd = Pd4ml::PD4ML.new
174
+ pd.set_page_size(Pd4ml::PageSize::A4)
175
+ pd.read_html_string('<h1>Hello, PDF</h1><p>Generated with <b>PD4ML</b>.</p>')
176
+ pd.write_pdf('hello.pdf')
177
+ ```
178
+
179
+ Or in one call:
180
+
181
+ ```ruby
182
+ require 'pd4ml'
183
+
184
+ pdf = Pd4ml.html_to_pdf('<h1>Invoice 42</h1>'.b)
185
+ puts pdf[0, 5] # %PDF-
186
+ ```
187
+
188
+ ## 4. How it works
189
+
190
+ ### 4.1 The two-phase model
191
+
192
+ A document is **read once and written any number of times**:
193
+
194
+ - `read_html` parses the document and lays it out;
195
+ - each `write_pdf`, `write_docx`, `write_rtf` or `render_as_images` call then produces one output
196
+ from that same layout.
197
+
198
+ Settings (page size, headers, fonts...) go before `read_html`. See
199
+ [the two-phase model](https://pd4ml.com/docs/#6.1-the-two-phase-model).
200
+
201
+ ```ruby
202
+ require 'pd4ml'
203
+
204
+ pd = Pd4ml::PD4ML.new
205
+ pd.read_html_string('<h1>Quarterly report</h1>')
206
+ pd.write_pdf('report.pdf')
207
+ pd.write_docx('report.docx')
208
+ thumbnails = pd.render_png # PNG images, one binary String per page
209
+ ```
210
+
211
+ ### 4.2 The bridge and threads
212
+
213
+ - **One bridge per process.** The first use of PD4ML starts a Java bridge process. It serves
214
+ every conversion of the Ruby process, from any thread, and ends with it. Starting Java takes a
215
+ fraction of a second; after that, converting an ordinary document takes milliseconds.
216
+ - **Threads.** The bridge is thread-safe: use one `Pd4ml::PD4ML` per document (each thread its
217
+ own) and convert from as many threads as you like (Puma, Sidekiq). The conversions themselves
218
+ run one at a time, so for parallel throughput use several processes, or point them all at one
219
+ bridge server (next item). Each Puma or Unicorn worker process starts its own bridge on first
220
+ use; there is nothing to set up before forking.
221
+ - **Shared bridge server.** Several processes, or short-lived ones (rake tasks, CLI tools), can
222
+ use one long-running bridge instead of each starting Java:
223
+
224
+ ```
225
+ pd4ml serve --port 8723 # keep it running (systemd, a sidecar container, foreman)
226
+ ```
227
+
228
+ Then set `PD4ML_BRIDGE=tcp://127.0.0.1:8723`, or call
229
+ `Pd4ml.configure(server: 'tcp://127.0.0.1:8723')`. The server listens on the loopback
230
+ interface only, and each connection has its own objects.
231
+ - **JVM options** (memory, system properties): set `PD4ML_JVM_OPTIONS="-Xmx2g"`, or call
232
+ `Pd4ml.configure(jvm_options: ['-Xmx2g'])` before the first use. Java runs headless.
233
+ - **The bridge's console output** is discarded unless `PD4ML_BRIDGE_LOG=path` (or
234
+ `Pd4ml.configure(log_file: ...)`) keeps it. `Pd4ml.shutdown` stops the bridge; the next use
235
+ starts it again.
236
+
237
+ ### 4.3 Layout engines
238
+
239
+ PD4ML 4.2 introduces a second HTML layout engine, **v42**, written to match current browsers
240
+ closely (flexbox, grid, modern CSS). `Pd4ml::PD4ML.new` uses it by default:
241
+
242
+ ```ruby
243
+ require 'pd4ml'
244
+
245
+ if Pd4ml.layout_engines?
246
+ pd = Pd4ml::PD4ML.new # v42
247
+ legacy = Pd4ml::PD4ML.new(engine: 'v40') # the previous engine
248
+ puts pd.engine, legacy.engine
249
+ end
250
+ ```
251
+
252
+ Builds before 4.2 have one engine only; this includes the current public release, 4.1.x. With
253
+ such a build:
254
+
255
+ - the gem uses that build's engine and warns once;
256
+ - `Pd4ml.layout_engines?` returns false;
257
+ - asking for an engine explicitly raises.
258
+
259
+ ### 4.4 Ruby and Java values
260
+
261
+ - **Method names**: Java methods can be called by their Java names (`setPageSize`, `readHTML`)
262
+ or Ruby-style: `set_page_size`, `use_ttf` (for `useTTF`), `demo_mode?` (for `isDemoMode`).
263
+ Ruby-style names are matched to the real Java names, acronyms included.
264
+ - **Classes**: the PD4ML classes are Ruby classes in the `Pd4ml` module. Constants are Ruby
265
+ constants (`Pd4ml::PageSize::A4`, `Pd4ml::Constants::AllowPrint`); lower-case static fields and
266
+ static methods are class methods (`Pd4ml::LayoutEngine.v42`,
267
+ `Pd4ml::PdfSpec.get_instance('pdf/a-2b')`); constructors use `new`. Any other Java class:
268
+ `Pd4ml::Java.type('java.util.Locale')`.
269
+ - **Objects**: a Java object comes back as a `Pd4ml::JavaObject` with all its Java methods.
270
+ Public fields: `obj[:width]` and `obj[:width] = value`. `to_s` is Java's `toString()`,
271
+ `enum_name` an enum constant's name, `java_class` the class name.
272
+ - **Collections**: a Java `List` or `Map` generally comes back as a `JavaObject` (`size`,
273
+ `get(i)`). The DOM API ([5.18](#518-modifying-the-document-before-conversion-dom-api)) is the
274
+ exception: its lists and sets come back as Ruby Arrays and its maps as Hashes. `call_java` returns
275
+ them as Java objects.
276
+ - **Values**: Strings, Integers, Floats (`100.0` goes to Java as a float), `true`/`false`, `nil`,
277
+ Symbols (as strings: `engine: :v40`), Arrays, Hashes (as Java `Map`s).
278
+ - **Binary data**: a binary String (`File.binread`, `"...".b`, encoding ASCII-8BIT) goes to Java
279
+ as `byte[]`; an IO is read. Java's `byte[]` comes back as a binary String. Text Strings (UTF-8)
280
+ go to Java as text.
281
+ - Java objects are released when Ruby garbage-collects them.
282
+
283
+ ### 4.5 Exceptions
284
+
285
+ Java exceptions are raised as `Pd4ml::JavaError`, which knows the Java class and its superclasses:
286
+
287
+ ```ruby
288
+ require 'pd4ml'
289
+
290
+ begin
291
+ pd = Pd4ml::PD4ML.new
292
+ pd.read_html('file:///no/such/document.html')
293
+ pd.write_pdf
294
+ rescue Pd4ml::JavaError => e
295
+ raise unless e.instance_of_java?('java.io.IOException')
296
+
297
+ puts "cannot read the document: #{e.java_message}"
298
+ end
299
+ ```
300
+
301
+ `e.java_class`, `e.causes` and `e.java_trace` hold the details. Other errors:
302
+
303
+ | Error | Raised when |
304
+ |---|---|
305
+ | `Pd4ml::BridgeError` | the bridge itself fails: no Java, no PD4ML build, or the process ended |
306
+ | `Pd4ml::CallbackError` | a Ruby block raised, such as a progress listener; the original error is `cause` |
307
+ | `Pd4ml::InstallError` | downloading PD4ML fails |
308
+
309
+ ---
310
+
311
+ ## 5. Use cases
312
+
313
+ The snippets below are complete and runnable. Files they read (`report.html`...) are assumed to
314
+ exist in the working directory.
315
+
316
+ ### 5.1 HTML string to PDF
317
+
318
+ This is the common case for web applications: render HTML with your template engine (ERB, Haml,
319
+ Slim, a Rails view) and convert it in memory.
320
+
321
+ ```ruby
322
+ require 'pd4ml'
323
+
324
+ html = '<html><body><h1>Order 1001</h1><p>Thank you for your order.</p></body></html>'
325
+
326
+ pd = Pd4ml::PD4ML.new
327
+ pd.read_html_string(html)
328
+ pdf = pd.write_pdf # the PDF as a binary String
329
+ ```
330
+
331
+ `write_pdf`, `write_docx` and `write_rtf` return the document as a binary String when given no
332
+ target. Given a path (a String or Pathname) they write the file, given an IO (a `File`, a
333
+ `StringIO`, a Rack body) they write into it.
334
+
335
+ ### 5.2 Files and URLs
336
+
337
+ ```ruby
338
+ require 'pd4ml'
339
+
340
+ pd = Pd4ml::PD4ML.new
341
+ pd.read_html('report.html') # also: 'https://example.com/report', a Pathname, a URI
342
+ pd.write_pdf('report.pdf')
343
+ ```
344
+
345
+ **Relative links** (`<img src="logo.png">`, `<link href="style.css">`) resolve against the
346
+ document's own location when it is read from a path or URL. When the HTML comes from a string,
347
+ binary String or IO, pass the base location. Otherwise images and stylesheets go missing without
348
+ an error (see [reading the source](https://pd4ml.com/docs/#6.2-reading-the-source)):
349
+
350
+ ```ruby
351
+ require 'pd4ml'
352
+
353
+ html = '<link rel="stylesheet" href="style.css"><p class="note">Styled</p>'
354
+ pd = Pd4ml::PD4ML.new
355
+ pd.read_html_string(html, '.') # a directory, file path or URL
356
+ pd.write_pdf('styled.pdf')
357
+ ```
358
+
359
+ A text String that does not start with `<` is taken as a path or URL; content always goes in as a
360
+ binary String (`File.binread`), an IO, or through `read_html_string`. For a document in a known
361
+ encoding other than UTF-8, call `pd.read_html(File.binread(path), base, 'windows-1251')`.
362
+
363
+ ### 5.3 Page size, orientation, margins and scaling
364
+
365
+ ```ruby
366
+ require 'pd4ml'
367
+
368
+ pd = Pd4ml::PD4ML.new
369
+ pd.set_page_size(Pd4ml::PageSize::LETTER.rotate) # landscape
370
+ pd.set_page_margins(Pd4ml::PageMargins.new(15, 20, 15, 20, Pd4ml::Units::MM)) # left, top, right, bottom
371
+ pd.set_html_width(1000) # the virtual browser width the page is laid out in
372
+ pd.read_html_string("<table width='100%'><tr><td>wide content</td></tr></table>")
373
+ pd.write_pdf('landscape.pdf')
374
+ ```
375
+
376
+ The layout is scaled to the page: `scale = printable width / html width`, so a smaller HTML width
377
+ makes everything larger. Related controls:
378
+
379
+ - `adjust_html_width(true)` lays the page out at the document's own natural width.
380
+ - `fit_page_vertically(Pd4ml::Constants::CENTER_ALIGN)` shrinks the whole document onto a
381
+ single page.
382
+ - `set_page_size` and `set_page_margins` accept a page *scope* (`'1'`, `'2+'`, `'odd'`...) as a
383
+ second argument.
384
+
385
+ More in [page layout](https://pd4ml.com/docs/#7.-page-layout-size-margins-and-scaling).
386
+
387
+ For a custom size, use `Pd4ml::PageSize.new(100.0, 150.0, Pd4ml::Units::MM)`. For a named size,
388
+ use `Pd4ml::PageSize.get_instance('a5')` (the name in lower case).
389
+
390
+ ### 5.4 Headers, footers and page numbers
391
+
392
+ Headers and footers are HTML fragments. PD4ML fills in `$[page]`, `$[total]` and `$[title]` on
393
+ each page, and `set_dynamic_data` adds placeholders of your own. `$[...]` is safe in both single-
394
+ and double-quoted Ruby strings (only `#{...}` is interpolated).
395
+
396
+ ```ruby
397
+ require 'pd4ml'
398
+
399
+ pd = Pd4ml::PD4ML.new
400
+ pd.set_dynamic_data({ 'customer' => 'Acme Corp.', 'invoice' => 'INV-2026-0031' })
401
+ pd.set_page_header("<div style='font-size:9pt'><b>$[title]</b></div>", 30, '1+')
402
+ pd.set_page_footer(
403
+ "<div style='font-size:8pt; text-align:right'>" \
404
+ 'Invoice $[invoice] &mdash; $[customer] &mdash; page $[page] of $[total]</div>',
405
+ 24, '1+'
406
+ )
407
+ pd.read_html_string("<title>Invoice</title><h1>Invoice</h1>#{'<p>Line item</p>' * 80}")
408
+ pd.write_pdf('invoice.pdf')
409
+ ```
410
+
411
+ The second argument is the area height in pixels. The third is the page scope: `'1'` (first
412
+ page), `'2+'` (all but the first), `'odd'`, `'even'`, `'3-7'`... You can also write headers and
413
+ footers inline in the HTML with PD4ML's extension tags. See
414
+ [headers, footers, watermarks and backgrounds](https://pd4ml.com/docs/#8.-headers-footers-watermarks-and-backgrounds)
415
+ and the [usage examples](https://pd4ml.com/docs/usage-examples.html#set-page-header-and-footer).
416
+
417
+ ### 5.5 Watermarks and page backgrounds
418
+
419
+ ```ruby
420
+ require 'pd4ml'
421
+
422
+ pd = Pd4ml::PD4ML.new
423
+ pd.set_watermark("<span style='font-size:72pt; color:#c00'>DRAFT</span>",
424
+ 120.0, 350.0, # left, top (px)
425
+ 0.15, # opacity
426
+ 45.0, # rotation, degrees
427
+ 1.0, # scale
428
+ true, true, # visible on screen, in print
429
+ '1+') # pages
430
+ pd.set_page_background("<div style='background:#f7f7f2; height:100%'></div>", '1+')
431
+ pd.read_html_string('<h1>Proposal</h1>')
432
+ pd.write_pdf('draft.pdf')
433
+ ```
434
+
435
+ ### 5.6 Fonts
436
+
437
+ PD4ML's built-in fonts cover Unicode broadly. For a specific typeface (a corporate font, CJK
438
+ coverage), give it a font directory:
439
+
440
+ <!-- no-test -->
441
+ ```ruby
442
+ require 'pd4ml'
443
+
444
+ pd = Pd4ml::PD4ML.new
445
+ pd.use_ttf('/opt/fonts', true) # a directory of TTF/OTF fonts (indexed: see below)
446
+ pd.embed_ttfs(true, false) # embed only the glyphs used
447
+ pd.apply_kerning(true)
448
+ pd.enable_ligatures(true)
449
+ ```
450
+
451
+ Index a font directory once, ahead of time. PD4ML then reads the index (`pd4fonts.properties`)
452
+ instead of scanning all the fonts on every conversion
453
+ ([font indexing](https://pd4ml.com/docs/#4.3-font-indexing)):
454
+
455
+ ```
456
+ java -jar <pd4ml jar> -configure.fonts /opt/fonts
457
+ ```
458
+
459
+ `ruby -rpd4ml -e 'puts Pd4ml.installed_jar'` prints the jar's path. CSS `@font-face` web fonts in
460
+ the document work as well.
461
+
462
+ ### 5.7 Bookmarks, links and tables of contents
463
+
464
+ ```ruby
465
+ require 'pd4ml'
466
+
467
+ pd = Pd4ml::PD4ML.new
468
+ pd.generate_bookmarks_from_headings(true) # one PDF bookmark per <h1>..<h6>
469
+ pd.enable_hyperlinks(true) # <a href> become clickable links (the default)
470
+ pd.read_html_string("<h1>Introduction</h1><p>...</p><h1 style='page-break-before:always'>Details</h1>")
471
+ pd.write_pdf('bookmarked.pdf')
472
+ ```
473
+
474
+ PD4ML's `<pd4ml:toc>` tag generates a table of contents with page numbers from the headings; see
475
+ the [usage examples](https://pd4ml.com/docs/usage-examples.html#add-toc). For explicit page
476
+ breaks, use CSS `page-break-before` / `break-before`, or `<pd4ml:page.break>`.
477
+
478
+ ### 5.8 PDF/A and PDF/UA
479
+
480
+ ```ruby
481
+ require 'pd4ml'
482
+
483
+ pd = Pd4ml::PD4ML.new
484
+ pd.read_html_string("<html lang='en'><title>Annual report</title><h1>Annual report</h1>")
485
+ pd.write_pdf('archive.pdf', spec: Pd4ml::PdfSpec::PDFA_3B) # archival
486
+ pd.write_pdf('accessible.pdf', spec: Pd4ml::PdfSpec::PDFUA_1) # accessible
487
+ pd.write_pdf('both.pdf', spec: Pd4ml::PdfSpec::PDF_1_7.combine(Pd4ml::PdfSpec::PDFA_2A))
488
+ pd.write_pdf('keyword.pdf', spec: 'pdf/a-2b') # by keyword
489
+
490
+ status = pd.get_last_render_info(Pd4ml::Constants::PD4ML_PDFA_STATUS) # after writing
491
+ ```
492
+
493
+ `Pd4ml::PdfSpec.list_keywords` lists the keywords; an unknown one raises `ArgumentError`
494
+ (`PdfSpec.get_instance` alone would fall back to PDF 1.7 without an error).
495
+
496
+ - PDF/A embeds all fonts.
497
+ - PDF/UA needs well-structured HTML: real headings, `alt` texts, `lang`, table headers.
498
+ Combine it with `pd.set_error_policy(Pd4ml::ErrorPolicy::PEDANTIC)` to be told when the source
499
+ does not satisfy the standard.
500
+
501
+ Details: [PDF/A, PDF/UA and e-invoicing](https://pd4ml.com/docs/#11.2-pdfa-pdfua-and-e-invoicing-formats).
502
+
503
+ ### 5.9 ZUGFeRD / Factur-X e-invoices
504
+
505
+ `PdfSpec::ZUGFeRD` and `PdfSpec::FacturX` select the PDF/A-3 container the standards require.
506
+ The HTML attaches the invoice XML with PD4ML's attachment tag. The
507
+ [ZUGFeRD and Factur-X page](https://pd4ml.com/docs/features/zugferd-and-factur-x-pdf-invoices.html)
508
+ has the complete recipe, including the required attachment names.
509
+
510
+ <!-- no-test -->
511
+ ```ruby
512
+ require 'pd4ml'
513
+
514
+ pd = Pd4ml::PD4ML.new
515
+ pd.read_html('invoice.html') # contains the <pd4ml:attachment> of factur-x.xml
516
+ pd.write_pdf('invoice.pdf', spec: Pd4ml::PdfSpec::FacturX)
517
+ ```
518
+
519
+ ### 5.10 Passwords and permissions
520
+
521
+ ```ruby
522
+ require 'pd4ml'
523
+
524
+ c = Pd4ml::Constants
525
+ pd = Pd4ml::PD4ML.new
526
+ pd.set_permissions('owner-secret', c::AllowPrint | c::AllowCopy, c::AES_256BIT)
527
+ pd.read_html_string('<p>Confidential</p>')
528
+ pd.write_pdf('protected.pdf')
529
+ ```
530
+
531
+ Combine the `Allow*` flags with `|`, or start from `Constants::DefaultPermissions` and remove
532
+ flags with `& ~`. Encryption needs the Bouncy Castle jars, which the installation downloads. See
533
+ [permissions and encryption](https://pd4ml.com/docs/#11.1-permissions-and-encryption).
534
+
535
+ ### 5.11 Word (DOCX, RTF) and images
536
+
537
+ ```ruby
538
+ require 'pd4ml'
539
+
540
+ pd = Pd4ml::PD4ML.new
541
+ pd.read_html_string('<h1>Letter</h1><p>Dear customer, ...</p>')
542
+ pd.write_docx('letter.docx')
543
+ pd.write_rtf('letter.rtf')
544
+ pages = pd.render_png # a PNG per page
545
+ tiffs = pd.render_as_images(Pd4ml::Constants::TIFF) # one TIFF per page
546
+ pd.render_as_images('pages', 'page', Pd4ml::Constants::PNG24) # page files in a directory
547
+ ```
548
+
549
+ The image formats are `PNG8`, `PNG24`, `TIFF` and `TIFF_COMPRESSED` (constants in `Constants`).
550
+ The TIFF formats need Java 9 or later.
551
+ The v42 engine does not support the multi-page TIFF formats yet; use the v40 engine for them.
552
+
553
+ With the v42 engine, DOCX/RTF output is *editable*: positioned content becomes text boxes
554
+ and form fields, not pictures.
555
+
556
+ ### 5.12 Interactive forms and multi-column layout
557
+
558
+ ```ruby
559
+ require 'pd4ml'
560
+
561
+ pd = Pd4ml::PD4ML.new
562
+ pd.generate_forms(true, nil) # <input>/<select>/<textarea> become PDF form fields
563
+ pd.read_html_string("<form>Name: <input name='name' size='30'></form>")
564
+ pd.write_pdf('form.pdf')
565
+
566
+ news = Pd4ml::PD4ML.new
567
+ news.generate_multicolumn(2, 20, true) # two columns, 20 mm apart
568
+ news.read_html_string("<h1>Newsletter</h1>#{'<p>Lorem ipsum dolor sit amet.</p>' * 200}")
569
+ news.write_pdf('newsletter.pdf')
570
+ ```
571
+
572
+ See [forms](https://pd4ml.com/docs/#10.1-interactive-pdf-forms) and
573
+ [multi-column pages](https://pd4ml.com/docs/#10.2-multi-column-pages). Multi-column pages need
574
+ PD4ML 4.0.24 / 4.1.2 or later.
575
+
576
+ ### 5.13 Adding CSS and HTML programmatically
577
+
578
+ ```ruby
579
+ require 'pd4ml'
580
+
581
+ pd = Pd4ml::PD4ML.new
582
+ pd.add_style('body { font-family: sans-serif } h1 { color: #036 }', true) # true: after the document's styles
583
+ pd.inject_html("<div style='font-size:8pt'>Generated by the reporting service</div>", false) # before </body>
584
+ pd.read_html_string('<h1>Report</h1>')
585
+ pd.write_pdf('report-styled.pdf')
586
+ ```
587
+
588
+ `add_style` also takes the URL of a stylesheet: `pd.add_style(Pd4ml.to_url('print.css'), true)`.
589
+ See [injecting HTML](https://pd4ml.com/docs/#10.3-injecting-html-and-custom-resource-providers)
590
+ and [adding styles](https://pd4ml.com/docs/usage-examples.html#add-style-programmatically).
591
+
592
+ ### 5.14 Progress, diagnostics and metadata
593
+
594
+ ```ruby
595
+ require 'pd4ml'
596
+
597
+ pd = Pd4ml::PD4ML.new
598
+ pd.monitor_progress_with do |message_id, percent, note, msec|
599
+ puts format('%3d%% %s (%d ms)', percent, note, msec)
600
+ end
601
+ pd.set_error_policy(Pd4ml::ErrorPolicy::NORMAL) # NONE, RELAXED, NORMAL, PEDANTIC, STRICT
602
+ pd.set_log_level(1) # 0 (silent) .. 5
603
+ pd.set_author_name('Reporting service')
604
+ pd.set_document_title('Monthly statement')
605
+ pd.add_metadata('StatementId', '2026-10', false)
606
+ pd.add_metadata('http://www.w3.org/ns/tdmrep/', 'tdm', 'reservation', '1') # XMP, a namespace of your own
607
+ pd.read_html_string('<h1>Statement</h1>')
608
+ pd.write_pdf('statement.pdf')
609
+ pages = pd.get_last_render_info(Pd4ml::Constants::PD4ML_TOTAL_PAGES).to_i
610
+ ```
611
+
612
+ The block runs on the calling thread while the conversion runs in Java, and it may call PD4ML
613
+ itself. An error it raises comes out of the call that caused it, as a `Pd4ml::CallbackError`.
614
+ The v42 engine currently reports fewer progress stages than v40. More in
615
+ [extensibility and diagnostics](https://pd4ml.com/docs/#12.-extensibility-and-diagnostics).
616
+
617
+ `add_metadata(key, value, hide)` adds a custom property to the PDF's XMP metadata, which viewers
618
+ show among the custom document properties (`hide`: true keeps it out of their view).
619
+ `add_metadata(namespace_uri, prefix, key, value)` (PD4ML 4.2) adds an XMP property in a
620
+ namespace of your own; the example writes `<tdm:reservation>1</tdm:reservation>`, which tells
621
+ text and data mining crawlers the rights are reserved (the W3C TDM Reservation Protocol). In PDF/A
622
+ output, PD4ML declares the namespace in a PDF/A extension schema. A reserved prefix (`rdf`, `xmp`,
623
+ `dc`, `pdf`, `pd4ml`...) raises `Pd4ml::JavaError` (`IllegalArgumentException`).
624
+
625
+ ### 5.15 Working with existing PDFs
626
+
627
+ ```ruby
628
+ require 'pd4ml'
629
+
630
+ cover = Pd4ml.html_to_pdf("<h1 style='margin-top:300px; text-align:center'>Annual Report</h1>".b)
631
+ body = Pd4ml.html_to_pdf('<h2>Chapter 1</h2><p>...</p>'.b)
632
+
633
+ doc = Pd4ml.open_pdf(cover) # a binary String, IO, path or URL (+ password)
634
+ doc.append(Pd4ml.open_pdf(body)) # pages of another document after these
635
+ doc.set_title('Annual Report')
636
+ Pd4ml.write_pdf(doc, 'annual-report.pdf')
637
+
638
+ stamp = Pd4ml.html_to_pdf("<p style='color:#c00; font-size:40pt'>COPY</p>".b)
639
+ original = Pd4ml.open_pdf('annual-report.pdf')
640
+ original.overlay(Pd4ml.open_pdf(stamp), 128) # draw over every page, half opaque (0..255)
641
+ Pd4ml.write_pdf(original, 'annual-report-copy.pdf', password: 'secret')
642
+
643
+ puts Pd4ml.open_pdf('annual-report.pdf').get_number_of_pages
644
+ both = Pd4ml.merge_pdfs(cover, body) # two documents appended in one call
645
+ ```
646
+
647
+ `underlay` draws another document *under* the pages (a letterhead, for example);
648
+ `delete_page`, `get_page_size`, `get_metadata` and `dump_outlines` inspect and edit a document.
649
+ PD4ML's PDF toolkit (signing, optimisation, XFDF) is described in
650
+ [the PDF post-processing toolkit](https://pd4ml.com/docs/#15.-the-pdf-post-processing-toolkit)
651
+ and is reachable through `Pd4ml::Java.type(...)`.
652
+
653
+ ### 5.16 E-mail messages and saved web pages
654
+
655
+ PD4ML 4.2 reads e-mail messages (`.eml`) and web pages saved as a single file (`.mht`, `.mhtml`)
656
+ and writes them to any output format:
657
+
658
+ ```ruby
659
+ require 'pd4ml'
660
+
661
+ pd = Pd4ml::PD4ML.new
662
+ pd.read_eml('message.eml') # a path or URL, or the message itself: File.binread, an IO
663
+ pd.write_pdf('message.pdf') # the attachments are embedded in the PDF
664
+
665
+ page = Pd4ml::PD4ML.new
666
+ page.read_mht('page.mht') # a saved web page, with its images, styles and fonts
667
+ page.write_pdf('page.pdf')
668
+ ```
669
+
670
+ - A message becomes a block with From, Date, To, Cc, Subject and the attachments, followed by its
671
+ body: the HTML version if there is one, otherwise the plain text, with the images it embeds. The
672
+ attached files are embedded in the PDF next to their names (PDF/A-1 and PDF/A-2 and the other
673
+ outputs keep only the names).
674
+ - A saved page shows as it was saved, with the resources saved in the file; its links lead where
675
+ they led on the original page.
676
+ - `read_html` recognizes these files too, by the `.eml`, `.mht` or `.mhtml` ending or by how the
677
+ content starts. For mail from outside your control, use `read_eml` and `read_mht`.
678
+ - Of the resources a file refers to but does not contain, only `https://` ones are loaded.
679
+ `add_allowed_resource_location` ([5.17](#517-untrusted-html-restricting-resource-loading))
680
+ replaces that list. A file read from a path or URL must then be within the allowed locations
681
+ itself; one passed as content (a binary String or IO) need not be.
682
+ - Scripts do not run, the content of frames is not shown, and encrypted (S/MIME, PGP) messages
683
+ are not decrypted.
684
+
685
+ See [converting e-mail and saved web pages](https://pd4ml.com/docs/features/converting-e-mail-and-saved-web-pages.html).
686
+
687
+ ### 5.17 Untrusted HTML: restricting resource loading
688
+
689
+ HTML from users, customers or other systems decides what PD4ML loads: `<img src="file:/etc/...">`
690
+ reads local files, `http://169.254.169.254/...` calls services inside your network.
691
+ `add_allowed_resource_location` (PD4ML 4.2) limits loading to the locations you list:
692
+
693
+ ```ruby
694
+ require 'pd4ml'
695
+
696
+ user_html = '<p>Comment <img src="file:/etc/hosts"> <img src="http://169.254.169.254/latest/meta-data/"></p>'
697
+ pd = Pd4ml::PD4ML.new
698
+ pd.add_allowed_resource_location('https://cdn.example.com/assets/')
699
+ pd.add_allowed_resource_location('https://*.example.org/, file:/opt/app/templates/')
700
+ pd.read_html_string(user_html, 'https://cdn.example.com/assets/')
701
+ pd.write_pdf('comment.pdf') # without the two images
702
+ ```
703
+
704
+ - `locations` is a comma-separated list of URL prefixes; each call adds to the list. `*` matches
705
+ any characters except `/` (one host name or path segment). The comparison ignores case, and a
706
+ path containing `..` is always rejected.
707
+ - It covers style sheets, images, web fonts and attachments, and the document itself when it is
708
+ read from a path or URL. `data:` URIs, `java:` resources and the font directories of `use_ttf`
709
+ are not affected.
710
+ - A rejected resource is left out and the conversion goes on;
711
+ `set_error_policy(Pd4ml::ErrorPolicy::PEDANTIC)` makes it fail with a `Pd4ml::JavaError`
712
+ instead, and `set_log_level(Pd4ml::Constants::LOG_CACHE)` logs what was rejected.
713
+ - With no location added, resources load from anywhere. Call it before `read_html`.
714
+
715
+ See [restricting resource loading for untrusted HTML](https://pd4ml.com/docs/features/restricting-resource-loading-for-untrusted-html.html).
716
+
717
+ ### 5.18 Modifying the document before conversion (DOM API)
718
+
719
+ With the v42 layout engine, the document `read_html` parsed can be read, queried with CSS
720
+ selectors and changed before it is written; `layout` lays it out into pages without writing it
721
+ and tells where each element ended up.
722
+
723
+ > The DOM API (`get_document`, `layout` and the `Pd4ml::Dom` classes) is part of the **DMS and UA
724
+ > editions** of PD4ML 4.2. Without a license it can be evaluated (the output carries the
725
+ > evaluation watermark); with an STD or PRO license it works, but the output of a document it was
726
+ > used on carries "PD4ML EVALUATION MODE" banners. See [editions](https://pd4ml.com/products/).
727
+
728
+ ```ruby
729
+ require 'pd4ml'
730
+
731
+ pd = Pd4ml::PD4ML.new # the v42 engine
732
+ pd.read_html_string(<<~HTML)
733
+ <h1 id="title">Invoice</h1>
734
+ <table class="items"><tr><td>Consulting</td><td class="price">1 200,00</td></tr></table>
735
+ <img src="logo.png">
736
+ <div id="totals">Total: <span id="total"></span></div>
737
+ HTML
738
+
739
+ doc = pd.get_document
740
+ doc.get_element_by_id('total').set_text_content('1 250,00 EUR')
741
+ doc.query_selector_all('img:not([alt])').each { |img| img.set_attribute('alt', '') } # a Ruby Array
742
+ doc.query_selector('table.items').add_class('striped')
743
+ note = doc.create_element('p')
744
+ note.set_inner_html('Payable within <b>30 days</b>.')
745
+ doc.get_body.append_child(note)
746
+
747
+ layout = pd.layout # the pages as write_pdf would make them
748
+ totals = doc.get_element_by_id('totals')
749
+ totals.set_attribute('style', 'page-break-before: always') if layout.get_pages(totals).size > 1
750
+ layout.get_boxes(totals).each do |box|
751
+ puts format('page %d: %.1f, %.1f, %.1f x %.1f pt', box.get_page, box.get_x, box.get_y, box.get_width, box.get_height)
752
+ end
753
+ puts "#{layout.get_page_count} page(s), total in #{layout.get_computed_style(totals, 'color')}"
754
+ pd.write_pdf('invoice.pdf')
755
+ ```
756
+
757
+ | | Methods (Ruby-style; the Java names work too) |
758
+ |---|---|
759
+ | `Dom::Document` (`pd.get_document`) | `get_element_by_id`, `query_selector`, `query_selector_all`, `get_elements_by_tag_name`, `get_elements_by_class_name`, `get_head`, `get_body`, `get_title`, `create_element`, `create_text_node`, `create_comment` |
760
+ | `Dom::Node` (every node) | `get_parent_node`, `get_child_nodes`, `get_first_child`, `get_next_sibling`..., `append_child`, `insert_before`, `replace_child`, `remove_child`, `remove`, `clone_node`, `contains`, `get_text_content`, `set_text_content`, `to_html` |
761
+ | `Dom::Element` | `get_attribute`, `set_attribute`, `remove_attribute`, `get_attributes` (a Hash), `get_id`, `get_class_list`, `add_class`, `remove_class`, `toggle_class`, `get_inner_html`, `set_inner_html`, `get_outer_html`, `set_outer_html`, `matches`, `closest`, `get_children`... and the queries of `Document` within the element |
762
+ | `Dom::Layout` (`pd.layout`) | `get_page_count`, `get_page_width(page)`, `get_page_height(page)`, `get_page_number(page)`, `get_boxes(element)`, `get_pages(element)`, `get_computed_style(element, property)`, `element_at(page, x, y)` |
763
+ | `Dom::LayoutBox` | `get_page`, `get_x`, `get_y`, `get_width`, `get_height` (PDF points from the top left corner of the page) |
764
+
765
+ - Lists (`query_selector_all`, `get_child_nodes`, `get_boxes`, `get_pages`...) come back as Ruby
766
+ Arrays, `get_attributes` as a Hash. They are snapshots: removing or moving nodes while going
767
+ through one is safe.
768
+ - Two objects for the same node are `==` (Java `equals`), but not necessarily the same object.
769
+ - A layout is a snapshot too: after changing the document, call `layout` again. Pages are
770
+ numbered from 1.
771
+ - Change the document before the first output; every later write lays out the changed document.
772
+ - `get_document` and `layout` raise `Pd4ml::JavaError` (`IllegalStateException`) before
773
+ `read_html` or with the v40 engine.
774
+
775
+ The full method list is in [api.md](api.md#pd4mldomnode); the PD4ML guide is
776
+ [modifying the HTML document before conversion](https://pd4ml.com/docs/features/modifying-the-html-document-before-conversion.html).
777
+
778
+ ### 5.19 Web applications
779
+
780
+ Rails controller, with the HTML from a view:
781
+
782
+ <!-- no-test -->
783
+ ```ruby
784
+ class InvoicesController < ApplicationController
785
+ def show
786
+ invoice = Invoice.find(params[:id])
787
+ html = render_to_string(:show, layout: 'pdf', locals: { invoice: invoice })
788
+ pd = Pd4ml::PD4ML.new
789
+ pd.read_html_string(html, Rails.public_path.to_s) # base: where images/CSS live
790
+ send_data pd.write_pdf, type: 'application/pdf', disposition: 'inline',
791
+ filename: "invoice-#{invoice.number}.pdf"
792
+ end
793
+ end
794
+ ```
795
+
796
+ Sinatra:
797
+
798
+ <!-- no-test -->
799
+ ```ruby
800
+ require 'sinatra'
801
+ require 'pd4ml'
802
+
803
+ get '/report.pdf' do
804
+ pd = Pd4ml::PD4ML.new
805
+ pd.read_html_string(erb(:report), settings.public_folder)
806
+ content_type 'application/pdf'
807
+ pd.write_pdf
808
+ end
809
+ ```
810
+
811
+ To pay the cost of starting Java at boot instead of on the first request, touch PD4ML in an
812
+ initializer (`Pd4ml.version`). Each Puma worker process starts its own bridge; set
813
+ `PD4ML_BRIDGE` to share one bridge server between them.
814
+
815
+ ### 5.20 Batch conversion
816
+
817
+ ```ruby
818
+ require 'pd4ml'
819
+
820
+ Dir.glob('*.html').each do |source|
821
+ pd = Pd4ml::PD4ML.new # one converter per document
822
+ pd.read_html(source)
823
+ pd.write_pdf(source.sub(/\.html\z/, '.pdf'))
824
+ puts "written #{source}"
825
+ end
826
+ ```
827
+
828
+ One bridge serves the whole loop. To convert in parallel, run several processes (each with its
829
+ own bridge), or point them all at one bridge server.
830
+
831
+ ### 5.21 Licensing
832
+
833
+ <!-- no-test -->
834
+ ```ruby
835
+ require 'pd4ml'
836
+
837
+ pd = Pd4ml::PD4ML.new(license: 'YOUR-SERIAL-NUMBER') # or a license file URL; or pd4ml.lic in the working dir / classpath
838
+ puts 'unlicensed: output is watermarked' if pd.demo_mode?
839
+ puts "PD4ML #{Pd4ml.version}#{pd.pro? ? ' pro' : ''}"
840
+ ```
841
+
842
+ Without a license, PD4ML is fully functional but marks its output. See
843
+ [licensing](https://pd4ml.com/docs/#3.-licensing).
844
+
845
+ ---
846
+
847
+ ## 6. Tips and hints
848
+
849
+ - **Start the bridge early.** The first use starts Java. Touch PD4ML when the application
850
+ boots, or use a bridge server for short-lived processes.
851
+ - **One converter per document.** `Pd4ml::PD4ML.new` is cheap; reuse the bridge, not the object.
852
+ - **Set the base location for HTML from strings.** Missing images and unstyled output are almost
853
+ always caused by unresolved relative links ([details](https://pd4ml.com/docs/#6.2-reading-the-source)).
854
+ - **Settings go before `read_html`.** Page size, headers, fonts and styles apply to the layout
855
+ `read_html` makes; writes afterwards only choose the output format.
856
+ - **Write several formats from one read.** PDF, DOCX and page images from a single `read_html`
857
+ cost one layout.
858
+ - **Binary in, binary out.** Pass content as a binary String (`File.binread`, `.b`) or an IO;
859
+ results are binary Strings, ready for `File.binwrite` or `send_data`.
860
+ - **Print CSS works.** `@page { size: A4 landscape; margin: 15mm }`, `@media print` and
861
+ `page-break-*` / `break-*` are honoured. They are often simpler than API calls.
862
+ - **Control the scale with the HTML width.** If text is too small, or a layout wraps
863
+ differently than in the browser, set `set_html_width` to the width the page was designed for.
864
+ - **Index fonts once** with `-configure.fonts`, instead of pointing `use_ttf` at a large
865
+ unindexed directory on every conversion.
866
+ - **Restrict what documents can load** when converting untrusted HTML, with
867
+ `add_allowed_resource_location` ([5.17](#517-untrusted-html-restricting-resource-loading)); also
868
+ consider `set_param(Pd4ml::Constants::PD4ML_DISABLE_EXTERNAL_ATTACHMENTS, 'true')`.
869
+ - **Pin the build in production.** Use `pd4ml install <version>` or a `PD4ML_CLASSPATH` to a
870
+ jar you keep, rather than following the latest release.
871
+
872
+ ## 7. Troubleshooting
873
+
874
+ | Symptom | Cause and remedy |
875
+ |---|---|
876
+ | `Pd4ml::BridgeError: PD4ML not found` | No build is configured and the automatic download is off (`PD4ML_AUTO_INSTALL=0`) or failed: run `pd4ml install`, or set `PD4ML_CLASSPATH`. |
877
+ | `Pd4ml::BridgeError: cannot start java` / "closed the connection" | Java is missing or failed. Check `java -version`, set `JAVA_HOME` or `PD4ML_JAVA`; `PD4ML_BRIDGE_LOG=file` keeps Java's output. |
878
+ | `Pd4ml::BridgeError: cannot connect to the PD4ML bridge` | The bridge server is not running, or `PD4ML_BRIDGE` names another port. |
879
+ | A pause and a "downloading" warning on the first conversion | The automatic first-use download ([2.2](#22-install-pd4ml)). Run `pd4ml install` at deployment instead. |
880
+ | `Errno::ENOENT` reading the document | A text String is taken as a path: pass HTML content with `read_html_string`, or as a binary String (`.b`). |
881
+ | Images or styles missing | Relative links have no base location, or the server cannot reach the resources (proxies, authentication). See `set_cookie` and `set_http_options`. |
882
+ | Output is watermarked | No license is installed ([licensing](https://pd4ml.com/docs/#3.-licensing)). |
883
+ | Squares instead of characters | The fonts lack those glyphs. Provide fonts with `use_ttf` ([fonts](https://pd4ml.com/docs/#9.-fonts-bookmarks-and-hyperlinks)). |
884
+ | `NoClassDefFoundError: org/bouncycastle/...` with `set_permissions` | The Bouncy Castle jars are missing. Install without `--no-bouncycastle`, or add them to the classpath. |
885
+ | `NoClassDefFoundError: com/sun/media/jai/...` with `render_as_images` / `render_png` | PD4ML 4.1.1 and earlier need the Java Advanced Imaging jars for page images. Use a newer PD4ML build, or add `jai-core` and `jai-codec` 1.1.3 (from the JBoss Maven repository) to the classpath. |
886
+ | 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. |
887
+ | `Pd4ml::JavaError: ... NoSuchMethodException` | No Java method of that name takes those argument types. Check [api.md](api.md); pass floats as `10.0`, binary data as a binary String. |
888
+
889
+ ## 8. API reference
890
+
891
+ ### Module `Pd4ml`
892
+
893
+ | | |
894
+ |---|---|
895
+ | `Pd4ml::PD4ML` | the converter: `Pd4ml::PD4ML.new(license:, html_width:, engine:)`; every Java method of `com.pd4ml.PD4ML` |
896
+ | `PageSize`, `PageMargins`, `Units`, `Dimensions`, `Constants`, `LayoutEngine`, `ErrorPolicy`, `PdfSpec`, `PdfDocument`, `ProgressListener`, `StatusMessage`, `ResourceProvider`, `CustomTag` | the PD4ML Java classes ([api.md](api.md)) |
897
+ | `Dom::Document`, `Dom::Node`, `Dom::Element`, `Dom::Text`, `Dom::Comment`, `Dom::DocumentType`, `Dom::Layout`, `Dom::LayoutBox` | the DOM API classes, from `pd.get_document` and `pd.layout` ([5.18](#518-modifying-the-document-before-conversion-dom-api)); their lists come back as Arrays |
898
+ | `html_to_pdf(source, target, base_url:, page_size:, page_margins:, spec:, engine:, html_width:)`, `open_pdf(source, password)`, `write_pdf(doc, target, password:, permissions:)`, `merge_pdfs(a, b, target)` | one-call conversion and PDF documents |
899
+ | `version`, `layout_engines?`, `to_url(location)`, `pdf_spec(keyword)` | runtime helpers |
900
+ | `configure(**options)`, `shutdown` | the bridge: `classpath:`, `java:`, `jvm_options:`, `server:`, `log_file:` |
901
+ | `install(version, **options)`, `installed_jar`, `installed_classpath`, `installed_version`, `available_versions` | PD4ML builds |
902
+ | `Java.type(name)`, `Java.proxy(iface) { \|method, args\| ... }` | any Java class, Java interfaces implemented by Ruby blocks |
903
+ | `JavaError`, `BridgeError`, `CallbackError`, `InstallError` | errors |
904
+
905
+ ### `Pd4ml::PD4ML` methods with Ruby additions
906
+
907
+ | Method | |
908
+ |---|---|
909
+ | `read_html(source, base_url = nil, encoding = nil)` | `source`: a path or URL (String, Pathname, URI), a binary String or IO, or a Java `URL`/`InputStream` |
910
+ | `read_html_string(html, base_url = nil)` | the document as a String |
911
+ | `read_eml(source)`, `read_mht(source)` | an e-mail message, a saved web page ([5.16](#516-e-mail-messages-and-saved-web-pages)); `source` as for `read_html` |
912
+ | `write_pdf(target = nil, spec: nil)` | `target`: a path or IO; none: returns a binary String. `spec`: a `PdfSpec`, a keyword (`'pdf/a-2b'`) or `'PDF'`/`'PDFA'`/`'PDFUA'` |
913
+ | `write_docx(target = nil)`, `write_rtf(target = nil, rtf_wmf: false)` | as `write_pdf` |
914
+ | `render_as_images(format)`, `render_png` | pages as encoded images (binary Strings); `render_as_images(dir, name, format)` writes files |
915
+ | `monitor_progress_with { \|message_id, progress, note, msec\| ... }` | a block, or a Java `ProgressListener` |
916
+ | `use_html_renderer(engine, html_width: nil, params: nil)` | the layout engine by name; `pd.engine` tells which one is in use |
917
+
918
+ The Java names of these methods (`readHTML`, `writePDF`...) work too. All other methods are those
919
+ of the Java class `com.pd4ml.PD4ML` ([api.md](api.md#pd4mlpd4ml),
920
+ [Java API](https://pd4ml.com/docs/#6.-the-java-api)); static ones are called on the class
921
+ (`Pd4ml::PD4ML.get_version`). The Java object itself is `pd.java`.
922
+
923
+ `ResourceProvider` and `CustomTag` are extended in **Java**: register yours with
924
+ `add_custom_resource_provider` / `add_custom_tag_handler`, and put its classes on the classpath.
925
+ Java *interfaces* (such as `ProgressListener`) can be implemented in Ruby with
926
+ `Pd4ml::Java.proxy`. Examples: [resource loaders](https://github.com/zxfr/pd4ml-examples/blob/master/src/main/java/advanced/A005AddCustomResourceLoader.java),
927
+ [custom tags](https://github.com/zxfr/pd4ml-examples/blob/master/src/main/java/advanced/A008AddingCustomTagRenderer.java).
928
+
929
+ ## 9. Further reading
930
+
931
+ - [PD4ML documentation](https://pd4ml.com/docs/) and the full [programmer's manual](https://pd4ml.com/docs/pd4ml-manual.html)
932
+ - [Usage examples](https://pd4ml.com/docs/usage-examples.html)
933
+ - [HTML5 element support matrix](https://pd4ml.com/docs/html5-element-support-matrix.html)
934
+ - [Command-line tool reference](https://pd4ml.com/docs/pd4cmd-command-line-reference.html)
935
+ - [ZUGFeRD and Factur-X invoices](https://pd4ml.com/docs/features/zugferd-and-factur-x-pdf-invoices.html)
936
+ - [PDF post-processing toolkit](https://pd4ml.com/docs/#15.-the-pdf-post-processing-toolkit)
937
+ - [Java examples on GitHub](https://github.com/zxfr/pd4ml-examples)