@t4h.framework/pdf 0.0.0-experimental-20260907052843
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 +353 -0
- package/CHANGELOG.md +35 -0
- package/README.md +175 -0
- package/dist/activities/RenderPDFActivity.d.ts +26 -0
- package/dist/activities/RenderPDFActivity.d.ts.map +1 -0
- package/dist/activities/RenderPDFActivity.js +35 -0
- package/dist/activities/RenderPDFActivity.js.map +1 -0
- package/dist/components.d.ts +50 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +41 -0
- package/dist/components.js.map +1 -0
- package/dist/helpers/document-to-element.d.ts +5 -0
- package/dist/helpers/document-to-element.d.ts.map +1 -0
- package/dist/helpers/document-to-element.js +52 -0
- package/dist/helpers/document-to-element.js.map +1 -0
- package/dist/helpers/element-to-document.d.ts +4 -0
- package/dist/helpers/element-to-document.d.ts.map +1 -0
- package/dist/helpers/element-to-document.js +135 -0
- package/dist/helpers/element-to-document.js.map +1 -0
- package/dist/helpers/register-fonts.d.ts +14 -0
- package/dist/helpers/register-fonts.d.ts.map +1 -0
- package/dist/helpers/register-fonts.js +40 -0
- package/dist/helpers/register-fonts.js.map +1 -0
- package/dist/pdf.d.ts +5 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +5 -0
- package/dist/pdf.js.map +1 -0
- package/dist/tools/encrypt-pdf.d.ts +25 -0
- package/dist/tools/encrypt-pdf.d.ts.map +1 -0
- package/dist/tools/encrypt-pdf.js +42 -0
- package/dist/tools/encrypt-pdf.js.map +1 -0
- package/dist/typings/pdf.d.ts +85 -0
- package/dist/typings/pdf.d.ts.map +1 -0
- package/dist/typings/pdf.js +2 -0
- package/dist/typings/pdf.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-pdf
|
|
3
|
+
description: >-
|
|
4
|
+
Guides correct use of @t4h.framework/pdf in T4H Framework workflows: the
|
|
5
|
+
`render()` helper, PDF marker components (Document, Page, View, Text, Link,
|
|
6
|
+
Image, PageNumber), custom fonts, Binary image sources, styling,
|
|
7
|
+
password-protected PDFs via the `encrypt` option, and the FileSystemClaim
|
|
8
|
+
the render activity requires. Use when generating PDFs in workflows,
|
|
9
|
+
building PDF templates with JSX, registering fonts, encrypting PDFs, or
|
|
10
|
+
working in packages/pdf.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# @t4h.framework/pdf
|
|
14
|
+
|
|
15
|
+
Declarative PDF rendering for workflows, backed by `@react-pdf/renderer`.
|
|
16
|
+
Package path: `framework/packages/pdf`. Peer dependencies:
|
|
17
|
+
`@t4h.framework/core`, `@t4h.framework/fs`. Regular dependencies: `react`,
|
|
18
|
+
`@react-pdf/renderer`.
|
|
19
|
+
|
|
20
|
+
Import surface (from `src/pdf.ts` only — the `helpers/` modules are internal):
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
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. The
|
|
52
|
+
> package also exports the underlying `RenderPDFActivity`, but workflow code
|
|
53
|
+
> never imports, instantiates, or extends it. The `encryptPDF` tool in
|
|
54
|
+
> `src/tools/` is internal and not exported — encryption always goes through
|
|
55
|
+
> `render`'s `encrypt` option.
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
`Workflow` → `render(<Document>…</Document>, options?)`
|
|
60
|
+
(`History.reconciler(RenderPDFActivity)`) →
|
|
61
|
+
|
|
62
|
+
1. **`toInput`** (workflow side): the JSX tree is serialized into a plain
|
|
63
|
+
`DocumentNode` JSON tree that is stored in the workflow history. User
|
|
64
|
+
function components, fragments, arrays, and conditionals are flattened
|
|
65
|
+
here; only marker components and strings remain.
|
|
66
|
+
2. **`run`** (activity side): fonts are registered, the `DocumentNode` tree is
|
|
67
|
+
converted back into real `@react-pdf/renderer` elements (`Binary` image
|
|
68
|
+
sources become `data:` URIs), rendered to a buffer, and written through
|
|
69
|
+
**`FileSystemClaim`** at `pdf/${activityId}.pdf`. With the `encrypt`
|
|
70
|
+
option, a password-protected copy is also written at
|
|
71
|
+
`pdf/${activityId}-encrypted.pdf`.
|
|
72
|
+
3. The activity returns the **`Binary`** the claim produced — a reference to
|
|
73
|
+
the stored file, not in-memory bytes. Replay-safe by construction. With
|
|
74
|
+
`encrypt`, it returns the `[original, encrypted]` pair instead, and the
|
|
75
|
+
`render` return type narrows automatically based on the options.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Claims
|
|
80
|
+
|
|
81
|
+
### FileSystemClaim (required)
|
|
82
|
+
|
|
83
|
+
The render activity persists the PDF through `fs`. Any workflow that calls
|
|
84
|
+
`render()` needs a `FileSystemClaim` provider:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
{ provide: FileSystemClaim, value: new MyFileSystemClaimImpl() }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Tests mock its `write` to return a `Binary` (see **Testing**).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Marker components
|
|
95
|
+
|
|
96
|
+
The components exported by this package are **markers**: they exist only as
|
|
97
|
+
type-safe JSX tags inside a tree passed to `render()`. JSX never invokes
|
|
98
|
+
them — the serializer recognizes them by reference. Calling one directly
|
|
99
|
+
(`Image({ src })`) or rendering them with another renderer throws.
|
|
100
|
+
|
|
101
|
+
| Component | Props (beyond `NodeProps`) | Notes |
|
|
102
|
+
|-----------|---------------------------|-------|
|
|
103
|
+
| `Document` | `title? author? subject? creator? producer? language?` | Root. Children must be `Page` elements |
|
|
104
|
+
| `Page` | `size? orientation? style? wrap? dpi?` | `size` defaults to A4. Overflowing content paginates automatically |
|
|
105
|
+
| `View` | — | Flex container, analogous to a `<div>` |
|
|
106
|
+
| `Text` | — | All raw strings must live inside a `Text` |
|
|
107
|
+
| `Link` | `href` (required) | Hyperlink; serialized `href` becomes react-pdf's `src` internally |
|
|
108
|
+
| `Image` | `src: string \| Binary` (required) | URL, `data:` URI, or `Binary` with an image `contentType` |
|
|
109
|
+
| `PageNumber` | `format?` | Resolves at layout time. Tokens: `{pageNumber}`, `{totalPages}`. Default `'{pageNumber}'`. Combine with `fixed` for running footers |
|
|
110
|
+
|
|
111
|
+
`NodeProps` (shared by every node inside a page): `style`, `fixed` (repeat on
|
|
112
|
+
every page — headers/footers), `break` (force page break before the node),
|
|
113
|
+
`wrap` (allow splitting across pages, default `true`), `minPresenceAhead`
|
|
114
|
+
(move to next page unless this many points remain — avoids orphaned headings).
|
|
115
|
+
|
|
116
|
+
**Tree rules enforced at serialization** (violations throw `TypeError`):
|
|
117
|
+
|
|
118
|
+
- The tree must resolve to a single `Document` root.
|
|
119
|
+
- `Document` children must be `Page` elements.
|
|
120
|
+
- Raw text must be wrapped in `Text` (`"x"` directly inside `View`/`Page` throws).
|
|
121
|
+
- Only `Text`, `Link`, and `PageNumber` are allowed inside `Text`.
|
|
122
|
+
- `Image` is not allowed inside `Text`.
|
|
123
|
+
|
|
124
|
+
User function components, `.map()` arrays, fragments, and conditional
|
|
125
|
+
rendering (`null` / `false` / `undefined`) work exactly like normal React —
|
|
126
|
+
they are evaluated and flattened during serialization.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Styling
|
|
131
|
+
|
|
132
|
+
`PDFStyle` is a loose camelCase record (`fontSize`, `flexDirection`,
|
|
133
|
+
`marginTop`, …). Layout is the CSS **flex model only** — no grid, no float.
|
|
134
|
+
Values: numbers are points; strings accept units (`'2cm'`, `'5mm'`, `'50%'`).
|
|
135
|
+
`style` also accepts an array of styles.
|
|
136
|
+
|
|
137
|
+
Text styles (`color`, `fontSize`, `fontFamily`, …) **inherit** through the
|
|
138
|
+
tree, so defaults belong on `Page`:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<Page size='A4' style={{ padding: '2cm', fontSize: 12, color: '#00513a' }}>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Built-in fonts (no registration needed): Helvetica, Courier, Times-Roman —
|
|
145
|
+
with `fontWeight: 'bold'` / `fontStyle: 'italic'` variants.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Rendering in a workflow
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { Workflow } from '@t4h.framework/core'
|
|
153
|
+
import { Document, Page, PageNumber, Text, View, render } from '@t4h.framework/pdf'
|
|
154
|
+
|
|
155
|
+
function Header({ title }: { title: string }) {
|
|
156
|
+
return (
|
|
157
|
+
<View style={{ borderBottom: '1 solid #333', marginBottom: 16 }}>
|
|
158
|
+
<Text style={{ fontSize: 18 }}>{title}</Text>
|
|
159
|
+
</View>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const invoice = new Workflow({ id: 'invoice' }, async () => {
|
|
164
|
+
const pdf = await render(
|
|
165
|
+
<Document title='Invoice'>
|
|
166
|
+
<Page size='A4' style={{ padding: '2cm', fontSize: 12 }}>
|
|
167
|
+
<Header title='Invoice #42' />
|
|
168
|
+
{items.map(item => (
|
|
169
|
+
<View key={item.id} style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
170
|
+
<Text>{item.label}</Text>
|
|
171
|
+
<Text>{item.total}</Text>
|
|
172
|
+
</View>
|
|
173
|
+
))}
|
|
174
|
+
<PageNumber
|
|
175
|
+
fixed
|
|
176
|
+
format='{pageNumber}/{totalPages}'
|
|
177
|
+
style={{ position: 'absolute', bottom: 24, right: '2cm' }}
|
|
178
|
+
/>
|
|
179
|
+
</Page>
|
|
180
|
+
</Document>,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// pdf is a Binary — pass it to other activities, e.g. an http form upload:
|
|
184
|
+
// await http.post(url, { form: { file: pdf } })
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The returned `Binary` exposes `contentType` (`application/pdf`),
|
|
189
|
+
`contentLength`, and `stream()`.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Images
|
|
194
|
+
|
|
195
|
+
`Image src` accepts three shapes:
|
|
196
|
+
|
|
197
|
+
- **URL string** — fetched by react-pdf at layout time. Simple, but the fetch
|
|
198
|
+
happens on every render/replay and cannot authenticate.
|
|
199
|
+
- **`data:` URI string** — for small assets bundled with the app.
|
|
200
|
+
- **`Binary`** — the replay-safe choice for anything private or that must stay
|
|
201
|
+
byte-identical across replays. The bytes are inlined as a `data:` URI on the
|
|
202
|
+
activity side. Typical source: an `http` response body
|
|
203
|
+
(`response.body.binary`) or any other claim-produced `Binary`.
|
|
204
|
+
|
|
205
|
+
Supported formats: PNG and JPEG.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Fonts
|
|
210
|
+
|
|
211
|
+
`render(element, { fonts })` — an object keyed by **family name**. The value
|
|
212
|
+
is either a bare source (registers the regular 400/normal variant) or an
|
|
213
|
+
array of variants:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const pdf = await render(<Doc />, {
|
|
217
|
+
fonts: {
|
|
218
|
+
// bare source: string URL or Binary
|
|
219
|
+
Inter: 'https://cdn.example.com/Inter-Regular.ttf',
|
|
220
|
+
Geometric: fontResponse.body.binary,
|
|
221
|
+
|
|
222
|
+
// multiple weights/styles under one family
|
|
223
|
+
Roboto: [
|
|
224
|
+
{ src: robotoRegular.body.binary },
|
|
225
|
+
{ src: robotoBold.body.binary, weight: 700 },
|
|
226
|
+
{ src: robotoItalic.body.binary, style: 'italic' },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Select variants in styles with `fontFamily` + `fontWeight` + `fontStyle`.
|
|
233
|
+
Weights are `100–900`; styles `'normal' | 'italic'`.
|
|
234
|
+
|
|
235
|
+
Rules of thumb:
|
|
236
|
+
|
|
237
|
+
- **TTF and WOFF only** — react-pdf cannot parse WOFF2 (Google Fonts links
|
|
238
|
+
often point to WOFF2; pick the TTF URL).
|
|
239
|
+
- Prefer `Binary` for private/licensed fonts and replay stability; a URL is a
|
|
240
|
+
mutable external reference fetched at layout time.
|
|
241
|
+
- Fonts registered per render; same-family re-registration is safe.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Encryption
|
|
246
|
+
|
|
247
|
+
`render(element, { encrypt })` also persists a password-protected copy and
|
|
248
|
+
resolves with an `[original, encrypted]` tuple — the return type narrows
|
|
249
|
+
automatically:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
const [original, encrypted] = await render(<Invoice />, {
|
|
253
|
+
encrypt: {
|
|
254
|
+
password: customer.documentNumber,
|
|
255
|
+
protection: ProtectionPresets.ReadOnly,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
- `password`: a bare string (same password opens and owns the file) or
|
|
261
|
+
`{ user, owner }` for a distinct owner password that bypasses permission
|
|
262
|
+
restrictions.
|
|
263
|
+
- `protection`: a `ProtectionFlags` bitmask granted to whoever opens with the
|
|
264
|
+
user password. Combine flags with `|` (`Print`, `Modify`, `CopyAndExtract`,
|
|
265
|
+
`AnnotateAndFill`, `FillForms`, `ExtractForAccessibility`, `Assemble`,
|
|
266
|
+
`PrintHighResolution`) or use a preset: `ProtectionPresets.ReadOnly`
|
|
267
|
+
(view + print only — invoices, statements) or `ProtectionPresets.All`.
|
|
268
|
+
- Permission flags are honored by PDF viewers, **not enforced
|
|
269
|
+
cryptographically** — the open password is the only real protection.
|
|
270
|
+
- Encryption is **only** available through `render`'s `encrypt` option. Never
|
|
271
|
+
import from `src/tools/encrypt-pdf.js` — it is internal and not exported.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Testing
|
|
276
|
+
|
|
277
|
+
Test the **workflow** with `TestWorkflowEnvironment` from
|
|
278
|
+
`@t4h.framework/core/testing`, mocking the single claim the render activity
|
|
279
|
+
uses: `FileSystemClaim`. See the **framework-workflow-testing** skill for the
|
|
280
|
+
harness API.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { Readable } from 'node:stream'
|
|
284
|
+
import { Binary } from '@t4h.framework/core'
|
|
285
|
+
import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
|
|
286
|
+
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
287
|
+
|
|
288
|
+
class MockBinary extends Binary {
|
|
289
|
+
public readonly contentType = 'application/pdf'
|
|
290
|
+
public readonly contentLength: number
|
|
291
|
+
|
|
292
|
+
constructor(private readonly data: Buffer) {
|
|
293
|
+
super()
|
|
294
|
+
this.contentLength = data.byteLength
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public stream() {
|
|
298
|
+
return Readable.from(this.data)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const mockWrite = vi.fn().mockImplementation(async (_path, pdf: Buffer) => {
|
|
303
|
+
return new MockBinary(pdf)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
307
|
+
|
|
308
|
+
await env.execute(workflow, input, {
|
|
309
|
+
claims: [mockClaim(FileSystemClaim, { write: mockWrite, read: vi.fn() })],
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// The activity hands the rendered buffer to fs.write — assert on it:
|
|
313
|
+
const [path, pdf] = mockWrite.mock.calls[0]
|
|
314
|
+
expect(path).toMatch(/^pdf\/.+\.pdf$/)
|
|
315
|
+
expect(pdf.subarray(0, 5).toString()).toBe('%PDF-')
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
For helper-level tests (serialization, element mapping, fonts), see
|
|
319
|
+
`src/helpers/__tests__/` — they assert on the `DocumentNode` JSON shape and on
|
|
320
|
+
react-pdf element trees without rendering.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Checklist for agents
|
|
325
|
+
|
|
326
|
+
1. In workflows, **always** use **`render()`** — never define, extend, or
|
|
327
|
+
hand-instantiate `RenderPDFActivity`.
|
|
328
|
+
2. Build templates with the **marker components from this package** — never
|
|
329
|
+
import `Document`/`Page`/etc. from `@react-pdf/renderer` in workflow code,
|
|
330
|
+
and never call a marker as a function.
|
|
331
|
+
3. Wrap every raw string in **`Text`**; only `Text`/`Link`/`PageNumber` go
|
|
332
|
+
inside `Text`; `Document` children are `Page` only.
|
|
333
|
+
4. Ensure the runtime provides **`FileSystemClaim`** (or mock it in tests) —
|
|
334
|
+
the rendered PDF is persisted through it and returned as a `Binary`.
|
|
335
|
+
5. Prefer **`Binary`** sources for images and fonts that must be private or
|
|
336
|
+
replay-stable; URLs are fetched at layout time on every render.
|
|
337
|
+
6. Fonts: object keyed by family, **TTF/WOFF only**, select variants via
|
|
338
|
+
`fontFamily`/`fontWeight`/`fontStyle`.
|
|
339
|
+
7. Style with the **flex model** (`PDFStyle`, camelCase, points or unit
|
|
340
|
+
strings); put inheritable text defaults on `Page`.
|
|
341
|
+
8. Use `fixed` + absolute positioning for running headers/footers and
|
|
342
|
+
`PageNumber` for pagination stamps.
|
|
343
|
+
9. Password-protect PDFs **only** via `render`'s `encrypt` option — it
|
|
344
|
+
returns `[original, encrypted]`. Never import the internal `encryptPDF`
|
|
345
|
+
tool. Remember permission flags are viewer-honored, not cryptographic.
|
|
346
|
+
10. Test workflows with **`TestWorkflowEnvironment`** + **`mockClaim`**,
|
|
347
|
+
asserting the `%PDF-` magic bytes on the `fs.write` call.
|
|
348
|
+
11. Do not document or import APIs not exported from `@t4h.framework/pdf`
|
|
349
|
+
(the `helpers/` and `tools/` modules are internal).
|
|
350
|
+
|
|
351
|
+
See also: **framework-workflow-testing** for the test harness;
|
|
352
|
+
**framework-fs** for `FileSystemClaim`; **framework-http** for fetching
|
|
353
|
+
`Binary` assets (images, fonts) used in templates.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @t4h.framework/pdf
|
|
2
|
+
|
|
3
|
+
## 0.0.0-experimental-20260907052843
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`a30386e`](https://github.com/tech4humans-brasil/framework/commit/a30386ebf832d1eec5989b0a12a56d91a6999421)]:
|
|
8
|
+
- @t4h.framework/core@0.0.0-experimental-20260907052843
|
|
9
|
+
- @t4h.framework/fs@0.0.0-experimental-20260907052843
|
|
10
|
+
|
|
11
|
+
## 0.2.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Re-releases the accidental `1.0.0` as a patch. `1.0.0` was published by mistake (changesets bumps peer dependents with a major when a peer dependency gets a minor bump); this package stays on the `0.x` line.
|
|
16
|
+
- Updated dependencies [[`8c56f76`](https://github.com/tech4humans-brasil/framework/commit/8c56f7697390a1ea966d1e91ff5f35ce5751a0d7), [`8c56f76`](https://github.com/tech4humans-brasil/framework/commit/8c56f7697390a1ea966d1e91ff5f35ce5751a0d7)]:
|
|
17
|
+
- @t4h.framework/fs@0.8.0
|
|
18
|
+
|
|
19
|
+
## 0.2.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [`389e0b7`](https://github.com/tech4humans-brasil/framework/commit/389e0b72b5025dc85f33b371c320a3821314d41e) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Adds encryption option
|
|
24
|
+
|
|
25
|
+
## 0.1.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- [`095c3a9`](https://github.com/tech4humans-brasil/framework/commit/095c3a91191b9f541519447f212a8e9509065569) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Republish with resolved dependency versions. The previous release leaked the `workspace:^` protocol into the published manifests, breaking installs outside the monorepo.
|
|
30
|
+
|
|
31
|
+
## 0.1.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- [`dd3bc50`](https://github.com/tech4humans-brasil/framework/commit/dd3bc508e295e531bf59ce2909fc22b3b4355d2c) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Initialize package
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# @t4h.framework/pdf
|
|
2
|
+
|
|
3
|
+
Declarative PDF rendering for workflows. Build documents with JSX components, render them replay-safely through the workflow history, and get back a `Binary` persisted via the filesystem claim. Backed by [`@react-pdf/renderer`](https://react-pdf.org).
|
|
4
|
+
|
|
5
|
+
## Claim
|
|
6
|
+
|
|
7
|
+
### FileSystemClaim
|
|
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() }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Rendering
|
|
19
|
+
|
|
20
|
+
### render
|
|
21
|
+
|
|
22
|
+
Renders a JSX tree to a PDF `Binary` through the workflow history. The tree is serialized into the history on the workflow side and rendered on the activity side, so the step is replay-safe.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Workflow } from '@t4h.framework/core'
|
|
26
|
+
import { Document, Page, Text, render } from '@t4h.framework/pdf'
|
|
27
|
+
|
|
28
|
+
export const invoice = new Workflow({ id: 'invoice' }, async () => {
|
|
29
|
+
const pdf = await render(
|
|
30
|
+
<Document title='Invoice'>
|
|
31
|
+
<Page size='A4' style={{ padding: '2cm', fontSize: 12 }}>
|
|
32
|
+
<Text>Hello world</Text>
|
|
33
|
+
</Page>
|
|
34
|
+
</Document>,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// pdf is a Binary — hand it to other activities, e.g. an upload:
|
|
38
|
+
// await http.post(url, { form: { file: pdf } })
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The returned `Binary` exposes `contentType` (`application/pdf`), `contentLength`, and `stream()`.
|
|
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
|
|
116
|
+
|
|
117
|
+
Register custom fonts per render with an object keyed by family name. Values are a bare source — a URL string or a `Binary` — for the regular variant, or an array of variants with `weight` / `style`. TTF and WOFF only (no WOFF2).
|
|
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
|
+
## Encryption
|
|
135
|
+
|
|
136
|
+
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.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { ProtectionPresets, render } from '@t4h.framework/pdf'
|
|
140
|
+
|
|
141
|
+
const [original, encrypted] = await render(<Invoice />, {
|
|
142
|
+
encrypt: { password: '12345678900', protection: ProtectionPresets.ReadOnly },
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Page numbers
|
|
147
|
+
|
|
148
|
+
`PageNumber` resolves at layout time. The `format` string may reference `{pageNumber}` and `{totalPages}`; combine with `fixed` and absolute positioning for running footers.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
<PageNumber
|
|
152
|
+
fixed
|
|
153
|
+
format='{pageNumber}/{totalPages}'
|
|
154
|
+
style={{ position: 'absolute', bottom: 24, right: '2cm' }}
|
|
155
|
+
/>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Testing
|
|
159
|
+
|
|
160
|
+
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:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
|
|
164
|
+
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
165
|
+
|
|
166
|
+
const mockWrite = vi.fn().mockImplementation(async (_path, pdf) => new MockBinary(pdf))
|
|
167
|
+
|
|
168
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
169
|
+
|
|
170
|
+
await env.execute(workflow, input, {
|
|
171
|
+
claims: [mockClaim(FileSystemClaim, { write: mockWrite, read: vi.fn() })],
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(mockWrite.mock.calls[0][1].subarray(0, 5).toString()).toBe('%PDF-')
|
|
175
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Binary, SyncActivity } from '@t4h.framework/core';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
import { type Fonts } from '../helpers/register-fonts.js';
|
|
4
|
+
import { type EncryptPDFOptions } from '../tools/encrypt-pdf.js';
|
|
5
|
+
import type { DocumentNode } from '../typings/pdf.js';
|
|
6
|
+
export type RenderPDFOptions = {
|
|
7
|
+
fonts?: Fonts;
|
|
8
|
+
encrypt?: EncryptPDFOptions;
|
|
9
|
+
};
|
|
10
|
+
export type RenderPDFInput = {
|
|
11
|
+
document: DocumentNode;
|
|
12
|
+
} & RenderPDFOptions;
|
|
13
|
+
export declare class RenderPDFActivity extends SyncActivity<readonly [element: ReactElement, options?: RenderPDFOptions], RenderPDFInput, Binary | [Binary, Binary], Binary | [Binary, Binary]> {
|
|
14
|
+
private readonly claims;
|
|
15
|
+
toInput(element: ReactElement, options?: RenderPDFOptions): RenderPDFInput;
|
|
16
|
+
run(input: RenderPDFInput): Promise<Binary | [Binary, Binary]>;
|
|
17
|
+
toOutput(output: Binary | [Binary, Binary]): Binary | [Binary, Binary];
|
|
18
|
+
}
|
|
19
|
+
export declare function render(element: ReactElement, options: RenderPDFOptions & {
|
|
20
|
+
encrypt: EncryptPDFOptions;
|
|
21
|
+
}): Promise<[original: Binary, encrypted: Binary]>;
|
|
22
|
+
export declare function render(element: ReactElement, options?: RenderPDFOptions & {
|
|
23
|
+
encrypt?: undefined;
|
|
24
|
+
}): Promise<Binary>;
|
|
25
|
+
export declare function render(element: ReactElement, options?: RenderPDFOptions): Promise<Binary | [Binary, Binary]>;
|
|
26
|
+
//# sourceMappingURL=RenderPDFActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenderPDFActivity.d.ts","sourceRoot":"","sources":["../../src/activities/RenderPDFActivity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAE1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAIzC,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACxE,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,YAAY,CAAA;CACvB,GAAG,gBAAgB,CAAA;AAEpB,qBAAa,iBAAkB,SAAQ,YAAY,CACjD,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,gBAAgB,CAAC,EAC5D,cAAc,EACd,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAC1B;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAErD,OAAO,CACZ,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,gBAAgB,GACzB,cAAc;IAQJ,GAAG,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAmBpE,QAAQ,CACb,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;CAG7B;AAED,wBAAsB,MAAM,CAC1B,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,gBAAgB,GAAG;IAAE,OAAO,EAAE,iBAAiB,CAAA;CAAE,GACzD,OAAO,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAA;AACjD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,gBAAgB,GAAG;IAAE,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GACnD,OAAO,CAAC,MAAM,CAAC,CAAA;AAClB,wBAAsB,MAAM,CAC1B,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
2
|
+
import { Binary, Claim, History, SyncActivity } from '@t4h.framework/core';
|
|
3
|
+
import { FileSystemClaim } from '@t4h.framework/fs';
|
|
4
|
+
import { documentToElement } from '../helpers/document-to-element.js';
|
|
5
|
+
import { elementToDocument } from '../helpers/element-to-document.js';
|
|
6
|
+
import { registerFonts } from '../helpers/register-fonts.js';
|
|
7
|
+
import { encryptPDF } from '../tools/encrypt-pdf.js';
|
|
8
|
+
export class RenderPDFActivity extends SyncActivity {
|
|
9
|
+
claims = new Claim({ fs: FileSystemClaim });
|
|
10
|
+
toInput(element, options) {
|
|
11
|
+
return {
|
|
12
|
+
document: elementToDocument(element),
|
|
13
|
+
fonts: options?.fonts,
|
|
14
|
+
encrypt: options?.encrypt,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async run(input) {
|
|
18
|
+
if (input.fonts)
|
|
19
|
+
await registerFonts(input.fonts);
|
|
20
|
+
const element = await documentToElement(input.document);
|
|
21
|
+
const pdf = await renderToBuffer(element);
|
|
22
|
+
const original = await this.claims.fs.write(`pdf/${this.id}.pdf`, pdf);
|
|
23
|
+
if (!input.encrypt)
|
|
24
|
+
return original;
|
|
25
|
+
const encrypted = await this.claims.fs.write(`pdf/${this.id}-encrypted.pdf`, encryptPDF(pdf, input.encrypt));
|
|
26
|
+
return [original, encrypted];
|
|
27
|
+
}
|
|
28
|
+
toOutput(output) {
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function render(element, options) {
|
|
33
|
+
return await History.reconciler(RenderPDFActivity, element, options);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=RenderPDFActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenderPDFActivity.js","sourceRoot":"","sources":["../../src/activities/RenderPDFActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAGnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AACrE,OAAO,EAAE,aAAa,EAAc,MAAM,8BAA8B,CAAA;AACxE,OAAO,EAAE,UAAU,EAA0B,MAAM,yBAAyB,CAAA;AAY5E,MAAM,OAAO,iBAAkB,SAAQ,YAKtC;IACkB,MAAM,GAAG,IAAI,KAAK,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,CAAA;IAErD,OAAO,CACZ,OAAqB,EACrB,OAA0B;QAE1B,OAAO;YACL,QAAQ,EAAE,iBAAiB,CAAC,OAAO,CAAC;YACpC,KAAK,EAAE,OAAO,EAAE,KAAK;YACrB,OAAO,EAAE,OAAO,EAAE,OAAO;SAC1B,CAAA;IACH,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,KAAqB;QACpC,IAAI,KAAK,CAAC,KAAK;YAAE,MAAM,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAEjD,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QAEvD,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAA;QAEzC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QAEtE,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO,QAAQ,CAAA;QAEnC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAC1C,OAAO,IAAI,CAAC,EAAE,gBAAgB,EAC9B,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAA;QAED,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IAC9B,CAAC;IAEM,QAAQ,CACb,MAAiC;QAEjC,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAcD,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,OAAqB,EACrB,OAA0B;IAE1B,OAAO,MAAM,OAAO,CAAC,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;AACtE,CAAC"}
|