@reportwright/pdf 0.1.0-beta.10 → 0.1.0-beta.11

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.
package/README.md CHANGED
@@ -1,41 +1,39 @@
1
- # @reportwright/pdf
2
-
3
- **Create, comply, sign, light edit: a streaming PDF library for Node and browsers with zero runtime dependencies.**
4
-
5
- [![npm](https://img.shields.io/npm/v/@reportwright/pdf?color=1f4e79)](https://www.npmjs.com/package/@reportwright/pdf)
6
- [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
- [![dependencies: 0](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)](./package.json)
8
- [![node >= 20.16](https://img.shields.io/badge/node-%3E%3D20.16-339933)](./package.json)
9
- [![PDF/A + PDF/UA: veraPDF](https://img.shields.io/badge/PDF%2FA%201b%E2%80%933a%20%2B%20UA--1-veraPDF%20clean-6f42c1)](./conformance)
10
-
11
- **Live demo:** [mrarun005.github.io/reportwright-demo](https://mrarun005.github.io/reportwright-demo) ·
12
- [PDF benchmarks](https://mrarun005.github.io/reportwright-demo/bench.html) ·
13
- [playground](https://mrarun005.github.io/reportwright-demo/play.html)
14
-
15
- Pages are written to your sink as they end, so memory stays flat however many pages you write. It is the writer inside
16
- ReportWright, extracted, plus a hardened reader for merging, splitting, filling and signing existing files.
17
-
18
- What it is for: **create, comply, sign, light edit**. It does not render PDFs: for rendering use
19
- [pdf.js](https://mozilla.github.io/pdf.js/) or [PDFium](https://pdfium.googlesource.com/pdfium/). For heavy
20
- rewriting, repair or raw speed, MuPDF and qpdf are the established tools; this library does not claim to be faster
21
- than them.
22
-
23
- - **Write**: text in the 14 standard fonts or embedded TrueType/OpenType (subset, kerned, any Unicode), complex
24
- scripts through a HarfBuzz shaper, JPEG/PNG images, Canvas-style vector paths, gradients, patterns, layers.
25
- - **Navigate**: links, named destinations, bookmarks, page labels, annotations.
26
- - **Forms**: every AcroForm field type with appearance streams; fill and flatten existing forms.
27
- - **Protect**: AES-256 / AES-128 encryption with permissions; PAdES B-B / B-T signatures while streaming.
28
- - **Comply**: PDF/A-1b, 2b/2u/2a, 3b/3u/3a and PDF/UA-1 (veraPDF clean), Factur-X / ZUGFeRD / XRechnung.
29
- - **Read and edit**: load (also damaged or encrypted files), merge, split, reorder, extract text, fill, stamp,
30
- `saveIncremental` that keeps existing signatures valid.
31
- - **Safe by default**: no JavaScript or launch actions can be written; active content is stripped on read; every
32
- input is budgeted.
1
+ <h1 align="center">@reportwright/pdf</h1>
2
+
3
+ <p align="center">
4
+ <b>Create, comply, sign, light edit.</b><br>
5
+ A streaming PDF library for Node and browsers with <b>zero runtime dependencies</b>.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/@reportwright/pdf"><img alt="npm" src="https://img.shields.io/npm/v/@reportwright/pdf?color=1f4e79"></a>
10
+ <a href="https://github.com/MrArun005/reportwright-pdf/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/MrArun005/reportwright-pdf/actions/workflows/ci.yml/badge.svg"></a>
11
+ <a href="./LICENSE"><img alt="license: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
12
+ <a href="./package.json"><img alt="runtime dependencies: 0" src="https://img.shields.io/badge/runtime%20dependencies-0-brightgreen"></a>
13
+ <a href="./package.json"><img alt="node >= 20.16" src="https://img.shields.io/badge/node-%3E%3D20.16-339933"></a>
14
+ <a href="./conformance"><img alt="PDF/A + PDF/UA: veraPDF clean" src="https://img.shields.io/badge/PDF%2FA%201b%E2%80%933a%20%2B%20UA--1-veraPDF%20clean-6f42c1"></a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="https://mrarun005.github.io/reportwright-demo">Live demo</a> ·
19
+ <a href="https://mrarun005.github.io/reportwright-demo/play.html">Playground</a> ·
20
+ <a href="#benchmarks">Benchmarks</a> ·
21
+ <a href="#security-model">Security model</a>
22
+ </p>
23
+
24
+ Pages are written to your sink as they end, so memory stays flat however many pages you write: 20,000 pages hold
25
+ about the heap that 1,000 do. It is the writer inside ReportWright, extracted, plus a hardened reader for merging,
26
+ splitting, filling and signing existing files.
27
+
28
+ It does **not** render PDFs (use [pdf.js](https://mozilla.github.io/pdf.js/) or
29
+ [PDFium](https://pdfium.googlesource.com/pdfium/)), and for heavy rewriting, repair or raw speed MuPDF, qpdf and
30
+ pdfcpu are faster (see [Benchmarks](#benchmarks)).
33
31
 
34
32
  ## Contents
35
33
 
36
- [Install](#install) · [Quick start](#quick-start-30-seconds) · [Guide](#guide) · [Examples](#examples) ·
37
- [Comparison](#comparison) · [Options reference](#options-reference) · [Security model](#security-model) ·
38
- [Known limits](#known-limits) · [Changes](#changes)
34
+ [Install](#install) · [Quick start](#quick-start-30-seconds) · [Features](#features) · [Screenshots](#screenshots) ·
35
+ [Guide](#guide) · [Bundle size](#bundle-size) · [Examples](#examples) · [Benchmarks](#benchmarks) · [Options reference](#options-reference) ·
36
+ [Security model](#security-model) · [Known limits](#known-limits) · [Releasing](#releasing) · [Changes](#changes)
39
37
 
40
38
  ## Install
41
39
 
@@ -43,8 +41,8 @@ than them.
43
41
  npm i @reportwright/pdf
44
42
  ```
45
43
 
46
- ESM only, Node 20.16 or later, and modern browsers (the same `dist/index.js`; crypto comes from WebCrypto). Types are
47
- included (`index.d.ts`). Optional, for complex scripts only: `npm i harfbuzzjs`.
44
+ ESM only. Node 20.16 or later, and modern browsers (the same `dist/index.js`; crypto comes from WebCrypto). Types are
45
+ included (`index.d.ts`). Optional, for complex scripts (Indic, Arabic) only: `npm i harfbuzzjs`.
48
46
 
49
47
  ## Quick start (30 seconds)
50
48
 
@@ -63,6 +61,40 @@ console.log(await pdf.end()); // { pages: 1,
63
61
 
64
62
  In memory instead of a stream: `const bytes = await toBytes(async (pdf) => { … }, options)`.
65
63
 
64
+ ## Features
65
+
66
+ | Area | What you get |
67
+ |---|---|
68
+ | **Write** | Text in the 14 standard fonts or embedded TrueType/OpenType (subset, kerned, any Unicode); text boxes with wrapping and alignment; JPEG/PNG images (alpha as a soft mask); Canvas-style vector paths, gradients, patterns, layers |
69
+ | **Complex scripts** | Devanagari, Bengali, Tamil, Telugu, Kannada, Arabic… through a HarfBuzz shaper you plug in (`harfbuzzjs`), text still extractable |
70
+ | **Streaming** | Each page is written and freed when it ends; flat memory for 20,000+ pages; `timeoutMs` and `AbortSignal` |
71
+ | **Navigate** | Links, named destinations, bookmarks, page labels, annotations |
72
+ | **Forms** | Every AcroForm field type with appearance streams; fill and flatten existing forms |
73
+ | **Protect** | AES-256 / AES-128 encryption with permissions |
74
+ | **Sign** | PAdES B-B / B-T signatures while streaming; incremental countersignatures that keep earlier ones valid |
75
+ | **Comply** | PDF/A-1b, 2b/2u/2a, 3b/3u/3a and PDF/UA-1 (veraPDF clean); Factur-X / ZUGFeRD / XRechnung invoices |
76
+ | **Read and edit** | Load (also damaged or encrypted files), merge, split, reorder, extract text, fill, stamp, `saveIncremental` |
77
+ | **Bulk** | One task over many files in worker threads, each with a heap cap and a per-file timeout; one bad file never stops the batch |
78
+ | **Safe by default** | No JavaScript or launch actions can be written; active content is stripped on read; every input is budgeted |
79
+ | **Small** | Zero runtime dependencies; one ESM file plus a lazily loaded `fflate.js`; TypeScript types included |
80
+
81
+ ## Screenshots
82
+
83
+ Page 1 of the [examples](#examples), rendered by poppler (`pdftoppm`) on GitHub Actions straight from their output.
84
+
85
+ <table>
86
+ <tr>
87
+ <td align="center"><img src="https://raw.githubusercontent.com/MrArun005/reportwright-pdf/main/docs/screenshots/write.png" width="240" alt="write.mjs: a report with a logo, a justified paragraph, a table drawn with paths and a link"><br><code>write.mjs</code><br>text, image, table, link</td>
88
+ <td align="center"><img src="https://raw.githubusercontent.com/MrArun005/reportwright-pdf/main/docs/screenshots/invoice-facturx.png" width="240" alt="invoice-facturx.mjs: a Factur-X PDF/A-3a invoice in Inter"><br><code>invoice-facturx.mjs</code><br>Factur-X, PDF/A-3a</td>
89
+ <td align="center"><img src="https://raw.githubusercontent.com/MrArun005/reportwright-pdf/main/docs/screenshots/indic.png" width="240" alt="indic.mjs: Devanagari text shaped by HarfBuzz mixed with English"><br><code>indic.mjs</code><br>Devanagari via HarfBuzz</td>
90
+ </tr>
91
+ <tr>
92
+ <td align="center"><img src="https://raw.githubusercontent.com/MrArun005/reportwright-pdf/main/docs/screenshots/fill-form.png" width="240" alt="fill-form.mjs: a leave request form, filled in"><br><code>fill-form.mjs</code><br>a form, filled</td>
93
+ <td align="center"><img src="https://raw.githubusercontent.com/MrArun005/reportwright-pdf/main/docs/screenshots/accessible.png" width="240" alt="accessible.mjs: a tagged PDF/UA-1 and PDF/A-2a policy page"><br><code>accessible.mjs</code><br>PDF/UA-1 + PDF/A-2a, tagged</td>
94
+ <td></td>
95
+ </tr>
96
+ </table>
97
+
66
98
  ## Guide
67
99
 
68
100
  Every snippet below is cut from a file in [`examples/`](./examples), each of which was run and produces a PDF. The
@@ -297,6 +329,21 @@ from the best of 3 calls in one process (warm), and the first call's ms (cold);
297
329
  | large | encrypt | 1 | 149 | 155 | 213 | 212 | 181 |
298
330
  | large | encrypt | 4 | 93 | 217 | 218 | 210 | 416 |
299
331
 
332
+ ## Bundle size
333
+
334
+ Gzip (`gzip -9`) of the built files in `dist/`. `.` is `dist/index.js` (writer and reader); `./write` is `dist/write.js`
335
+ (writer only: `createPdf`, fonts, images, graphics, forms, standards, encryption, signing). fflate is a separate file
336
+ (`dist/fflate.js`) loaded on first use by either entry.
337
+
338
+ | entry | before (unminified) | after (minified) |
339
+ | --- | --- | --- |
340
+ | `.` (`dist/index.js`) | 166.8 KiB (170,811 B) | 133.4 KiB (136,583 B) |
341
+ | `./write` (`dist/write.js`) | not exported | 85.0 KiB (87,080 B) |
342
+ | `fflate` (`dist/fflate.js`) | 15.5 KiB (15,842 B) | 12.6 KiB (12,953 B) |
343
+
344
+ Import `@reportwright/pdf/write` when the code only writes PDFs: it leaves out the reader (`loadPdf`, `mergePdfs`) and
345
+ `bulk`. The two entries are separate bundles, so an app that imports both pays for the writer twice.
346
+
300
347
  ## Examples
301
348
 
302
349
  | file | what it shows | run |
@@ -315,30 +362,41 @@ from the best of 3 calls in one process (warm), and the first call's ms (cold);
315
362
 
316
363
  Run from `node_modules/@reportwright/pdf` (or the package folder of the repository).
317
364
 
318
- ## Comparison
319
-
320
- Positioning: **create, comply, sign, light edit**. Only measured facts: `bench/compare.mjs` (Node 20, Apple Silicon laptop, median of 10;
321
- reading cases median of 3) and the beta.0 measurements. Where something was not measured the cell says so. MuPDF and qpdf are faster at rewriting and repair: this table does not compare against them.
322
-
323
- | | @reportwright/pdf | pdf-lib 1.17.1 | PDFKit | jsPDF |
324
- |---|---|---|---|---|
325
- | Runtime dependencies | 0 | 4 (pako, tslib, @pdf-lib/standard-fonts, @pdf-lib/upng) | not measured | not measured |
326
- | Package size | 161 KB gzipped (dist/index.js, unminified, beta.2) | not measured | not measured | not measured |
327
- | 10,000 pages | 3.2 s, 123 MB (beta.0); heap 5.0 MB at 20,000 pages (streaming) | not measured | not measured | not measured |
328
- | Cold start, new process | 91 ms (beta.0); 43 ms for a 1-page invoice now | 96 ms (1-page invoice) | not measured | not measured |
329
- | 1,000-page text: bytes / warm ms | 461,320 / 134 | 1,421,783 / 918 | not measured | not measured |
330
- | 1,000 vector shapes: bytes / warm ms | 8,353 / 5.3 | 22,273 / 20.5 | not measured | not measured |
331
- | 200-field form, filled: bytes / warm ms | 63,095 / 15.4 | 165,910 / 97.0 | not measured | not measured |
332
- | Merge 10 ten-page PDFs: warm ms | 11.6 | 14.5 | not measured | not measured |
333
- | Split 100 pages into 100 files: warm ms | 33.7 | 30.3 (faster) | not measured | not measured |
334
- | Real-world PDFs read | 196 of 206 (beta.0 corpus) | not measured | not measured | not measured |
335
- | AES encryption | AES-256, AES-128 | cannot encrypt | not measured | not measured |
336
- | Digital signatures | PAdES B-B / B-T, incremental countersign | cannot sign | not measured | not measured |
337
- | PDF/A, PDF/UA | 1b, 2b/2u/2a, 3b/3u/3a, UA-1: veraPDF 0 failures | not measured | not measured | not measured |
338
- | Factur-X / ZUGFeRD | yes | not measured | not measured | not measured |
339
-
340
- The split files are larger than pdf-lib's (each carries ~1 KB of XMP and Info). PDFKit and jsPDF were not installed
341
- where the benchmark ran, so they have no numbers here; run `bench/compare.mjs` where they are. Full tables and method: `NOTES.md` ("Measurements") and the [benchmarks page](https://mrarun005.github.io/reportwright-demo/bench.html).
365
+ ## Benchmarks
366
+
367
+ From the deep benchmark of the ReportWright project: every library in its own process, median of 5 runs after a
368
+ warm-up, contestants interleaved on the same GitHub `ubuntu-latest` runner (2 vCPU), every output checked (qpdf,
369
+ MuPDF, pdf.js or the job's own text check) before it is ranked. Times are wall time from process start to exit, so they include each runtime's
370
+ start-up. Full tables, method and raw numbers:
371
+ **[bench/deep/RESULTS.md](https://github.com/MrArun005/reportwright/blob/bench-deep/bench/deep/RESULTS.md)**
372
+ (the `@reportwright/pdf` measured there is the 0.1.0-beta.7 code).
373
+
374
+ **Where it wins**
375
+
376
+ | Task | @reportwright/pdf | Next best |
377
+ |---|---|---|
378
+ | PDF/A-2b invoice (veraPDF-checked) | 255 ms · 75 MB | pdfkit 543 ms · 103 MB (the only other writer with PDF/A in the test) |
379
+ | Fill and flatten a form | **100 ms** | PyMuPDF / pypdf 139 ms, pdf-lib 296 ms, pdfbox 669 ms |
380
+ | Sign (PAdES/CMS) | **~100 ms** | pdfbox 705 ms, openpdf 760 ms, iText 1,113 ms |
381
+ | Incremental metadata change | 100-125 ms | PyMuPDF / pypdf 139 ms, pdfbox 463 ms |
382
+ | 1,000 pages of text, against other JS writers | 3.1 s · 135 MB | pdfkit 4.5 s, jsPDF 5.8 s, pdf-lib 215 s |
383
+ | Reading fuzzed PDFs (200 mutations) | 182 read, 18 clean errors, 0 crashes or hangs | pdfbox 176 read; most readers refuse far more |
384
+
385
+ **Where it loses** (honestly)
386
+
387
+ | Task | @reportwright/pdf | Fastest |
388
+ |---|---|---|
389
+ | Plain text pages (10 / 100 / 1,000) | 0.30 / 0.65 / 3.1 s | fpdf (PHP) 0.05 / 0.11 / 0.57 s: **5-6× faster** |
390
+ | Grouped report, 100 to 100,000 rows | 2.2-5.2× slower | fpdf |
391
+ | One invoice in a warm process | 11 ms | PDFsharp (.NET) 1.7× faster |
392
+ | Images (6 MP PNG + 24 MP JPEG) | 1.2 s | printpdf (Rust) 3.3× faster |
393
+ | Unicode (Hindi, Arabic, CJK) | 3.1× slower | fpdf |
394
+ | Text of a 1,200-page PDF | 1.0 s | pdftotext 0.65 s (1.5× faster) |
395
+ | Merge 100 files / split 100 pages / AES-256 encrypt | 204 / 217 / 147 ms | pdfcpu (Go) 3-4.3× faster |
396
+
397
+ In short: it is not the fastest PDF writer or reader. It is a fast JavaScript one with flat memory, and it is the
398
+ quickest of those measured at the jobs it is built for (compliance, forms, signing, incremental edits), where most
399
+ libraries have no support at all.
342
400
 
343
401
  ## Options reference
344
402
 
@@ -394,11 +452,15 @@ writer to be ready. `pdf.end()` ends a Node stream and closes a `WritableStream`
394
452
  - `await pdf.embedFont(bytes, { subset, shaper, textMapping })` — a TrueType (`glyf`) or OpenType/CFF (`.otf`) font. Any Unicode
395
453
  text (Identity-H with a ToUnicode map, so text can be copied and searched). TrueType embeds as `FontFile2`
396
454
  (CIDFontType2); OpenType/CFF as `FontFile3 /Subtype /OpenType` (CIDFontType0; `pdffonts` says "CID Type 0C (OT)").
397
- `subset`: `true` (default) keeps only the glyphs used, both kinds built in (CFF: unused glyphs' charstrings become
455
+ `subset`: `true` (default) keeps only the glyphs used, both kinds built in (TrueType: the glyphs used, their
456
+ components and `.notdef` are renumbered 0…n and only `glyf loca head hhea hmtx maxp` and the hinting tables
457
+ `cvt fpgm prep` are kept; the content still draws by the original glyph ID, through a `/CIDToGIDMap` stream, so
458
+ ToUnicode and widths are unchanged. CFF: unused glyphs' charstrings become
398
459
  `endchar`, subroutines are kept, glyph IDs stay; a font using the deprecated `seac` accents embeds whole; a
399
460
  CID-keyed CFF gets an identity charset so every reader maps its glyphs alike); `false` embeds the whole font; or a
400
461
  function `(font, codePoints, { glyphs }) => Promise<Uint8Array>` (e.g. a HarfBuzz subsetter, which also
401
- desubroutinises CFF) that must keep glyph IDs. `shaper`: see Shaping.
462
+ desubroutinises CFF) that must keep glyph IDs (its font is embedded as returned, `/CIDToGIDMap /Identity`, as
463
+ `core` callers that write their own fonts expect). `shaper`: see Shaping.
402
464
  A variable font (an `fvar` table) is drawn at its default instance, which may be its thinnest weight (Noto Sans JP's
403
465
  is Thin), with a warning naming the axes' values: for another weight, embed a static instance of it (fonttools
404
466
  `varLib.instancer`, or `hb-subset --instance`).
@@ -797,6 +859,11 @@ f.signature('sig', { page, x: 40, y: 360, width: 200, height: 40, tooltip: 'Sign
797
859
  - An embedded font in a fillable field is subset to the characters drawn; for fields people will type into, embed it
798
860
  with `subset: false` so a viewer has every glyph.
799
861
 
862
+ A caller that draws its own appearances (pdf.core users) passes `appearance: { normal }` (a checkbox: `{ on, off }`)
863
+ of content-stream operators, written exactly, and `da` (`'/F1 12 Tf 0 g'`, a font it registered with
864
+ `core.resource('Font', …)`): with both, no `font` is needed. Each field's `widgetRef` is its widget's object number,
865
+ and `structParent` puts a `/StructParent` on it for a structure tree of the caller's own.
866
+
800
867
  ### Encryption
801
868
 
802
869
  ```js
@@ -858,6 +925,11 @@ await pdf.end(); // the signer runs here
858
925
  AcroForm gets `/SigFlags 3`. **One signature** a document (more need incremental updates).
859
926
  - Works with encryption (the signature's `/Contents` is not encrypted, as the spec requires) and with PDF/A-2b.
860
927
 
928
+ An invisible signature: `pdf.sign({ name: 'Signature1' }, { signer })` (no page, no size) is a zero-`/Rect` field in
929
+ the AcroForm only, so it works after every page is written (a document built through `pdf.core`); with `page` (still
930
+ open) it goes in that page's `/Annots`. A PKCS#12 (.p12/.pfx) file signs through `cmsSigner`:
931
+ `examples/p12-signer.mjs` (node-forge, which you install; not a dependency) gives `p12Signer(bytes, password)`.
932
+
861
933
  ### Tagged PDF
862
934
 
863
935
  `page.tag(type, fn, options)` options: `alt` (`/Alt`), `actualText` (`/ActualText`: the text the element stands for),
@@ -961,6 +1033,34 @@ default configuration `/D` (`/Name`, `/Order` listing every layer, `/ON`, `/OFF`
961
1033
  - `await pdf.end()` — ends open pages, writes fonts, outline, metadata and the cross-reference stream; resolves to
962
1034
  `{ pages, bytes, warnings }`.
963
1035
 
1036
+ ### Extension API (core)
1037
+
1038
+ `pdf.core` (typed `PdfCore` in index.d.ts) is the layer every feature module is written on, for a caller that writes
1039
+ its own content streams and objects beside the Pdf methods (the ReportWright engine does). Not covered by semver in
1040
+ 0.x; ARCHITECTURE.md ("Extension points") has the details.
1041
+
1042
+ ```js
1043
+ const pdf = createPdf(sink);
1044
+ const { core } = pdf;
1045
+ const n = core.reserve(); // an object number, written later
1046
+ await core.write(n, '<< /Type /ExtGState /ca 0.5 >>'); // small objects go into object streams
1047
+ const gs = core.resource('ExtGState', `${n} 0 R`); // a name in every page's shared Resources
1048
+ core.catalog('PageLabels', '<< /Nums [0 << /S /r >>] >>');
1049
+ core.onFinish(async () => {
1050
+ /* runs at pdf.end(), after the last page, before the fonts and the catalog */
1051
+ });
1052
+ ```
1053
+
1054
+ - `reserve()`, `write(n, dict, stream?)`, `writeStream(n, dict, raw)`, `writeBig(n, head, parts, tail)`,
1055
+ `object(dict, stream?)` — objects; a stream's bytes are already filtered (`/Length` is added).
1056
+ - `resource(kind, value, name?)`, `resourcesRef` — the shared Resources dictionary.
1057
+ - `imageResource(image)` → `{ name, ref, alpha, orientation }` — an image from `pdf.embedImage` for your own content
1058
+ stream: `q w 0 0 h x y cm <orientation> cm /<name> Do Q` (with `alpha`, give the page a transparency `/Group`).
1059
+ - `catalog(key, value)`, `onFinish(fn)`, `info`, `xmp` — the end of the document; `info` set to a dictionary's text
1060
+ replaces the Info made from the options.
1061
+ - `pageRef(i)`, `pageHeight(i)`, `pageCount`, `warn(msg)`, `serial(fn)`, `tags` — pages, warnings, ordering, tagging.
1062
+ - In an encrypted file, every string an object holds (literal or hex) is encrypted as it is written.
1063
+
964
1064
  ### Reading and modifying
965
1065
 
966
1066
  ```js
@@ -1128,11 +1228,42 @@ widgets may cover at most 40% of a page in all, and a widget with an opaque back
1128
1228
 
1129
1229
  ## Known limits
1130
1230
 
1131
- Image masks (stencils) and colour-managed images (embedded ICC profiles of PNG/JPEG files are ignored); the Unicode
1132
- bidi algorithm, hyphenation, kerning of the standard fonts, WOFF fonts; popup annotations; RC4 encryption (refused; read only), public-key (certificate) encryption (neither written nor read), verifying signatures' cryptography (they are listed with their coverage); PDF/A-1a, PDF/X, attachment annotations in PDF/A-3 (use `pdf.attach`), validating a Factur-X invoice's content; layout-perfect text extraction; page edits in an incremental update (use `save()`); reading encrypted files in browsers (WebCrypto has no synchronous AES; Node only); with `textMapping: 'actualText'`, readers that ignore ActualText or close it early (pdf.js; a tester saw an older MuPDF drop a line's last Bengali cluster) lose the spanned clusters: the writer closes every span after its cluster's last glyph, and MuPDF 1.28.2 (PyMuPDF) reads Bengali, Devanagari and the other test scripts whole. See NOTES.md for the plan. Extending the library: ARCHITECTURE.md.
1231
+ - **Images**: image masks (stencils); embedded ICC profiles of PNG/JPEG files are ignored.
1232
+ - **Text**: no Unicode bidi algorithm, no hyphenation, no kerning of the standard fonts, no WOFF fonts; text
1233
+ extraction is reading order, not layout-perfect. With `textMapping: 'actualText'`, readers that ignore ActualText
1234
+ or close it early (pdf.js; a tester saw an older MuPDF drop a line's last Bengali cluster) lose the spanned
1235
+ clusters: the writer closes every span after its cluster's last glyph, and MuPDF 1.28.2 (PyMuPDF) reads Bengali,
1236
+ Devanagari and the other test scripts whole.
1237
+ - **Annotations**: no popup annotations.
1238
+ - **Crypto**: RC4 is refused (read only); public-key (certificate) encryption is neither written nor read; signatures
1239
+ are listed with their coverage, but their cryptography is not verified.
1240
+ - **Standards**: no PDF/A-1a or PDF/X; no attachment annotations in PDF/A-3 (use `pdf.attach`); a Factur-X invoice's
1241
+ content is not validated.
1242
+ - **Editing**: no page edits in an incremental update (use `save()`).
1243
+ - **Browsers**: reading encrypted files needs Node (WebCrypto has no synchronous AES).
1244
+
1245
+ See NOTES.md for the plan, and ARCHITECTURE.md for extending the library.
1246
+
1247
+ ## Releasing
1248
+
1249
+ Releases are published to npm by GitHub Actions from a GitHub Release (tag `v<version>`). The one-time `NPM_TOKEN`
1250
+ setup and the steps are in [RELEASING.md](./RELEASING.md).
1133
1251
 
1134
1252
  ## Changes
1135
1253
 
1254
+ Unreleased: the built-in TrueType subset renumbers its glyphs and drops the tables a PDF does not read (`name`,
1255
+ `OS/2`, `gasp`; OpenType/CFF also drops `name` and `OS/2`): a 19-character DejaVu Sans line is 6.5 KB, was 20.2 KB.
1256
+ Renders and extracted text are unchanged. A subsetter function, and `core`, still keep glyph IDs.
1257
+
1258
+ 0.1.0-beta.11: `pdf.core`, the low-level API under `createPdf`, is documented (`PdfCore` in the types), with
1259
+ `core.imageResource`; encryption now covers every literal and hex string in direct objects (it leaked some and threw
1260
+ on others); `readFont` and `checkSubset` read a font's metrics and check a subset before embedding; form fields take
1261
+ a caller's `appearance`, `da`, `structParent` and `widgetRef`; a document whose pages were written through `core` can
1262
+ be signed (an invisible field), with a PKCS#12 example (`examples/p12-signer.mjs`); `svgPathOps` turns SVG path data
1263
+ into path operators; the build is minified, and `@reportwright/pdf/write` exports the writer without the reader; the
1264
+ `page`, `width` and `height` of `SignatureOptions` are optional.
1265
+ It includes everything from beta.9 and beta.10.
1266
+
1136
1267
  0.1.0-beta.10: a WritableStream's writer lock is released when the export ends, fails or is stopped (`timeoutMs`,
1137
1268
  `signal`), so the caller can reuse or cancel the stream; the abort reason is still the original error.
1138
1269