@t4h.framework/pdf 0.4.6 → 0.4.7
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/.ai/skills/framework-pdf/SKILL.md +78 -316
- package/CHANGELOG.md +6 -0
- package/README.md +11 -154
- package/package.json +3 -3
|
@@ -1,355 +1,117 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-pdf
|
|
3
3
|
description: >-
|
|
4
|
-
Guides
|
|
5
|
-
|
|
6
|
-
Image, PageNumber), custom fonts, Binary image
|
|
7
|
-
password
|
|
8
|
-
the
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Guides the use of @t4h.framework/pdf to generate PDF files in T4H Framework
|
|
5
|
+
workflows: the render() function, the JSX marker components (Document, Page,
|
|
6
|
+
View, Text, Link, Image, PageNumber), styles, custom fonts, Binary image
|
|
7
|
+
sources, password protection with the encrypt option (ProtectionFlags,
|
|
8
|
+
ProtectionPresets), the FileSystemClaim that render needs, and tests with
|
|
9
|
+
TestWorkflowEnvironment and mockClaim. Use when a workflow must produce a
|
|
10
|
+
PDF, an invoice, a report or a statement, when building a PDF template in
|
|
11
|
+
JSX, or when someone mentions @react-pdf/renderer in a workflow.
|
|
11
12
|
---
|
|
12
13
|
|
|
13
14
|
# @t4h.framework/pdf
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
Package path: `framework/packages/pdf`. Peer dependencies:
|
|
17
|
-
`@t4h.framework/core`, `@t4h.framework/fs`. Regular dependencies: `react`,
|
|
18
|
-
`@react-pdf/renderer`.
|
|
16
|
+
## Render a PDF in a workflow
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
render,
|
|
25
|
-
Document,
|
|
26
|
-
Page,
|
|
27
|
-
View,
|
|
28
|
-
Text,
|
|
29
|
-
Link,
|
|
30
|
-
Image,
|
|
31
|
-
PageNumber,
|
|
32
|
-
RenderPDFActivity,
|
|
33
|
-
ProtectionFlags,
|
|
34
|
-
ProtectionPresets,
|
|
35
|
-
type RenderPDFOptions,
|
|
36
|
-
type RenderPDFInput,
|
|
37
|
-
type EncryptPDFOptions,
|
|
38
|
-
type DocumentProps,
|
|
39
|
-
type PageProps,
|
|
40
|
-
type ViewProps,
|
|
41
|
-
type TextProps,
|
|
42
|
-
type LinkProps,
|
|
43
|
-
type ImageProps,
|
|
44
|
-
type PageNumberProps,
|
|
45
|
-
type DocumentNode,
|
|
46
|
-
type PDFNode,
|
|
47
|
-
type PDFStyle,
|
|
48
|
-
} from '@t4h.framework/pdf'
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
> Workflows use **`render()`** — for everything, including encryption.
|
|
52
|
-
> `@t4h.framework/core/no-framework-activity-in-app` rejects `new` and
|
|
53
|
-
> `extends` on `RenderPDFActivity`.
|
|
54
|
-
|
|
55
|
-
## Architecture
|
|
56
|
-
|
|
57
|
-
`Workflow` → `render(<Document>…</Document>, options?)`
|
|
58
|
-
(`History.reconciler(RenderPDFActivity)`) →
|
|
59
|
-
|
|
60
|
-
1. **`toInput`** (workflow side): the JSX tree is serialized into a plain
|
|
61
|
-
`DocumentNode` JSON tree that is stored in the workflow history. User
|
|
62
|
-
function components, fragments, arrays, and conditionals are flattened
|
|
63
|
-
here; only marker components and strings remain.
|
|
64
|
-
2. **`run`** (activity side): `Binary` image and font sources are read through
|
|
65
|
-
their claims and inlined as `data:` URIs on the calling thread, then the
|
|
66
|
-
resolved tree is handed to a **worker thread** (pool size
|
|
67
|
-
`PDF_RENDER_CONCURRENCY`, default `1`, created on first render, idle workers
|
|
68
|
-
unrefed) that registers fonts once per process, converts the `DocumentNode`
|
|
69
|
-
tree back into real `@react-pdf/renderer` elements and renders it to a
|
|
70
|
-
buffer. The buffer is written through **`FileSystemClaim`** at
|
|
71
|
-
`pdf/${activityId}.pdf`. With the `encrypt` option, a password-protected
|
|
72
|
-
copy (AES-128, `%PDF-1.6`) is written from the same layout at
|
|
73
|
-
`pdf/${activityId}-encrypted.pdf` — no second layout pass, no re-encryption
|
|
74
|
-
of the finished file. A render therefore blocks the event loop of the
|
|
75
|
-
calling process for milliseconds, not for the whole document.
|
|
76
|
-
3. The activity returns the **`Binary`** the claim produced — a reference to
|
|
77
|
-
the stored file, not in-memory bytes. Replay-safe by construction. With
|
|
78
|
-
`encrypt`, it returns the `[original, encrypted]` pair instead, and the
|
|
79
|
-
`render` return type narrows automatically based on the options.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Claims
|
|
84
|
-
|
|
85
|
-
### FileSystemClaim (required)
|
|
86
|
-
|
|
87
|
-
The render activity persists the PDF through `fs`. Any workflow that calls
|
|
88
|
-
`render()` needs a `FileSystemClaim` provider:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
{ provide: FileSystemClaim, value: new MyFileSystemClaimImpl() }
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Tests mock its `write` to return a `Binary` (see **Testing**).
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Marker components
|
|
99
|
-
|
|
100
|
-
The components exported by this package are **markers**: they exist only as
|
|
101
|
-
type-safe JSX tags inside a tree passed to `render()`. JSX never invokes
|
|
102
|
-
them — the serializer recognizes them by reference. A marker called as a
|
|
103
|
-
function (`Image({ src })`) throws.
|
|
104
|
-
|
|
105
|
-
| Component | Props (beyond `NodeProps`) | Notes |
|
|
106
|
-
|-----------|---------------------------|-------|
|
|
107
|
-
| `Document` | `title? author? subject? creator? producer? language?` | Root. Children must be `Page` elements |
|
|
108
|
-
| `Page` | `size? orientation? style? wrap? dpi?` | `size` defaults to A4. Overflowing content paginates automatically |
|
|
109
|
-
| `View` | — | Flex container, analogous to a `<div>` |
|
|
110
|
-
| `Text` | — | All raw strings must live inside a `Text` |
|
|
111
|
-
| `Link` | `href` (required) | Hyperlink; serialized `href` becomes react-pdf's `src` internally |
|
|
112
|
-
| `Image` | `src: string \| Binary` (required) | URL, `data:` URI, or `Binary` with an image `contentType` |
|
|
113
|
-
| `PageNumber` | `format?` | Resolves at layout time. Tokens: `{pageNumber}`, `{totalPages}`. Default `'{pageNumber}'`. Combine with `fixed` for running footers |
|
|
114
|
-
|
|
115
|
-
`NodeProps` (shared by every node inside a page): `style`, `fixed` (repeat on
|
|
116
|
-
every page — headers/footers), `break` (force page break before the node),
|
|
117
|
-
`wrap` (allow splitting across pages, default `true`), `minPresenceAhead`
|
|
118
|
-
(move to next page unless this many points remain — avoids orphaned headings).
|
|
119
|
-
|
|
120
|
-
**Valid tree.** `render()` throws a `TypeError` for a tree that breaks one
|
|
121
|
-
of these rules:
|
|
122
|
-
|
|
123
|
-
- One `Document` root, with only `Page` children.
|
|
124
|
-
- Raw text only inside `Text` or `Link`.
|
|
125
|
-
- Inside `Text`: only text, `Text`, `Link` and `PageNumber`.
|
|
126
|
-
- Inside `Page` and `View`: only the marker components of this package.
|
|
127
|
-
|
|
128
|
-
User function components, `.map()` arrays, fragments, and conditional
|
|
129
|
-
rendering (`null` / `false` / `undefined`) work exactly like normal React —
|
|
130
|
-
they are evaluated and flattened during serialization.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## Styling
|
|
135
|
-
|
|
136
|
-
`PDFStyle` is a loose camelCase record (`fontSize`, `flexDirection`,
|
|
137
|
-
`marginTop`, …). Layout is the CSS **flex model only** — no grid, no float.
|
|
138
|
-
Values: numbers are points; strings accept units (`'2cm'`, `'5mm'`, `'50%'`).
|
|
139
|
-
`style` also accepts an array of styles.
|
|
140
|
-
|
|
141
|
-
Text styles (`color`, `fontSize`, `fontFamily`, …) **inherit** through the
|
|
142
|
-
tree, so defaults belong on `Page`:
|
|
18
|
+
1. Build the tree with the markers of `@t4h.framework/pdf`.
|
|
19
|
+
2. Call `render` and `await` it. It returns a `Binary`, not bytes.
|
|
20
|
+
3. Pass the `Binary` to the next activity (upload, e-mail, return value).
|
|
143
21
|
|
|
144
22
|
```tsx
|
|
145
|
-
|
|
23
|
+
import { Document, Page, Text, View, render } from '@t4h.framework/pdf'
|
|
24
|
+
|
|
25
|
+
const pdf = await render(
|
|
26
|
+
<Document title='Invoice'>
|
|
27
|
+
<Page size='A4' style={{ padding: '2cm', fontSize: 12 }}>
|
|
28
|
+
{items.map(item => (
|
|
29
|
+
<View key={item.id} style={{ flexDirection: 'row' }}>
|
|
30
|
+
<Text>{item.label}</Text>
|
|
31
|
+
</View>
|
|
32
|
+
))}
|
|
33
|
+
</Page>
|
|
34
|
+
</Document>,
|
|
35
|
+
)
|
|
146
36
|
```
|
|
147
37
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
38
|
+
Your own function components, fragments, `.map()` and conditionals work as in
|
|
39
|
+
React.
|
|
40
|
+
|
|
41
|
+
## Traps
|
|
42
|
+
|
|
43
|
+
- Import `Document`, `Page`, `Text` and the other markers from
|
|
44
|
+
`@t4h.framework/pdf`, never from `@react-pdf/renderer`. A react-pdf component
|
|
45
|
+
in the tree throws a `TypeError`. Never call a marker as a function.
|
|
46
|
+
- One `Document` root with only `Page` children. Raw text only in `Text` or
|
|
47
|
+
`Link`. Only text, `Text`, `Link` and `PageNumber` inside `Text`.
|
|
48
|
+
- Never use `new RenderPDFActivity()` or extend it. The lint rule
|
|
49
|
+
`no-framework-activity-in-app` reports it. Use `render` for everything,
|
|
50
|
+
encryption included.
|
|
51
|
+
- The runtime must provide a `FileSystemClaim` (from `@t4h.framework/fs`).
|
|
52
|
+
With no provider, `render` throws `ClaimProviderNotFoundException`.
|
|
53
|
+
- Styles use the flex model only: no grid, no float. Numbers are points;
|
|
54
|
+
strings take units (`'2cm'`, `'50%'`). Put text defaults on `Page`: they
|
|
55
|
+
inherit.
|
|
56
|
+
- Use a `Binary` as `Image src` or font source for private files and for
|
|
57
|
+
stable replays. A URL is fetched at each render and cannot authenticate.
|
|
58
|
+
Images: PNG or JPEG.
|
|
59
|
+
- Fonts: TTF or WOFF, never WOFF2 (Google Fonts links often point to WOFF2).
|
|
60
|
+
- With `encrypt`, `render` returns `[original, encrypted]`. When `protection`
|
|
61
|
+
is omitted, only `ProtectionFlags.Print` is granted. Viewers honor the
|
|
62
|
+
permission flags, but they are not cryptographic: only the password
|
|
63
|
+
protects.
|
|
64
|
+
|
|
65
|
+
## Fonts and encryption
|
|
154
66
|
|
|
155
67
|
```tsx
|
|
156
|
-
import {
|
|
157
|
-
import { Document, Page, PageNumber, Text, View, render } from '@t4h.framework/pdf'
|
|
158
|
-
|
|
159
|
-
function Header({ title }: { title: string }) {
|
|
160
|
-
return (
|
|
161
|
-
<View style={{ borderBottom: '1 solid #333', marginBottom: 16 }}>
|
|
162
|
-
<Text style={{ fontSize: 18 }}>{title}</Text>
|
|
163
|
-
</View>
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export const invoice = new Workflow({ id: 'invoice' }, async () => {
|
|
168
|
-
const pdf = await render(
|
|
169
|
-
<Document title='Invoice'>
|
|
170
|
-
<Page size='A4' style={{ padding: '2cm', fontSize: 12 }}>
|
|
171
|
-
<Header title='Invoice #42' />
|
|
172
|
-
{items.map(item => (
|
|
173
|
-
<View key={item.id} style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
174
|
-
<Text>{item.label}</Text>
|
|
175
|
-
<Text>{item.total}</Text>
|
|
176
|
-
</View>
|
|
177
|
-
))}
|
|
178
|
-
<PageNumber
|
|
179
|
-
fixed
|
|
180
|
-
format='{pageNumber}/{totalPages}'
|
|
181
|
-
style={{ position: 'absolute', bottom: 24, right: '2cm' }}
|
|
182
|
-
/>
|
|
183
|
-
</Page>
|
|
184
|
-
</Document>,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
// pdf is a Binary — pass it to other activities, e.g. an http form upload:
|
|
188
|
-
// await http.post(url, { form: { file: pdf } })
|
|
189
|
-
})
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
The returned `Binary` exposes `contentType` (`application/pdf`),
|
|
193
|
-
`contentLength`, and `stream()`.
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
|
|
197
|
-
## Images
|
|
198
|
-
|
|
199
|
-
`Image src` accepts three shapes:
|
|
200
|
-
|
|
201
|
-
- **URL string** — fetched by react-pdf at layout time. Simple, but the fetch
|
|
202
|
-
happens on every render/replay and cannot authenticate.
|
|
203
|
-
- **`data:` URI string** — for small assets bundled with the app.
|
|
204
|
-
- **`Binary`** — the replay-safe choice for anything private or that must stay
|
|
205
|
-
byte-identical across replays. The bytes are inlined as a `data:` URI on the
|
|
206
|
-
activity side. Typical source: an `http` response body
|
|
207
|
-
(`response.body.binary`) or any other claim-produced `Binary`.
|
|
208
|
-
|
|
209
|
-
Supported formats: PNG and JPEG.
|
|
68
|
+
import { ProtectionPresets, render } from '@t4h.framework/pdf'
|
|
210
69
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
## Fonts
|
|
214
|
-
|
|
215
|
-
`render(element, { fonts })` — an object keyed by **family name**. The value
|
|
216
|
-
is either a bare source (registers the regular 400/normal variant) or an
|
|
217
|
-
array of variants:
|
|
218
|
-
|
|
219
|
-
```tsx
|
|
220
|
-
const pdf = await render(<Doc />, {
|
|
70
|
+
const [original, encrypted] = await render(<Invoice />, {
|
|
221
71
|
fonts: {
|
|
222
|
-
|
|
223
|
-
Inter: 'https://cdn.example.com/Inter-Regular.ttf',
|
|
224
|
-
Geometric: fontResponse.body.binary,
|
|
225
|
-
|
|
226
|
-
// multiple weights/styles under one family
|
|
227
|
-
Roboto: [
|
|
228
|
-
{ src: robotoRegular.body.binary },
|
|
229
|
-
{ src: robotoBold.body.binary, weight: 700 },
|
|
230
|
-
{ src: robotoItalic.body.binary, style: 'italic' },
|
|
231
|
-
],
|
|
72
|
+
Roboto: [{ src: robotoRegular }, { src: robotoBold, weight: 700 }],
|
|
232
73
|
},
|
|
74
|
+
encrypt: { password: '1234', protection: ProtectionPresets.ReadOnly },
|
|
233
75
|
})
|
|
234
76
|
```
|
|
235
77
|
|
|
236
|
-
|
|
237
|
-
Weights are `100–900`; styles `'normal' | 'italic'`.
|
|
238
|
-
|
|
239
|
-
Rules of thumb:
|
|
240
|
-
|
|
241
|
-
- **TTF and WOFF only** — react-pdf cannot parse WOFF2 (Google Fonts links
|
|
242
|
-
often point to WOFF2; pick the TTF URL).
|
|
243
|
-
- Prefer `Binary` for private/licensed fonts and replay stability; a URL is a
|
|
244
|
-
mutable external reference fetched at layout time.
|
|
245
|
-
- Fonts registered per render; same-family re-registration is safe.
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Encryption
|
|
250
|
-
|
|
251
|
-
`render(element, { encrypt })` also persists a password-protected copy and
|
|
252
|
-
resolves with an `[original, encrypted]` tuple — the return type narrows
|
|
253
|
-
automatically:
|
|
78
|
+
## Running footer
|
|
254
79
|
|
|
255
80
|
```tsx
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
})
|
|
81
|
+
<PageNumber
|
|
82
|
+
fixed
|
|
83
|
+
format='{pageNumber}/{totalPages}'
|
|
84
|
+
style={{ position: 'absolute', bottom: 24, right: '2cm' }}
|
|
85
|
+
/>
|
|
262
86
|
```
|
|
263
87
|
|
|
264
|
-
|
|
265
|
-
`{ user, owner }` for a distinct owner password that bypasses permission
|
|
266
|
-
restrictions.
|
|
267
|
-
- `protection`: a `ProtectionFlags` bitmask granted to whoever opens with the
|
|
268
|
-
user password. Combine flags with `|` (`Print`, `Modify`, `CopyAndExtract`,
|
|
269
|
-
`AnnotateAndFill`, `FillForms`, `ExtractForAccessibility`, `Assemble`,
|
|
270
|
-
`PrintHighResolution`) or use a preset: `ProtectionPresets.ReadOnly`
|
|
271
|
-
(view + print only — invoices, statements) or `ProtectionPresets.All`.
|
|
272
|
-
- Permission flags are honored by PDF viewers, **not enforced
|
|
273
|
-
cryptographically** — the open password is the only real protection.
|
|
274
|
-
- Encryption is available only through `render`'s `encrypt` option.
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## Testing
|
|
88
|
+
## Test
|
|
279
89
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
harness API. The render runs in a worker in tests too, so the first render in a
|
|
284
|
-
test file pays for the thread starting up (around 400 ms).
|
|
90
|
+
Mock `FileSystemClaim.write`. It receives the path (`pdf/<id>.pdf`) and the
|
|
91
|
+
rendered `Buffer`; return any `Binary` stub. A class that extends `Binary` is
|
|
92
|
+
allowed in test files only.
|
|
285
93
|
|
|
286
94
|
```typescript
|
|
287
|
-
import { Readable } from 'node:stream'
|
|
288
|
-
import { Binary } from '@t4h.framework/core'
|
|
289
95
|
import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
|
|
290
96
|
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
291
97
|
|
|
292
|
-
|
|
293
|
-
public readonly contentType = 'application/pdf'
|
|
294
|
-
public readonly contentLength: number
|
|
295
|
-
|
|
296
|
-
constructor(private readonly data: Buffer) {
|
|
297
|
-
super()
|
|
298
|
-
this.contentLength = data.byteLength
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
public stream() {
|
|
302
|
-
return Readable.from(this.data)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const mockWrite = vi.fn().mockImplementation(async (_path, pdf: Buffer) => {
|
|
307
|
-
return new MockBinary(pdf)
|
|
308
|
-
})
|
|
309
|
-
|
|
98
|
+
const write = vi.fn(async (path: string, pdf: Buffer) => storedBinary)
|
|
310
99
|
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
311
100
|
|
|
312
|
-
await env.execute(
|
|
313
|
-
claims: [mockClaim(FileSystemClaim, { write
|
|
101
|
+
await env.execute(invoice, undefined, {
|
|
102
|
+
claims: [mockClaim(FileSystemClaim, { write })],
|
|
314
103
|
})
|
|
315
104
|
|
|
316
|
-
|
|
317
|
-
const [path, pdf] = mockWrite.mock.calls[0]
|
|
318
|
-
expect(path).toMatch(/^pdf\/.+\.pdf$/)
|
|
105
|
+
const [, pdf] = write.mock.calls[0]
|
|
319
106
|
expect(pdf.subarray(0, 5).toString()).toBe('%PDF-')
|
|
320
107
|
```
|
|
321
108
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
react-pdf element trees without rendering.
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## Checklist for agents
|
|
109
|
+
The render runs in a worker thread, so the first render in a test file takes
|
|
110
|
+
about 400 ms.
|
|
329
111
|
|
|
330
|
-
|
|
331
|
-
2. Build templates with the **marker components from this package**.
|
|
332
|
-
`render()` throws a `TypeError` for a `@react-pdf/renderer` component, a
|
|
333
|
-
marker called as a function throws, and the **Valid tree** rules above
|
|
334
|
-
apply.
|
|
335
|
-
3. Provide **`FileSystemClaim`** in the runtime (or mock it in tests). A
|
|
336
|
-
missing provider throws `ClaimProviderNotFoundException`.
|
|
337
|
-
4. Prefer **`Binary`** sources for images and fonts that must be private or
|
|
338
|
-
replay-stable; URLs are fetched at layout time on every render.
|
|
339
|
-
5. Fonts: object keyed by family, **TTF/WOFF only**, select variants via
|
|
340
|
-
`fontFamily`/`fontWeight`/`fontStyle`.
|
|
341
|
-
6. Style with the **flex model** (`PDFStyle`, camelCase, points or unit
|
|
342
|
-
strings); put inheritable text defaults on `Page`.
|
|
343
|
-
7. Use `fixed` + absolute positioning for running headers/footers and
|
|
344
|
-
`PageNumber` for pagination stamps.
|
|
345
|
-
8. Password-protect PDFs via `render`'s `encrypt` option — it returns
|
|
346
|
-
`[original, encrypted]`. Permission flags are viewer-honored, not
|
|
347
|
-
cryptographic.
|
|
348
|
-
9. Test workflows with **`TestWorkflowEnvironment`** + **`mockClaim`**,
|
|
349
|
-
asserting the `%PDF-` magic bytes on the `fs.write` call.
|
|
350
|
-
10. The package exports only its entry, so a deep import into `helpers/` or
|
|
351
|
-
`tools/` fails in `tsc`.
|
|
112
|
+
## Reference
|
|
352
113
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
114
|
+
- Guide: https://tech4humans-brasil.github.io/framework/pdf/
|
|
115
|
+
- `FileSystemClaim`: https://tech4humans-brasil.github.io/framework/fs/
|
|
116
|
+
- Workflow testing: https://tech4humans-brasil.github.io/framework/core/testing
|
|
117
|
+
- Types: `node_modules/@t4h.framework/pdf/dist/pdf.d.ts`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @t4h.framework/pdf
|
|
2
2
|
|
|
3
|
+
## 0.4.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#257](https://github.com/tech4humans-brasil/framework/pull/257) [`586bfb4`](https://github.com/tech4humans-brasil/framework/commit/586bfb4663f4a478527ee0c8db360603f034d615) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The pdf README is now short and links to the site, and the skill keeps only the traps and the default steps. The docs no longer describe a `PDF_RENDER_CONCURRENCY` variable, and they give the default `protection` of `encrypt`.
|
|
8
|
+
|
|
3
9
|
## 0.4.6
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
# @t4h.framework/pdf
|
|
2
2
|
|
|
3
|
-
Declarative PDF rendering for workflows.
|
|
3
|
+
Declarative PDF rendering for T4H Framework workflows. You write the document
|
|
4
|
+
as JSX with the marker components of this package, and `render` gives back a
|
|
5
|
+
`Binary` that it stores through the `FileSystemClaim` of `@t4h.framework/fs`.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
The render activity persists the generated PDF through `FileSystemClaim` (from `@t4h.framework/fs`) and returns the resulting `Binary`. The runtime must provide a concrete implementation.
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
13
|
-
|
|
14
|
-
// the runtime provides this:
|
|
15
|
-
// { provide: FileSystemClaim, value: new MyFileSystemClaimImpl() }
|
|
9
|
+
```bash
|
|
10
|
+
npm install @t4h.framework/pdf
|
|
16
11
|
```
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
### render
|
|
13
|
+
Peer dependencies: `@t4h.framework/core` and `@t4h.framework/fs`.
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
## Usage
|
|
23
16
|
|
|
24
17
|
```tsx
|
|
25
18
|
import { Workflow } from '@t4h.framework/core'
|
|
@@ -34,146 +27,10 @@ export const invoice = new Workflow({ id: 'invoice' }, async () => {
|
|
|
34
27
|
</Document>,
|
|
35
28
|
)
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
// await http.post(url, { form: { file: pdf } })
|
|
30
|
+
return pdf
|
|
39
31
|
})
|
|
40
32
|
```
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Components
|
|
45
|
-
|
|
46
|
-
The components exported by this package are **markers**: type-safe JSX tags that only exist inside a tree passed to `render()`. They are never invoked as functions — calling one directly or rendering it with another renderer throws.
|
|
47
|
-
|
|
48
|
-
| Component | Key props | Description |
|
|
49
|
-
|-----------|-----------|-------------|
|
|
50
|
-
| `Document` | `title`, `author`, `subject`, `language` | Root of the PDF. Children must be `Page` elements |
|
|
51
|
-
| `Page` | `size` (default `A4`), `orientation`, `style`, `dpi` | A page. Overflowing content paginates automatically |
|
|
52
|
-
| `View` | — | Flex container, analogous to a `<div>` |
|
|
53
|
-
| `Text` | — | Text content. All raw strings must live inside a `Text` |
|
|
54
|
-
| `Link` | `href` | Hyperlink |
|
|
55
|
-
| `Image` | `src` | Image from a URL, `data:` URI, or `Binary` |
|
|
56
|
-
| `PageNumber` | `format` | Text resolved to the current page number at layout time |
|
|
57
|
-
|
|
58
|
-
Every node inside a page also accepts: `style`, `fixed` (repeat on every page), `break` (force a page break), `wrap` (allow splitting across pages, default `true`), and `minPresenceAhead`.
|
|
59
|
-
|
|
60
|
-
User function components, `.map()` arrays, fragments, and conditional rendering work exactly like normal React:
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
function LineItem({ label, value }: { label: string; value: string }) {
|
|
64
|
-
return (
|
|
65
|
-
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
66
|
-
<Text>{label}</Text>
|
|
67
|
-
<Text>{value}</Text>
|
|
68
|
-
</View>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const pdf = await render(
|
|
73
|
-
<Document>
|
|
74
|
-
<Page size='A4'>
|
|
75
|
-
{items.map(item => (
|
|
76
|
-
<LineItem key={item.id} {...item} />
|
|
77
|
-
))}
|
|
78
|
-
{showFooter && <Text>Footer</Text>}
|
|
79
|
-
</Page>
|
|
80
|
-
</Document>,
|
|
81
|
-
)
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Tree rules enforced at serialization (violations throw):
|
|
85
|
-
|
|
86
|
-
- The tree must resolve to a single `Document` root, and `Document` children must be `Page` elements.
|
|
87
|
-
- Raw text must be wrapped in `Text`.
|
|
88
|
-
- Only `Text`, `Link`, and `PageNumber` are allowed inside `Text`.
|
|
89
|
-
|
|
90
|
-
## Styling
|
|
91
|
-
|
|
92
|
-
Styles are camelCase records following the CSS **flex model** (no grid, no float). Numbers are points; strings accept units (`'2cm'`, `'5mm'`, `'50%'`). Text styles inherit through the tree, so defaults belong on `Page`.
|
|
93
|
-
|
|
94
|
-
```tsx
|
|
95
|
-
<Page size='A4' style={{ padding: '2cm', fontSize: 12, color: '#00513a' }}>
|
|
96
|
-
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
97
|
-
<Text style={{ fontWeight: 'bold' }}>Bold</Text>
|
|
98
|
-
<Text style={[{ fontSize: 10 }, { color: '#666' }]}>Small and gray</Text>
|
|
99
|
-
</View>
|
|
100
|
-
</Page>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
The standard PDF fonts (Helvetica, Courier, Times-Roman) are available with no registration, including `bold` and `italic` variants.
|
|
104
|
-
|
|
105
|
-
## Images
|
|
106
|
-
|
|
107
|
-
`Image src` accepts a URL, a `data:` URI, or a `Binary` whose `contentType` is an image type (PNG or JPEG). Prefer `Binary` for anything private or that must stay byte-identical across replays — URLs are fetched at layout time on every render.
|
|
108
|
-
|
|
109
|
-
```tsx
|
|
110
|
-
const logo = await http.get('https://cdn.example.com/logo.png')
|
|
111
|
-
|
|
112
|
-
<Image src={logo.body.binary} style={{ width: 120 }} />
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Fonts
|
|
34
|
+
## Documentation
|
|
116
35
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```tsx
|
|
120
|
-
const pdf = await render(<Doc />, {
|
|
121
|
-
fonts: {
|
|
122
|
-
Inter: 'https://cdn.example.com/Inter-Regular.ttf',
|
|
123
|
-
Roboto: [
|
|
124
|
-
{ src: robotoRegular.body.binary },
|
|
125
|
-
{ src: robotoBold.body.binary, weight: 700 },
|
|
126
|
-
{ src: robotoItalic.body.binary, style: 'italic' },
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Select variants in styles with `fontFamily`, `fontWeight` (`100–900`), and `fontStyle` (`'normal' | 'italic'`).
|
|
133
|
-
|
|
134
|
-
Each variant reaches the process-wide font store once, keyed by family, weight, style and the file's digest, so passing `fonts` on every render costs nothing after the first.
|
|
135
|
-
|
|
136
|
-
## Encryption
|
|
137
|
-
|
|
138
|
-
Pass `encrypt` to also persist a password-protected copy — `render` then resolves with an `[original, encrypted]` pair (the return type narrows automatically). `password` is a bare string or `{ user, owner }`; `protection` is a `ProtectionFlags` bitmask (defaults follow the viewer; use `ProtectionPresets.ReadOnly` for view-and-print documents). Permission flags are honored by viewers, not enforced cryptographically.
|
|
139
|
-
|
|
140
|
-
The encrypted copy is written with pdfkit's own encryption (AES-128, PDF 1.6) from the layout the first render already produced, so neither a re-encryption pass over the finished file nor a second layout is paid for.
|
|
141
|
-
|
|
142
|
-
```tsx
|
|
143
|
-
import { ProtectionPresets, render } from '@t4h.framework/pdf'
|
|
144
|
-
|
|
145
|
-
const [original, encrypted] = await render(<Invoice />, {
|
|
146
|
-
encrypt: { password: '12345678900', protection: ProtectionPresets.ReadOnly },
|
|
147
|
-
})
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## Page numbers
|
|
151
|
-
|
|
152
|
-
`PageNumber` resolves at layout time. The `format` string may reference `{pageNumber}` and `{totalPages}`; combine with `fixed` and absolute positioning for running footers.
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
<PageNumber
|
|
156
|
-
fixed
|
|
157
|
-
format='{pageNumber}/{totalPages}'
|
|
158
|
-
style={{ position: 'absolute', bottom: 24, right: '2cm' }}
|
|
159
|
-
/>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Testing
|
|
163
|
-
|
|
164
|
-
Test workflows with `TestWorkflowEnvironment` from `@t4h.framework/core/testing`, mocking `FileSystemClaim` — the rendered buffer is handed to `fs.write`, so assert on the `%PDF-` magic bytes there:
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
|
|
168
|
-
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
169
|
-
|
|
170
|
-
const mockWrite = vi.fn().mockImplementation(async (_path, pdf) => new MockBinary(pdf))
|
|
171
|
-
|
|
172
|
-
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
173
|
-
|
|
174
|
-
await env.execute(workflow, input, {
|
|
175
|
-
claims: [mockClaim(FileSystemClaim, { write: mockWrite, read: vi.fn() })],
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
expect(mockWrite.mock.calls[0][1].subarray(0, 5).toString()).toBe('%PDF-')
|
|
179
|
-
```
|
|
36
|
+
https://tech4humans-brasil.github.io/framework/pdf/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t4h.framework/pdf",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Declarative PDF rendering module for the T4H Framework",
|
|
5
5
|
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/pdf",
|
|
6
6
|
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"test:watch": "vitest"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@t4h.framework/core": "^0.
|
|
37
|
-
"@t4h.framework/fs": "^0.
|
|
36
|
+
"@t4h.framework/core": "^0.21.0",
|
|
37
|
+
"@t4h.framework/fs": "^0.14.0",
|
|
38
38
|
"@types/react": "^19.2.17",
|
|
39
39
|
"muhammara": "^6.0.5",
|
|
40
40
|
"typescript": "^5.9.3",
|