@zumer/snapdom-plugins 3.0.0-beta.0 → 4.0.0
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/CHANGELOG.md +13 -0
- package/README.md +54 -20
- package/agent-map.d.ts +32 -0
- package/agent-map.js +5 -3
- package/ascii-export.d.ts +10 -0
- package/ascii-export.js +21 -6
- package/capture-frames.js +5 -5
- package/color-tint.js +4 -0
- package/context-export.d.ts +23 -0
- package/context-export.js +4 -1
- package/filter.js +1 -1
- package/gif-export.d.ts +6 -0
- package/gif-export.js +3 -3
- package/html-export.d.ts +8 -0
- package/html-export.js +6 -4
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/package.json +17 -6
- package/pdf/LICENSE +21 -0
- package/pdf/README.md +22 -0
- package/pdf/REFERENCE.md +520 -0
- package/pdf/src/frontmatter.js +200 -0
- package/pdf/src/furniture.js +398 -0
- package/pdf/src/index.js +2415 -0
- package/pdf/src/mega/README.md +47 -0
- package/pdf/src/mega/diag.js +148 -0
- package/pdf/src/mega/flow.js +56 -0
- package/pdf/src/mega/fonts.js +287 -0
- package/pdf/src/mega/forms.js +479 -0
- package/pdf/src/mega/labels.js +43 -0
- package/pdf/src/mega/meta.js +145 -0
- package/pdf/src/nav.js +357 -0
- package/pdf/src/paginate.js +583 -0
- package/pdf/src/tagged.js +355 -0
- package/pdf/src/text-layer.js +1771 -0
- package/pdf/src/writer/encrypt.js +218 -0
- package/pdf/src/writer/font.js +507 -0
- package/pdf/src/writer/index.js +1135 -0
- package/pdf/src/writer/subset.js +387 -0
- package/pdf.d.ts +294 -0
- package/pdf.js +1 -0
- package/redact-inputs.js +6 -6
- package/vector/CONTRACT.md +906 -0
- package/vector/LICENSE +21 -0
- package/vector/README.md +35 -0
- package/vector/src/adapters/snapdom.js +795 -0
- package/vector/src/collect/box.js +1676 -0
- package/vector/src/collect/font.js +487 -0
- package/vector/src/collect/image.js +1458 -0
- package/vector/src/collect/mathml.js +272 -0
- package/vector/src/collect/text.js +3027 -0
- package/vector/src/css/color.js +823 -0
- package/vector/src/css/gradient.js +1126 -0
- package/vector/src/css/matrix.js +398 -0
- package/vector/src/css/radius.js +124 -0
- package/vector/src/css/shadow.js +201 -0
- package/vector/src/emit/figma-json.js +1995 -0
- package/vector/src/emit/h2d-vector.js +215 -0
- package/vector/src/emit/h2d.js +1147 -0
- package/vector/src/emit/pdf.js +2571 -0
- package/vector/src/emit/svg-flat.js +4842 -0
- package/vector/src/exclusion.js +13 -0
- package/vector/src/index.js +2910 -0
- package/vector/src/paint.js +2908 -0
- package/vector/src/public.js +57 -0
- package/vector/src/writer/encrypt.js +227 -0
- package/vector/src/writer/font.js +507 -0
- package/vector/src/writer/index.js +1148 -0
- package/vector/src/writer/subset.js +387 -0
- package/vector/svd/index.js +72 -0
- package/vector/svd/schema.js +985 -0
- package/vector.d.ts +28 -0
- package/vector.js +1 -0
- package/video-export.d.ts +6 -0
- package/video-export.js +7 -7
- package/pdf-image.d.ts +0 -18
- package/pdf-image.js +0 -114
package/pdf/REFERENCE.md
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# SnapDOM PDF reference
|
|
2
|
+
|
|
3
|
+
Version 1.0.0 reference. This is the canonical option and behaviour reference
|
|
4
|
+
for the build beside it.
|
|
5
|
+
|
|
6
|
+
SnapDOM PDF is a SnapDOM plugin. It does not expose a standalone `toPdf(element)`
|
|
7
|
+
function. Register the `pdf()` factory before capture; the resulting capture
|
|
8
|
+
then gains `toPdf()` and the generic `to('pdf')` export.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { snapdom } from '@zumer/snapdom'
|
|
12
|
+
import pdf from './snapdom-pdf.mjs'
|
|
13
|
+
|
|
14
|
+
snapdom.plugins(pdf())
|
|
15
|
+
|
|
16
|
+
const shot = await snapdom(document.querySelector('#report'), {
|
|
17
|
+
scale: 2,
|
|
18
|
+
dpr: 1,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const blob = await shot.toPdf({ page: 'a4' })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
See [README.md](./README.md) for the bundler and import-map installation
|
|
25
|
+
recipes. Both builds export the same factory as the default export and as the
|
|
26
|
+
named export `pdf`. The supported peer range is `@zumer/snapdom >=3.0.0 <4`.
|
|
27
|
+
The plugin uses v3's immutable capture metadata, requested export options and
|
|
28
|
+
pre-decode canvas crops. PDF declares `needs: 'render'` and uses the SVG path so
|
|
29
|
+
the text layer and raster share one immutable artifact.
|
|
30
|
+
|
|
31
|
+
## The two option moments
|
|
32
|
+
|
|
33
|
+
The plugin derives its geometry and document semantics lazily from SnapDOM's
|
|
34
|
+
final serialized clone when the first PDF export runs, rather than reading the
|
|
35
|
+
original live DOM. Five options control that capture model and therefore have
|
|
36
|
+
to be fixed on `pdf()` before the capture is created:
|
|
37
|
+
|
|
38
|
+
| Capture-time option | Default | Meaning |
|
|
39
|
+
|---|---:|---|
|
|
40
|
+
| `formValues` | `true` | Put `input`, `textarea` and selected `select` values in the text layer. Password values are replaced with bullets. |
|
|
41
|
+
| `shadow` | `true` | Walk open shadow roots as a flattened tree. Closed roots are unreachable. |
|
|
42
|
+
| `breakAvoid` | `true` | Measure rows, list items, figures and other atomic blocks so pagination can avoid splitting them. |
|
|
43
|
+
| `outline` | `true` | Build bookmarks from `h1`–`h6` and `role="heading"`. A string is a custom CSS selector; `false` disables the outline. |
|
|
44
|
+
| `forms` | `true` | Measure form controls (kind, name, value, options, geometry, painted style) so the export can write fillable fields. `false` skips the measurement entirely. |
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
snapdom.plugins(pdf({
|
|
48
|
+
shadow: true,
|
|
49
|
+
outline: 'h1, h2, [role="heading"]',
|
|
50
|
+
page: 'a4', // an export default may also live here
|
|
51
|
+
margin: 36,
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
const shot = await snapdom(report)
|
|
55
|
+
await shot.toPdf({ margin: 48 }) // overrides only this export
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Passing a capture-time option to `toPdf()` warns and ignores it. Every export
|
|
59
|
+
option below may also be supplied to `pdf()` as a default; `toPdf()` wins.
|
|
60
|
+
|
|
61
|
+
## `toPdf()`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
shot.toPdf(options?: PdfExportOptions): Promise<Blob>
|
|
65
|
+
shot.to('pdf', options?: PdfExportOptions): Promise<Blob>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`download` is an optional side effect. A `Blob` with type `application/pdf` is
|
|
69
|
+
returned in every case.
|
|
70
|
+
|
|
71
|
+
### Page and pagination
|
|
72
|
+
|
|
73
|
+
| Option | Default | Meaning |
|
|
74
|
+
|---|---:|---|
|
|
75
|
+
| `page` | `'fit'` | `'fit'`; `a3`, `a4`, `a5`, `a6`, `b4`, `b5`, `letter`, `legal`, `tabloid`, `executive`; or `[width, height]` in PDF points. |
|
|
76
|
+
| `orientation` | `'portrait'` | `'portrait'` or `'landscape'`. It has no effect on `'fit'`. |
|
|
77
|
+
| `margin` | `24` | Points on all sides. It has no effect on `'fit'`. |
|
|
78
|
+
| `repeatHeaders` | `true` | On paginated output, repeat a table's `<thead>` wherever that table continues. |
|
|
79
|
+
|
|
80
|
+
`page: 'fit'` creates one page at the captured element's size. Other sizes scale
|
|
81
|
+
the capture to the printable width and paginate vertically. Declared
|
|
82
|
+
`break-before`/`break-after` values `always`, `page`, `left`, `right`, `recto`
|
|
83
|
+
and `verso` create plain breaks when they do not create an empty page.
|
|
84
|
+
|
|
85
|
+
Atomic blocks include table rows, list items, `dt`/`dd`, figures, blockquotes,
|
|
86
|
+
replaced content, `break-inside: avoid` content and short leaf blocks. A block
|
|
87
|
+
taller than a page still has to be split.
|
|
88
|
+
|
|
89
|
+
Repeated `<thead>` pixels and text are re-stamped from the body capture. Links
|
|
90
|
+
inside the repeated copy are not repeated. A header taller than half a page is
|
|
91
|
+
left where it originally occurs and produces a warning.
|
|
92
|
+
|
|
93
|
+
### Raster
|
|
94
|
+
|
|
95
|
+
| Option | Default | Meaning |
|
|
96
|
+
|---|---:|---|
|
|
97
|
+
| `image` | `true` | Draw the visible page raster. `false` skips rasterization and produces an image-free PDF while the enabled text, links and structure remain. Element headers, footers and watermarks are omitted because they are images. |
|
|
98
|
+
| `codec` | `'auto'` | `'auto'` encodes JPEG and lossless Flate and keeps the smaller image. `'jpeg'` and `'flate'` pin the choice. |
|
|
99
|
+
| `quality` | capture quality or `0.92` | Quality of the JPEG candidate, taken from the capture when the export does not set one. It reaches nothing when Flate wins, and on ordinary page artwork Flate wins until roughly 0.5. Measured on a 37-page report: `0.92`, `0.8` and `0.6` all produce a byte-identical 16.1 MB, `0.4` gives 13.7 MB and `0.2` gives 10.1 MB. An export whose lowered quality changed nothing says so. |
|
|
100
|
+
| `backgroundColor` | capture background or `null` | Flatten this PDF export onto a colour. May be set on the capture, the plugin defaults or this export. |
|
|
101
|
+
|
|
102
|
+
Resolution belongs to `snapdom(...)`: use `scale`, `dpr`, `width` and `height`
|
|
103
|
+
there, not in `toPdf()`. On v3, an explicit width or height overrides `scale`;
|
|
104
|
+
`dpr` still multiplies the raster resolution. Keep the capture aspect ratio when
|
|
105
|
+
setting both dimensions. The exporter rasterizes page regions lazily and caches
|
|
106
|
+
them on the capture for later exports. A different codec, quality or flattening
|
|
107
|
+
choice may still require encoding work.
|
|
108
|
+
|
|
109
|
+
**`dpr` is the same knob as `scale` here, and it defaults to the machine.**
|
|
110
|
+
A PDF has no device to match, so neither option moves a point of geometry: the
|
|
111
|
+
MediaBox, the text layer and every annotation are identical whatever they are
|
|
112
|
+
set to. What they multiply is the page image's pixel count, and they multiply it
|
|
113
|
+
together. Measured on an 8-page A4 report, `scale: 1, dpr: 2` and
|
|
114
|
+
`scale: 2, dpr: 1` produce the same 1748px-wide raster and the same 3.18 MB
|
|
115
|
+
file, against 1.63 MB at 1×1 and 6.93 MB at 2×2.
|
|
116
|
+
|
|
117
|
+
The catch is the default. SnapDOM reads `dpr` from `window.devicePixelRatio`,
|
|
118
|
+
which is right for a screenshot and surprising for a document: the same code
|
|
119
|
+
exports 3.18 MB from a 2× laptop and 1.63 MB from a 1× monitor or a CI
|
|
120
|
+
container, with no visible difference in the page and no warning. Set `dpr`
|
|
121
|
+
explicitly on the capture (`1` for a light, screen-read file, `2` for one meant
|
|
122
|
+
to be printed) and the export stops depending on who ran it.
|
|
123
|
+
|
|
124
|
+
`codec: 'auto'` costs an extra encode. Pin a codec when latency matters more than
|
|
125
|
+
choosing the smallest file. If `CompressionStream` is unavailable, `auto` uses
|
|
126
|
+
JPEG and a requested `flate` warns before falling back to JPEG.
|
|
127
|
+
|
|
128
|
+
Transparency uses a soft mask. `backgroundColor` removes that mask and usually
|
|
129
|
+
produces a smaller file. Geometry comes from the same final serialized clone as
|
|
130
|
+
the raster. Real removals/replacements made by `exclude`, `resolveNode`,
|
|
131
|
+
`afterClone`, `clip`, or a geometry-preserving `afterRender` replacement of the
|
|
132
|
+
final serialized tree/URL stay synchronized with the PDF layer. An `afterRender`
|
|
133
|
+
replacement that changes the artifact's viewBox, root paint/layout attributes or
|
|
134
|
+
intrinsic aspect/geometry incompatibly is rejected rather than exported with a
|
|
135
|
+
misaligned layer. Uniform width/height scaling that preserves aspect is allowed.
|
|
136
|
+
|
|
137
|
+
### Text, links, navigation and accessibility
|
|
138
|
+
|
|
139
|
+
| Option | Default | Meaning |
|
|
140
|
+
|---|---:|---|
|
|
141
|
+
| `text` | `true` | Emit selectable/searchable invisible text. |
|
|
142
|
+
| `links` | `true` | Emit external URI and in-document link annotations. |
|
|
143
|
+
| `tagged` | `true` | Emit a structure tree for headings, paragraphs, lists, tables, sections and figures. |
|
|
144
|
+
| `lang` | DOM language or `null` | Set the PDF document language, such as `'en'` or `'es-MX'`. |
|
|
145
|
+
| `embedFonts` | `true` | Embed the capture's `@font-face` binaries and point the text at them. |
|
|
146
|
+
| `vectorText` | `false` | Paint eligible text as real vector glyphs instead of leaving it invisible over the raster. |
|
|
147
|
+
| `debugText` | `false` | Paint the normally invisible layer in red and leave streams readable. |
|
|
148
|
+
|
|
149
|
+
#### Embedded fonts
|
|
150
|
+
|
|
151
|
+
The capture must CARRY its fonts for the export to embed them: pass
|
|
152
|
+
`embedFonts: true` to `snapdom(…)` (the engine option that inlines `@font-face`
|
|
153
|
+
into the capture), and the PDF side reads those rules, fetches each face the
|
|
154
|
+
text actually painted with, and embeds a SUBSET of it: only the glyphs the
|
|
155
|
+
document drew, renumbered into a dense space. On a Latin page a ~320 kB face
|
|
156
|
+
travels as a few kB; the subset renders pixel-identical to the whole font
|
|
157
|
+
(asserted by rendering both through pdf.js). A face travels once however many
|
|
158
|
+
styles and pages use it, and the saving is declared per face (`font-embedded`).
|
|
159
|
+
|
|
160
|
+
`embedFonts` on the PDF export controls this: `true` (default) subsets;
|
|
161
|
+
`'full'` embeds the whole file unsubsetted (glyph ids stay the font's own);
|
|
162
|
+
`false` embeds nothing. CFF/OpenType (`OTTO`) faces are always embedded whole
|
|
163
|
+
(their charstrings are a different compiler), so `'full'` only differs for
|
|
164
|
+
TrueType. The subsetter is fail-safe: a TrueType face it cannot subset falls
|
|
165
|
+
back to a whole embed with an explicit `CIDToGIDMap`, never a corrupt glyph.
|
|
166
|
+
|
|
167
|
+
#### Vector text
|
|
168
|
+
|
|
169
|
+
By default the visible text is the raster: pixel-exact, but pixels. With
|
|
170
|
+
`vectorText: true` the exporter PAINTS eligible runs as real vector glyphs from
|
|
171
|
+
the embedded fonts, so the type is crisp at any zoom and the file is a fraction
|
|
172
|
+
of the size. This is a different fidelity model from the pixel-exact capture,
|
|
173
|
+
and it is opt-in for that reason.
|
|
174
|
+
|
|
175
|
+
A run is painted only where no shaping stands between code points and glyphs: a
|
|
176
|
+
left-to-right run, in a covering embedded face, with a resolved colour. RTL,
|
|
177
|
+
complex scripts, uncovered characters and unresolved colours are NOT painted,
|
|
178
|
+
because one CID per code point would place their glyphs wrong. Those runs stay
|
|
179
|
+
on the raster; in an image-free export they are invisible and `vector-missed`
|
|
180
|
+
says so.
|
|
181
|
+
|
|
182
|
+
The two useful pairings:
|
|
183
|
+
|
|
184
|
+
- `image: false, vectorText: true`: a small, fully-vector text PDF. Only the
|
|
185
|
+
text and vector furniture travel; borders, checkboxes, background images and
|
|
186
|
+
any un-paintable run are absent. Measured: the mega fixture is ~22 kB this way
|
|
187
|
+
against ~420 kB with the raster.
|
|
188
|
+
- `image: true, vectorText: true`: the raster plus crisp vector glyphs painted
|
|
189
|
+
over it. Larger, but sharp under magnification.
|
|
190
|
+
|
|
191
|
+
Interior glyph positions follow the embedded font's own advances scaled to the
|
|
192
|
+
run's measured width (`Tz`), not the browser's kerned positions, so a heavily
|
|
193
|
+
kerned pair can drift sub-pixel from the raster. Extraction is unchanged: the
|
|
194
|
+
painted glyphs carry the same `/ToUnicode` as the invisible layer.
|
|
195
|
+
|
|
196
|
+
Per run, the embedded face is used only when its real cmap covers every
|
|
197
|
+
character of the run; a run it cannot carry falls back to the unembedded layer
|
|
198
|
+
and is counted (`font-coverage`). System fonts have no bytes a page can hand
|
|
199
|
+
over and travel by name as before. WOFF unpacks; WOFF2, TrueType collections
|
|
200
|
+
and faces whose `OS/2 fsType` forbids embedding are refused with their reason
|
|
201
|
+
(`font-fallback`). No shaping is performed, which is correct for the invisible
|
|
202
|
+
layer, whose widths are pinned to the DOM measurement by `Tz`.
|
|
203
|
+
|
|
204
|
+
Text extraction is byte-for-byte identical with and without embedding (asserted
|
|
205
|
+
in the harness), because extraction runs off `/ToUnicode` either way.
|
|
206
|
+
|
|
207
|
+
#### The earned PDF/UA-1 claim
|
|
208
|
+
|
|
209
|
+
The exporter never takes a conformance claim as an option; it COMPUTES
|
|
210
|
+
eligibility per export: every text-showing operation on an embedded face,
|
|
211
|
+
`tagged` structure present, a document language, a `meta.title` to display, and
|
|
212
|
+
no painted image without alternative text. When all of it holds, the XMP carries
|
|
213
|
+
`pdfuaid:part 1` and the report says so (`ua-claimed`, `report.pdfua === true`).
|
|
214
|
+
When fonts are clean but a blocker remains, the blockers are named
|
|
215
|
+
(`ua-withheld`).
|
|
216
|
+
|
|
217
|
+
Two blockers are worth knowing in advance. Fillable fields block the claim,
|
|
218
|
+
because their editor-facing `/DA` types in unembedded Helvetica, so a
|
|
219
|
+
UA-claiming export pairs with `fields: false`. A painted `<img>` or
|
|
220
|
+
`role="img"` element with no accessible name blocks it too, because its pixels
|
|
221
|
+
live in the page raster, which is an artifact, and there is no `/Alt` to
|
|
222
|
+
announce: give it `alt`, `aria-label` or `title`, or `alt=""` when it is
|
|
223
|
+
decorative.
|
|
224
|
+
|
|
225
|
+
Text is measured from the final serialized clone, one run per word. Text
|
|
226
|
+
transforms are applied; open shadow roots, visible form values and bidi logical
|
|
227
|
+
order are included. Text hidden by layout, ancestor opacity or rectangular
|
|
228
|
+
overflow clipping is excluded.
|
|
229
|
+
|
|
230
|
+
Base-14 fonts carry WinAnsi text. Other scripts use an invisible Type0 font with
|
|
231
|
+
a `/ToUnicode` map. Without `vectorText`, the visible type remains the SnapDOM
|
|
232
|
+
raster.
|
|
233
|
+
|
|
234
|
+
Headings become bookmarks and named destinations. A same-document `#fragment`
|
|
235
|
+
link becomes a PDF destination; an unresolved or out-of-capture fragment gets no
|
|
236
|
+
annotation and produces a warning. External annotations are restricted to
|
|
237
|
+
`http:`, `https:`, `mailto:` and `tel:`. Malformed URLs and other schemes such
|
|
238
|
+
as `javascript:`, `data:`, `blob:` and `file:` are ignored and reported. A
|
|
239
|
+
custom `outline` selector is evaluated against the final flattened artifact, so
|
|
240
|
+
matching open-shadow and slotted headings participate too.
|
|
241
|
+
|
|
242
|
+
#### Redaction and semantic visibility
|
|
243
|
+
|
|
244
|
+
Visual occlusion is not redaction. A rectangle above a secret, blur, or arbitrary
|
|
245
|
+
overlap can hide pixels without removing the underlying text from the serialized
|
|
246
|
+
document. Remove or replace confidential nodes/text with `exclude`,
|
|
247
|
+
`resolveNode` or a clone/render hook. Do not use overlays or blur as a redaction
|
|
248
|
+
mechanism.
|
|
249
|
+
|
|
250
|
+
For known clipping, masking and filter geometry, the collector omits text and
|
|
251
|
+
links when it cannot prove they are visible. This is fail-safe: difficult visual
|
|
252
|
+
effects may lose selectable semantics instead of leaking content. An
|
|
253
|
+
`afterRender` hook is synchronized only when it replaces the final serialized
|
|
254
|
+
tree/URL while preserving its geometry. A purely visual overlay is not evidence
|
|
255
|
+
that semantic content vanished, and an incompatible viewBox, root paint/layout
|
|
256
|
+
or intrinsic-aspect change fails instead of guessing new coordinates.
|
|
257
|
+
|
|
258
|
+
Tagged output improves reading order and structure. It becomes a **PDF/UA-1
|
|
259
|
+
claim only when the export earns it** (see "The earned PDF/UA-1 claim" above);
|
|
260
|
+
short of that, no `pdfuaid` identifier is written. Either way the tree is
|
|
261
|
+
real: link annotations are tied in by
|
|
262
|
+
`/Link` structure elements carrying `/OBJR` references, fillable fields by
|
|
263
|
+
`/Form` elements, header cells carry `/Scope` (and spans), table row groups map
|
|
264
|
+
to `THead`/`TBody`/`TFoot`, pages declare `/Tabs /S` so tab order follows the
|
|
265
|
+
structure, and the parent tree resolves both marked content and annotations.
|
|
266
|
+
Inline formatting elements are deliberately not expanded into a structure node
|
|
267
|
+
each.
|
|
268
|
+
|
|
269
|
+
### Fillable form fields
|
|
270
|
+
|
|
271
|
+
| Option | Default | Meaning |
|
|
272
|
+
|---|---:|---|
|
|
273
|
+
| `fields` | `true` | Emit measured form controls as real AcroForm fields: fillable, tabbable, extractable. `false` keeps their values in the invisible text layer instead. |
|
|
274
|
+
|
|
275
|
+
A captured form should still BE a form. Text inputs, textareas, checkboxes,
|
|
276
|
+
radio groups, and selects become widget annotations with their DOM name, value,
|
|
277
|
+
options and checked state; `readonly`/`disabled` map to the read-only flag,
|
|
278
|
+
`required` and `maxlength` travel too, and the accessible label (`aria-label`,
|
|
279
|
+
`<label for>`, placeholder, title, in that order) becomes the field's tooltip,
|
|
280
|
+
which is what a screen reader speaks. Radios group by their HTML `name`;
|
|
281
|
+
duplicate non-radio names are uniquified with a suffix and reported.
|
|
282
|
+
|
|
283
|
+
The page image yields each fielded control's area to its widget, and the
|
|
284
|
+
widget's appearance follows one rule, dictated by macOS Preview: a field
|
|
285
|
+
HOLDING A VALUE paints it as real text, laid out from the control's own
|
|
286
|
+
measured runs so a textarea's wrap and an input's alignment are the page's
|
|
287
|
+
rather than a guess, while an EMPTY field wears the control's own crop of the
|
|
288
|
+
capture: border, corner radius and placeholder, pixel-exact. (Preview redraws a
|
|
289
|
+
text field's value over any appearance carrying an image, so a valued field's
|
|
290
|
+
crop would show the value twice there; text appearances render once everywhere:
|
|
291
|
+
Preview, Quick Look, Chrome, pdf.js, Acrobat, and every print/flatten path.)
|
|
292
|
+
|
|
293
|
+
What deliberately does not happen: a **password** field is written empty with
|
|
294
|
+
the password flag set, because a field value is plain text in the file; the
|
|
295
|
+
raster keeps its painted bullets. **Buttons, sliders, colour wells and file
|
|
296
|
+
pickers** stay pixels; a control that only means something to a running page is
|
|
297
|
+
not a field. **No JavaScript actions and no submit URLs** are ever written. A
|
|
298
|
+
rotated or clip-cut control is reported and left as invisible text, and a
|
|
299
|
+
control crossing a page break is clamped to one page and reported. With
|
|
300
|
+
`image: false` the appearance is synthesized (background, border and a base-14
|
|
301
|
+
value line), and a value outside WinAnsi sets `/NeedAppearances` so the viewer
|
|
302
|
+
draws it.
|
|
303
|
+
|
|
304
|
+
### Document metadata, identity and page labels
|
|
305
|
+
|
|
306
|
+
| Option | Default | Meaning |
|
|
307
|
+
|---|---:|---|
|
|
308
|
+
| `meta` | `null` | `{ title, author, subject, keywords, creator, created, modified }`: /Info plus a mirrored XMP packet. |
|
|
309
|
+
|
|
310
|
+
`title` also sets `/ViewerPreferences << /DisplayDocTitle true >>`, so viewers
|
|
311
|
+
show the document's name rather than its filename. `keywords` may be a string or
|
|
312
|
+
an array. `created`/`modified` accept a `Date` or ISO-8601 string and are
|
|
313
|
+
written ONLY when given: an invented "now" would break the byte-determinism
|
|
314
|
+
promise, so pass `new Date()` yourself when you mean it. `/Producer` is always
|
|
315
|
+
`snapdom-pdf` and is not an option.
|
|
316
|
+
|
|
317
|
+
Every file carries a trailer `/ID` derived from its own content: identical
|
|
318
|
+
unencrypted exports share an ID, different documents differ, and no clock is
|
|
319
|
+
involved. Paginated documents with front matter get `/PageLabels` (cover and
|
|
320
|
+
contents in lowercase roman, body decimal from 1), so the viewer's page field
|
|
321
|
+
agrees with the printed folio and with the generated contents.
|
|
322
|
+
|
|
323
|
+
### Encryption
|
|
324
|
+
|
|
325
|
+
| Option | Default | Meaning |
|
|
326
|
+
|---|---:|---|
|
|
327
|
+
| `encrypt` | `null` | `{ userPassword, ownerPassword, permissions }`: encrypt the file with AES-256 (standard security handler, revision 6). |
|
|
328
|
+
|
|
329
|
+
The exported file is the delivery: it gets downloaded, attached, and forwarded.
|
|
330
|
+
`encrypt` is what makes that file unreadable once it has left, and it is the
|
|
331
|
+
only protection here that is not advisory.
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
await shot.toPdf({
|
|
335
|
+
page: 'a4',
|
|
336
|
+
encrypt: { userPassword: 'ada-1815', ownerPassword: 'owner-key' },
|
|
337
|
+
})
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`userPassword` is **required and must be non-empty**, and an export that omits
|
|
341
|
+
it throws rather than producing a file. A PDF whose user password is `''` is
|
|
342
|
+
genuinely encrypted (the bytes are unreadable to a text editor), and every
|
|
343
|
+
viewer in the world still opens it without asking. That is the exact shape of a
|
|
344
|
+
document somebody believes is protected and is not. `ownerPassword` defaults to
|
|
345
|
+
the user password; it exists to lift the permission flags for whoever holds it.
|
|
346
|
+
|
|
347
|
+
**AES-256 and nothing older.** RC4 and revisions 2–4 are not offered at any
|
|
348
|
+
setting. A 40-bit key falls in seconds, 128-bit RC4 is not far behind, and both
|
|
349
|
+
need MD5, which the platform does not provide. Writing one by hand to ship a
|
|
350
|
+
weaker product is not a trade this exporter makes. Everything used here comes
|
|
351
|
+
from the browser's own `crypto.subtle`, so the plugin carries no cipher code and
|
|
352
|
+
no dependency. Encryption therefore needs a **secure context**: https, or
|
|
353
|
+
localhost. Encrypting a 14 MB document costs about 34 ms.
|
|
354
|
+
|
|
355
|
+
| | |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `/Filter /Standard /V 5 /R 6`, `/CFM /AESV3`, 256-bit key | ISO 32000-2 §7.6.4 |
|
|
358
|
+
| Header stays `%PDF-1.7`; the catalog declares `/ADBE /ExtensionLevel 3` | the shape readers learned before ISO 32000-2 |
|
|
359
|
+
| Every stream and every string is encrypted, `/Metadata` included | strings inside content streams ride the stream |
|
|
360
|
+
|
|
361
|
+
#### Permissions are a request, not a protection
|
|
362
|
+
|
|
363
|
+
`permissions` accepts `print`, `modify`, `copy`, `annotate`, `fillForms`,
|
|
364
|
+
`assemble` and `printHighRes`; each defaults to `true`, and setting one to
|
|
365
|
+
`false` clears its `/P` bit. Understand what that buys: a flag is something a
|
|
366
|
+
viewer *may* honour and any other tool can ignore. The password is what protects
|
|
367
|
+
the document; the flags only express intent to software that has already been
|
|
368
|
+
given the key. The export says so in the report when any of them is set.
|
|
369
|
+
|
|
370
|
+
Extraction for accessibility is deliberately not exposed. PDF 2.0 deprecates
|
|
371
|
+
that bit (a conforming reader must always allow it), and PDF/UA requires an
|
|
372
|
+
encrypted file to keep permitting it, so an exporter whose selling point is an
|
|
373
|
+
extractable text layer has no business offering to switch it off. A UA-claiming
|
|
374
|
+
export and an encrypted one are compatible for exactly that reason.
|
|
375
|
+
|
|
376
|
+
#### Encrypted exports are not deterministic
|
|
377
|
+
|
|
378
|
+
The file key, the salts and every IV are random, drawn from
|
|
379
|
+
`crypto.getRandomValues` for each export. Encrypting the same document twice with
|
|
380
|
+
the same password gives two different files that open to the same content, so
|
|
381
|
+
two encrypted files never reveal that their contents match. Unencrypted exports
|
|
382
|
+
stay byte-deterministic.
|
|
383
|
+
|
|
384
|
+
### Progress, cancellation and the report
|
|
385
|
+
|
|
386
|
+
| Option | Default | Meaning |
|
|
387
|
+
|---|---:|---|
|
|
388
|
+
| `onProgress` | `null` | `({ phase, done, total })` per step: `measure`, `page` (with counts), `matter`, `assemble`. |
|
|
389
|
+
| `signal` | `null` | An `AbortSignal`; aborting rejects with `AbortError` between pages. |
|
|
390
|
+
| `onReport` | `null` | Receives the structured diagnostics report once the Blob is finished. |
|
|
391
|
+
|
|
392
|
+
The report is `{ warnings, counts, pages, bytes, fields, annotations,
|
|
393
|
+
fontsEmbedded, pdfua, vectorText }`, where each warning is `{ code, message }`,
|
|
394
|
+
`counts` tallies by code and `vectorText` counts the runs painted as glyphs.
|
|
395
|
+
Codes are a stable contract (`text-clipped`, `link-unresolved`, `field-skipped`,
|
|
396
|
+
`layout-split`, …) so a CI step can gate on "no annotations lost" without
|
|
397
|
+
parsing prose. Identical unencrypted exports produce identical reports; the
|
|
398
|
+
console keeps warning regardless, so nothing changes for callers who ignore all
|
|
399
|
+
of this.
|
|
400
|
+
|
|
401
|
+
### Header, footer and page numbers
|
|
402
|
+
|
|
403
|
+
| Option | Default | Meaning |
|
|
404
|
+
|---|---:|---|
|
|
405
|
+
| `header` | `null` | A string/function, a text-band object, or an Element. |
|
|
406
|
+
| `footer` | `null` | The same along the bottom. |
|
|
407
|
+
| `pageNumbers` | `false` | `true` or `{ format, align, size, font, color }`. |
|
|
408
|
+
|
|
409
|
+
Text bands accept `{ left, center, right, size, font, color, rule, gap }`.
|
|
410
|
+
Segments may be strings or `(page, pages) => value`. `font` is `document`,
|
|
411
|
+
`sans`, `serif` or `mono`; colours are three- or six-digit hex values.
|
|
412
|
+
`document`, the default, is the body's own embedded face when the export has
|
|
413
|
+
one, so a running head is set in the same type as the report it runs over, in
|
|
414
|
+
real embedded glyphs; per string it falls back to base-14 sans when nothing is
|
|
415
|
+
embedded or the face lacks a character. An explicit `sans`/`serif`/`mono` keeps
|
|
416
|
+
meaning base-14. Base-14 furniture remains limited to WinAnsi scripts.
|
|
417
|
+
|
|
418
|
+
Text-band defaults are `size: 9`, `font: 'document'`, `rule: true` and
|
|
419
|
+
`gap: 10`. Page numbers default to `(page, pages) => page + ' / ' + pages`,
|
|
420
|
+
centred at size 9 in the document face. `colour` is accepted as an alias for
|
|
421
|
+
`color`; prefer `color` in new code.
|
|
422
|
+
|
|
423
|
+
An Element band captures its capturable content and styles, but it is a picture
|
|
424
|
+
without its own selectable layer; scripts are not executed by the measurement
|
|
425
|
+
sandbox. Header, footer and numbers reserve space and only apply to paginated
|
|
426
|
+
page sizes. With `image: false`, Element bands are omitted with a warning; text
|
|
427
|
+
bands and page numbers still work.
|
|
428
|
+
|
|
429
|
+
### Cover, generated contents, closing page and watermark
|
|
430
|
+
|
|
431
|
+
| Option | Default | Meaning |
|
|
432
|
+
|---|---:|---|
|
|
433
|
+
| `cover` | `null` | An Element captured as the first page. |
|
|
434
|
+
| `toc` | `null` | `true`, a title string, or `{ title, levels, size, font, indent, dots, label, color, leaderColor }`. |
|
|
435
|
+
| `back` | `null` | An Element captured as the last page. |
|
|
436
|
+
| `watermark` | `null` | A string, Element, text-watermark object or element-watermark object. |
|
|
437
|
+
|
|
438
|
+
Cover and back are scaled to fit a whole page without cropping and receive a
|
|
439
|
+
selectable text layer. The generated contents comes from the same outline as
|
|
440
|
+
the bookmarks; each row links to the body destination. Its page numbers refer
|
|
441
|
+
to body pages, excluding cover and contents pages. When a custom
|
|
442
|
+
`pageNumbers.format` changes numbering, use `toc.label` to keep both in step.
|
|
443
|
+
|
|
444
|
+
Text watermarks accept `{ text, opacity, angle, size, font, color }`; omitting
|
|
445
|
+
`size` auto-fits the rotated text. Element watermarks accept
|
|
446
|
+
`{ element, opacity, angle, fit }`. Watermarks draw over every page and also work
|
|
447
|
+
with `page: 'fit'`. With `image: false`, an Element watermark is omitted, while
|
|
448
|
+
text watermarks still work. Cover, contents and back require a paginated page
|
|
449
|
+
size; image-free cover/back pages retain their measured text, links and structure
|
|
450
|
+
but not their visible raster.
|
|
451
|
+
|
|
452
|
+
Contents default to title `Contents`, three outline depths, size 10, the
|
|
453
|
+
document face, 14-point indentation and leaders enabled. Text watermarks default
|
|
454
|
+
to 12% opacity and 45 degrees; Element watermarks default to 12% opacity, no
|
|
455
|
+
rotation and a 0.6 fit. `colour`/`leaderColour` are accepted aliases for
|
|
456
|
+
`color`/`leaderColor`.
|
|
457
|
+
|
|
458
|
+
### Output
|
|
459
|
+
|
|
460
|
+
| Option | Default | Meaning |
|
|
461
|
+
|---|---:|---|
|
|
462
|
+
| `deflate` | `true` | Compress PDF content streams. `debugText` leaves them readable. |
|
|
463
|
+
| `download` | `false` | A filename, or `true` for `snapdom.pdf`. `.pdf` is appended when absent. |
|
|
464
|
+
|
|
465
|
+
Use `deflate`, not SnapDOM's `compress`; use `download`, not SnapDOM's
|
|
466
|
+
`filename`. `compress`, `filename`, PDF-side `scale`, legacy `background`,
|
|
467
|
+
`mode` and `vector` are not PDF options and are reported when passed.
|
|
468
|
+
|
|
469
|
+
## Errors and warnings
|
|
470
|
+
|
|
471
|
+
Register the plugin before capture. A capture made without it has no `toPdf()`
|
|
472
|
+
helper, and its generic `to('pdf')` has no PDF exporter to run. An available PDF
|
|
473
|
+
export rejects an incompatible SnapDOM contract, invalid or empty capture
|
|
474
|
+
geometry, an incompatible final-artifact viewBox/root paint-layout/intrinsic
|
|
475
|
+
aspect, invalid orientation/margins/custom page geometry, and invalid active
|
|
476
|
+
image codec/quality.
|
|
477
|
+
An unknown string paper name warns and falls back to A4 for untyped JavaScript
|
|
478
|
+
callers. Other fidelity losses are reported under `[snapdom-pdf]`, including:
|
|
479
|
+
|
|
480
|
+
- page breaks that split atomic blocks or declared breaks that cannot be used;
|
|
481
|
+
- text or links clipped, invisible, outside the page, or under unsupported 3D geometry;
|
|
482
|
+
- unresolved internal links and invalid custom outline selectors;
|
|
483
|
+
- missing language on tagged output;
|
|
484
|
+
- a repeated table header too tall to repeat;
|
|
485
|
+
- a requested contents page with no headings;
|
|
486
|
+
- Base-14 page furniture that cannot represent its script;
|
|
487
|
+
- invalid or translucent flattening colours and unavailable Flate compression;
|
|
488
|
+
- malformed links or URI schemes outside the safe allowlist.
|
|
489
|
+
|
|
490
|
+
Every one of these is also a structured diagnostic with a stable code in the
|
|
491
|
+
report handed to `onReport`, so a build can gate on them instead of reading the
|
|
492
|
+
console.
|
|
493
|
+
|
|
494
|
+
## Known limits
|
|
495
|
+
|
|
496
|
+
- Visible text is raster by default, so zooming beyond capture resolution exposes pixels. `vectorText` paints eligible runs as glyphs, never RTL or complex scripts.
|
|
497
|
+
- `textarea` wrapping is greedy and can drift for hyphenation, CJK, unbreakable strings and `line-height: normal`.
|
|
498
|
+
- Date-like controls contribute their ISO value rather than the locale-painted value. Multi-row selects contribute no value.
|
|
499
|
+
- Closed shadow roots are unreachable.
|
|
500
|
+
- Upright glyphs in vertical writing modes are placed square; sideways runs are supported.
|
|
501
|
+
- Ambiguous clipping, masking and filter geometry is omitted fail-safe, so some visibly painted text may not be selectable.
|
|
502
|
+
- Native `<select>` insets were measured on Chromium and can differ in other engines.
|
|
503
|
+
- Rotated links use an axis-aligned PDF rectangle.
|
|
504
|
+
- Captured header/footer Elements have no selectable text layer.
|
|
505
|
+
- `image: false` omits Element headers, footers and watermarks; cover/back pages
|
|
506
|
+
keep semantics but lose their visible raster.
|
|
507
|
+
- Only `<thead>` repeats, and its repeated copy has no repeated link annotations.
|
|
508
|
+
|
|
509
|
+
## Browser status
|
|
510
|
+
|
|
511
|
+
The exporter is a browser ESM module. The complete PDF fixture suite runs in
|
|
512
|
+
Chromium through `npm run test:pdf`. The monorepo also tests registration,
|
|
513
|
+
searchable text, frozen capture state and redaction in Chromium and WebKit.
|
|
514
|
+
Firefox is not yet verified in this branch: its runner failed to connect.
|
|
515
|
+
The complete complex-layout matrix is not verified in every engine; known
|
|
516
|
+
engine-sensitive limits remain listed above.
|
|
517
|
+
|
|
518
|
+
The exporter runs locally in the page and returns a Blob. It does not upload the
|
|
519
|
+
PDF. SnapDOM may still fetch the page resources or use a proxy if the application
|
|
520
|
+
configured one.
|