@lupinum/nuxt-pdf 0.4.0-beta.4 → 0.4.0-beta.6
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 +18 -0
- package/CONFORMANCE.md +4 -4
- package/README.md +54 -12
- package/dist/agent/AGENTS.md +44 -0
- package/dist/agent/pages/docs/examples/supported-behavior.md +75 -0
- package/dist/agent/pages/docs/examples.md +163 -0
- package/dist/agent/pages/docs/getting-started/installation.md +75 -0
- package/dist/agent/pages/docs/getting-started/quickstart.md +126 -0
- package/dist/agent/pages/docs/getting-started.md +63 -0
- package/dist/agent/pages/docs/guides/contents-links-bookmarks.md +123 -0
- package/dist/agent/pages/docs/guides/deployment.md +118 -0
- package/dist/agent/pages/docs/guides/errors-and-debugging.md +90 -0
- package/dist/agent/pages/docs/guides/images-and-fonts.md +102 -0
- package/dist/agent/pages/docs/guides/recipes.md +258 -0
- package/dist/agent/pages/docs/guides/remote-images.md +72 -0
- package/dist/agent/pages/docs/guides/reusable-components.md +81 -0
- package/dist/agent/pages/docs/guides/standalone-node.md +107 -0
- package/dist/agent/pages/docs/guides/svg-graphics.md +165 -0
- package/dist/agent/pages/docs/guides/testing.md +119 -0
- package/dist/agent/pages/docs/learn/document-tree.md +91 -0
- package/dist/agent/pages/docs/learn/runtime-and-data.md +63 -0
- package/dist/agent/pages/docs/learn/styling-and-layout.md +79 -0
- package/dist/agent/pages/docs/learn/templates-and-registry.md +96 -0
- package/dist/agent/pages/docs/reference/composables.md +55 -0
- package/dist/agent/pages/docs/reference/define-pdf.md +96 -0
- package/dist/agent/pages/docs/reference/errors-and-limits.md +89 -0
- package/dist/agent/pages/docs/reference/module-options.md +104 -0
- package/dist/agent/pages/docs/reference/page-sizes.md +54 -0
- package/dist/agent/pages/docs/reference/primitives.md +216 -0
- package/dist/agent/pages/docs/reference/registry.md +185 -0
- package/dist/agent/pages/docs/reference/standalone.md +53 -0
- package/dist/agent/pages/docs/reference/styles.md +176 -0
- package/dist/agent/pages/docs/reference/test-utilities.md +128 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/test.mjs +13 -1
- package/package.json +27 -48
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Production recipes"
|
|
3
|
+
description: "Copyable Vue patterns for invoices, statements, reports, repeating matter, and payment QR codes."
|
|
4
|
+
url: "https://nuxt-pdf.lupinum.com/docs/guides/recipes"
|
|
5
|
+
route: "/docs/guides/recipes"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Production recipes
|
|
13
|
+
|
|
14
|
+
> Copyable Vue patterns for invoices, statements, reports, repeating matter, and payment QR codes.
|
|
15
|
+
|
|
16
|
+
These are application recipes, not extra framework primitives. They compose the
|
|
17
|
+
same small `PdfView`/`PdfText` surface used by the tested playground documents.
|
|
18
|
+
|
|
19
|
+
Use this page as a pattern index:
|
|
20
|
+
|
|
21
|
+
- [Repeating header, footer, and page numbers](#repeating-header-footer-and-page-numbers)
|
|
22
|
+
- [Address and key/value blocks](#address-or-party-block)
|
|
23
|
+
- [Invoice lines, totals, and shared table columns](#invoice-lines-and-totals)
|
|
24
|
+
- [Dot leaders](#dot-leaders-for-contents-and-price-lists)
|
|
25
|
+
- [Payment details and QR graphics](#payment-details-and-qr)
|
|
26
|
+
- [Headings and continuation headers](#keep-a-heading-with-what-follows)
|
|
27
|
+
- [Reports with a table of contents](#report-sections-and-a-table-of-contents)
|
|
28
|
+
|
|
29
|
+
Read [Build reusable document components](/raw/docs/guides/reusable-components.md)
|
|
30
|
+
before turning a recipe into a shared component. Exact style values remain in
|
|
31
|
+
the [style reference](/raw/docs/reference/styles.md).
|
|
32
|
+
|
|
33
|
+
## Repeating header, footer, and page numbers
|
|
34
|
+
|
|
35
|
+
A `fixed` node repeats when one authored page wraps into multiple output pages.
|
|
36
|
+
Keep the page padding large enough that flowing content never overlaps it.
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<PdfPage :style="{ paddingBottom: 52, paddingHorizontal: 40, paddingTop: 64 }">
|
|
40
|
+
<PdfView fixed :style="{ left: 40, position: 'absolute', right: 40, top: 24 }">
|
|
41
|
+
<PdfText>Quarterly statement</PdfText>
|
|
42
|
+
</PdfView>
|
|
43
|
+
<PdfText
|
|
44
|
+
fixed
|
|
45
|
+
:render="({ pageNumber, totalPages }) => `Page ${pageNumber} / ${totalPages}`"
|
|
46
|
+
:style="{ bottom: 24, position: 'absolute', right: 40 }"
|
|
47
|
+
/>
|
|
48
|
+
<slot />
|
|
49
|
+
</PdfPage>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Address or party block
|
|
53
|
+
|
|
54
|
+
Use a typed child component so the invoice template remains readable.
|
|
55
|
+
|
|
56
|
+
```vue [pdfs/components/PartyBlock.vue]
|
|
57
|
+
<script setup lang="ts">
|
|
58
|
+
defineProps<{ label: string, lines: string[] }>()
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<PdfView :style="{ width: 220 }">
|
|
63
|
+
<PdfText :style="{ color: '#68736B', fontSize: 8, marginBottom: 6 }">
|
|
64
|
+
{{ label }}
|
|
65
|
+
</PdfText>
|
|
66
|
+
<PdfText v-for="line in lines" :key="line" :style="{ fontSize: 10 }">
|
|
67
|
+
{{ line }}
|
|
68
|
+
</PdfText>
|
|
69
|
+
</PdfView>
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Key/value details
|
|
74
|
+
|
|
75
|
+
Keep widths explicit; PDF rows should not depend on browser table layout.
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<PdfView
|
|
79
|
+
v-for="entry in details"
|
|
80
|
+
:key="entry.label"
|
|
81
|
+
:style="{ flexDirection: 'row', marginBottom: 6 }"
|
|
82
|
+
>
|
|
83
|
+
<PdfText :style="{ color: '#68736B', width: 110 }">{{ entry.label }}</PdfText>
|
|
84
|
+
<PdfText :style="{ flex: 1 }">{{ entry.value }}</PdfText>
|
|
85
|
+
</PdfView>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Invoice lines and totals
|
|
89
|
+
|
|
90
|
+
Make each line indivisible with `wrap="false"`; keyed rows then move as a unit
|
|
91
|
+
instead of losing a description or amount at a page break.
|
|
92
|
+
|
|
93
|
+
```vue
|
|
94
|
+
<PdfView
|
|
95
|
+
v-for="line in invoice.lines"
|
|
96
|
+
:key="line.id"
|
|
97
|
+
:wrap="false"
|
|
98
|
+
:style="{ borderBottomColor: '#DDE3DE', borderBottomWidth: 1, flexDirection: 'row', paddingVertical: 10 }"
|
|
99
|
+
>
|
|
100
|
+
<PdfText :style="{ flex: 1 }">{{ line.description }}</PdfText>
|
|
101
|
+
<PdfText :style="{ textAlign: 'right', width: 90 }">{{ money(line.total) }}</PdfText>
|
|
102
|
+
</PdfView>
|
|
103
|
+
<PdfView :wrap="false" :style="{ marginLeft: 'auto', marginTop: 16, width: 220 }">
|
|
104
|
+
<PdfText :style="{ fontWeight: 700, textAlign: 'right' }">Total {{ money(total) }}</PdfText>
|
|
105
|
+
</PdfView>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Tables with a shared column spec
|
|
109
|
+
|
|
110
|
+
A table is a column of rows. Give every row the same `flexDirection: 'row'`
|
|
111
|
+
and fixed cell widths, and put those widths in one module. The header and the
|
|
112
|
+
body then cannot drift apart when a column changes.
|
|
113
|
+
|
|
114
|
+
```ts [pdfs/components/columns.ts]
|
|
115
|
+
export const columns = {
|
|
116
|
+
amount: { align: 'right', width: 86 },
|
|
117
|
+
description: { width: 246 },
|
|
118
|
+
quantity: { align: 'right', width: 52 },
|
|
119
|
+
} as const
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```vue
|
|
123
|
+
<PdfView :style="{ flexDirection: 'row', paddingBottom: 8 }" :wrap="false">
|
|
124
|
+
<PdfText :style="{ fontSize: 7, width: columns.description.width }">Description</PdfText>
|
|
125
|
+
<PdfText :style="{ fontSize: 7, textAlign: 'right', width: columns.quantity.width }">Qty</PdfText>
|
|
126
|
+
<PdfText :style="{ fontSize: 7, textAlign: 'right', width: columns.amount.width }">Amount</PdfText>
|
|
127
|
+
</PdfView>
|
|
128
|
+
<PdfView
|
|
129
|
+
v-for="line in invoice.lines"
|
|
130
|
+
:key="line.id"
|
|
131
|
+
:wrap="false"
|
|
132
|
+
:style="{ borderBottomColor: '#DDE3DE', borderBottomWidth: 1, flexDirection: 'row', paddingVertical: 10 }"
|
|
133
|
+
>
|
|
134
|
+
<PdfText :style="{ flex: 1 }">{{ line.description }}</PdfText>
|
|
135
|
+
<PdfText :style="{ textAlign: 'right', width: columns.amount.width }">{{ money(line.total) }}</PdfText>
|
|
136
|
+
</PdfView>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Keep body cells inside a `wrap="false"` row so a long line moves as a unit.
|
|
140
|
+
|
|
141
|
+
## Dot leaders for contents and price lists
|
|
142
|
+
|
|
143
|
+
A dotted rule that stretches between a label and a number is one view with a
|
|
144
|
+
dotted bottom border. Wrap each entry in a row with `alignItems: 'flex-end'`
|
|
145
|
+
and let the leader take the remaining space:
|
|
146
|
+
|
|
147
|
+
```vue [pdfs/components/common/DotLeader.vue]
|
|
148
|
+
<script setup lang="ts">
|
|
149
|
+
defineProps<{
|
|
150
|
+
baseline?: number
|
|
151
|
+
color: string
|
|
152
|
+
gap?: number
|
|
153
|
+
width?: number
|
|
154
|
+
}>()
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<template>
|
|
158
|
+
<PdfView
|
|
159
|
+
:style="{
|
|
160
|
+
borderBottomColor: color,
|
|
161
|
+
borderBottomStyle: 'dotted',
|
|
162
|
+
borderBottomWidth: width ?? 1,
|
|
163
|
+
flex: 1,
|
|
164
|
+
marginBottom: baseline ?? 3,
|
|
165
|
+
marginHorizontal: gap ?? 7,
|
|
166
|
+
}"
|
|
167
|
+
/>
|
|
168
|
+
</template>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```vue
|
|
172
|
+
<PdfView :style="{ alignItems: 'flex-end', flexDirection: 'row' }">
|
|
173
|
+
<PdfText>{{ section.title }}</PdfText>
|
|
174
|
+
<DotLeader color="#CDD5CF" />
|
|
175
|
+
<PdfText>{{ pageNumbers[section.id] ?? '' }}</PdfText>
|
|
176
|
+
</PdfView>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `baseline` offset sits the dots on the text baseline; start at `2` to `4`
|
|
180
|
+
and adjust against your font size.
|
|
181
|
+
|
|
182
|
+
## Payment details and QR
|
|
183
|
+
|
|
184
|
+
Generate the payment payload in setup code, turn consecutive dark QR modules
|
|
185
|
+
into one SVG path, and keep the payment panel together with `wrap="false"`.
|
|
186
|
+
|
|
187
|
+
```vue
|
|
188
|
+
<PdfView :wrap="false" :style="{ flexDirection: 'row', marginTop: 20 }">
|
|
189
|
+
<PdfView :style="{ flex: 1 }">
|
|
190
|
+
<PdfText>IBAN {{ payment.iban }}</PdfText>
|
|
191
|
+
<PdfText>BIC {{ payment.bic }}</PdfText>
|
|
192
|
+
<PdfText>Reference {{ invoice.number }}</PdfText>
|
|
193
|
+
</PdfView>
|
|
194
|
+
<PdfSvg :viewBox="`0 0 ${qrSize} ${qrSize}`" :style="{ height: 72, width: 72 }">
|
|
195
|
+
<PdfPath :d="qrPath" fill="#000000" />
|
|
196
|
+
</PdfSvg>
|
|
197
|
+
</PdfView>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Keep a heading with what follows
|
|
201
|
+
|
|
202
|
+
`minPresenceAhead` asks pagination to move the heading when too little usable
|
|
203
|
+
space remains. Use `wrap="false"` only when the whole section is genuinely
|
|
204
|
+
small enough to fit on a page.
|
|
205
|
+
|
|
206
|
+
```vue
|
|
207
|
+
<PdfView :style="{ marginTop: 24 }">
|
|
208
|
+
<PdfText :min-presence-ahead="54" :style="{ fontSize: 15, marginBottom: 10 }">
|
|
209
|
+
Payment terms
|
|
210
|
+
</PdfText>
|
|
211
|
+
<PdfText>{{ terms }}</PdfText>
|
|
212
|
+
</PdfView>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Long rows with continuation headers
|
|
216
|
+
|
|
217
|
+
Put the header and rows in one wrapping page. The fixed header repeats on every
|
|
218
|
+
continuation page; page padding reserves its space.
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<PdfPage :style="{ paddingHorizontal: 40, paddingTop: 72 }">
|
|
222
|
+
<PdfView fixed :style="{ flexDirection: 'row', left: 40, position: 'absolute', right: 40, top: 36 }">
|
|
223
|
+
<PdfText :style="{ flex: 1 }">Description</PdfText>
|
|
224
|
+
<PdfText :style="{ textAlign: 'right', width: 90 }">Amount</PdfText>
|
|
225
|
+
</PdfView>
|
|
226
|
+
<InvoiceLine v-for="line in lines" :key="line.id" :line="line" />
|
|
227
|
+
</PdfPage>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Report sections and a table of contents
|
|
231
|
+
|
|
232
|
+
Read the page map only where the document needs it. The first pass returns no
|
|
233
|
+
number, so keep an empty fallback; Nuxt PDF rerenders until the map stabilizes.
|
|
234
|
+
|
|
235
|
+
```vue
|
|
236
|
+
<script setup lang="ts">
|
|
237
|
+
const pageNumbers = usePdfPageNumbers()
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<template>
|
|
241
|
+
<PdfPage>
|
|
242
|
+
<PdfLink v-for="section in sections" :key="section.id" :href="`#${section.id}`">
|
|
243
|
+
{{ section.title }} ..... {{ pageNumbers[section.id] ?? '' }}
|
|
244
|
+
</PdfLink>
|
|
245
|
+
</PdfPage>
|
|
246
|
+
<PdfPage v-for="section in sections" :key="section.id">
|
|
247
|
+
<PdfView :id="section.id" :bookmark="section.title">
|
|
248
|
+
<PdfText :style="{ fontSize: 22 }">{{ section.title }}</PdfText>
|
|
249
|
+
<PdfText>{{ section.body }}</PdfText>
|
|
250
|
+
</PdfView>
|
|
251
|
+
</PdfPage>
|
|
252
|
+
</template>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The [links and table-of-contents guide](/raw/docs/guides/contents-links-bookmarks.md)
|
|
256
|
+
explains convergence and invalid destinations. Use the
|
|
257
|
+
[testing guide](/raw/docs/guides/testing.md) to protect document text, links, page
|
|
258
|
+
counts, and reviewed raster output.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Configure remote images"
|
|
3
|
+
description: "Admit specific HTTPS image paths and understand the request boundary."
|
|
4
|
+
url: "https://nuxt-pdf.lupinum.com/docs/guides/remote-images"
|
|
5
|
+
route: "/docs/guides/remote-images"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Configure remote images
|
|
13
|
+
|
|
14
|
+
> Admit specific HTTPS image paths and understand the request boundary.
|
|
15
|
+
|
|
16
|
+
Remote image fetching is off by default. Enable it only for HTTPS locations
|
|
17
|
+
that the application operator controls or trusts.
|
|
18
|
+
|
|
19
|
+
## Add an allowlist
|
|
20
|
+
|
|
21
|
+
Configure exact URL prefixes with a trailing slash:
|
|
22
|
+
|
|
23
|
+
```ts [nuxt.config.ts]
|
|
24
|
+
export default defineNuxtConfig({
|
|
25
|
+
modules: ['@lupinum/nuxt-pdf'],
|
|
26
|
+
pdf: {
|
|
27
|
+
remote: {
|
|
28
|
+
allow: [
|
|
29
|
+
'https://cdn.example.com/brand/',
|
|
30
|
+
'https://images.example.com/logos/',
|
|
31
|
+
],
|
|
32
|
+
timeoutMs: 10_000,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use an admitted URL in the template:
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<PdfImage
|
|
42
|
+
src="https://cdn.example.com/brand/logo.png"
|
|
43
|
+
:style="{ height: 40, width: 120 }"
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The module follows at most three redirects and checks the allowlist at each
|
|
48
|
+
hop. An allowed host cannot redirect to a disallowed location.
|
|
49
|
+
|
|
50
|
+
## Understand the boundary
|
|
51
|
+
|
|
52
|
+
- Allowlist entries use `https://host/path/`. Wildcards, credentials, query
|
|
53
|
+
strings, fragments, and missing trailing slashes are rejected.
|
|
54
|
+
- Requested image URLs can contain a query, but errors do not include it.
|
|
55
|
+
- Requests use `GET` without application headers, cookies, or credentials.
|
|
56
|
+
- PNG and JPEG signatures and dimensions are checked before layout. A response
|
|
57
|
+
`Content-Type` cannot admit other bytes.
|
|
58
|
+
- Per-image bytes, total bytes, decoded pixels, request count, concurrency, and
|
|
59
|
+
the full render deadline use the shared `pdf.limits` budget.
|
|
60
|
+
- Duplicate URLs are fetched once per render. No cross-render image cache is
|
|
61
|
+
promised.
|
|
62
|
+
|
|
63
|
+
<warning icon="lucide:shield-alert">
|
|
64
|
+
The allowlist controls hosts and paths. It does not provide private-IP or
|
|
65
|
+
DNS-rebinding protection. Do not allow user-controlled hosts. Authenticated
|
|
66
|
+
requests, custom headers, proxies, and remote fonts are not supported.
|
|
67
|
+
</warning>
|
|
68
|
+
|
|
69
|
+
`PDF_ASSET_BLOCKED` means the source form, allowlist, redirect, or timeout
|
|
70
|
+
policy rejected the request. `PDF_ASSET_INVALID` means admitted bytes were not a
|
|
71
|
+
valid PNG or JPEG. Size and request-budget failures use `PDF_LIMIT_EXCEEDED`.
|
|
72
|
+
See [Errors and limits](/raw/docs/reference/errors-and-limits.md) for the exact codes.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Build reusable document components"
|
|
3
|
+
description: "Organize repeated PDF sections without creating a second component system."
|
|
4
|
+
url: "https://nuxt-pdf.lupinum.com/docs/guides/reusable-components"
|
|
5
|
+
route: "/docs/guides/reusable-components"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Build reusable document components
|
|
13
|
+
|
|
14
|
+
> Organize repeated PDF sections without creating a second component system.
|
|
15
|
+
|
|
16
|
+
PDF components are ordinary local Vue components whose rendered roots are Nuxt
|
|
17
|
+
PDF primitives. Extract a component when it owns a meaningful document section
|
|
18
|
+
or repeats in more than one place.
|
|
19
|
+
|
|
20
|
+
## Keep ownership visible
|
|
21
|
+
|
|
22
|
+
Put document-specific components next to their document family:
|
|
23
|
+
|
|
24
|
+
```bash [Application files]
|
|
25
|
+
pdfs/
|
|
26
|
+
├── invoice.vue
|
|
27
|
+
└── components/
|
|
28
|
+
└── invoice/
|
|
29
|
+
├── InvoiceAddress.vue
|
|
30
|
+
├── InvoiceLine.vue
|
|
31
|
+
└── columns.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Move a component to `pdfs/components/` only when several templates use it. This
|
|
35
|
+
keeps invoice-specific decisions out of a generic component API.
|
|
36
|
+
|
|
37
|
+
## Pass typed document data
|
|
38
|
+
|
|
39
|
+
```vue [pdfs/components/invoice/InvoiceAddress.vue]
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
defineProps<{
|
|
42
|
+
label: string
|
|
43
|
+
lines: string[]
|
|
44
|
+
}>()
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<PdfView>
|
|
49
|
+
<PdfText :style="{ fontSize: 8, marginBottom: 4 }">
|
|
50
|
+
{{ label }}
|
|
51
|
+
</PdfText>
|
|
52
|
+
<PdfText v-for="line in lines" :key="line">
|
|
53
|
+
{{ line }}
|
|
54
|
+
</PdfText>
|
|
55
|
+
</PdfView>
|
|
56
|
+
</template>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Import it directly in the template. Use keyed `v-for`, slots, and `v-if` as you
|
|
60
|
+
would in another Vue component. Do not pass DOM attributes such as `class` or
|
|
61
|
+
`aria-*`; the PDF tree has no DOM.
|
|
62
|
+
|
|
63
|
+
## Share table geometry
|
|
64
|
+
|
|
65
|
+
Put widths used by both a header and row in one TypeScript module:
|
|
66
|
+
|
|
67
|
+
```ts [pdfs/components/invoice/columns.ts]
|
|
68
|
+
export const invoiceColumns = {
|
|
69
|
+
description: '58%',
|
|
70
|
+
quantity: '14%',
|
|
71
|
+
price: '14%',
|
|
72
|
+
total: '14%',
|
|
73
|
+
} as const
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A table is a column of rows. Give the header and every row the same
|
|
77
|
+
`flexDirection: 'row'` and column widths. The header and body then cannot drift
|
|
78
|
+
apart.
|
|
79
|
+
|
|
80
|
+
The [production recipes](/raw/docs/guides/recipes.md) show address blocks, shared
|
|
81
|
+
columns, totals, continuation headers, and table-of-contents patterns.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Render outside Nitro"
|
|
3
|
+
description: "Compile PDF templates once and render the generated registry in a Node worker."
|
|
4
|
+
url: "https://nuxt-pdf.lupinum.com/docs/guides/standalone-node"
|
|
5
|
+
route: "/docs/guides/standalone-node"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Render outside Nitro
|
|
13
|
+
|
|
14
|
+
> Compile PDF templates once and render the generated registry in a Node worker.
|
|
15
|
+
|
|
16
|
+
Use the standalone build when a Node worker owns document rendering outside
|
|
17
|
+
Nitro. Keep the Nuxt module for authoring and preview. Both paths use the same
|
|
18
|
+
compiler, resource admission, registry, and PDF engine.
|
|
19
|
+
|
|
20
|
+
## Build the registry
|
|
21
|
+
|
|
22
|
+
Keep trusted templates in `pdfs/` at the application root. Import application
|
|
23
|
+
helpers explicitly with relative paths. The standalone build does not read
|
|
24
|
+
Nuxt configuration, aliases, auto-imports, or layer configuration. PDF primitives,
|
|
25
|
+
`definePdf`, and `usePdfPageNumbers` remain available in templates.
|
|
26
|
+
Package imports must provide compiled JavaScript. Do not import source-only
|
|
27
|
+
packages or add runtime filesystem reads to templates or their helpers.
|
|
28
|
+
|
|
29
|
+
```ts [scripts/build-pdfs.mjs]
|
|
30
|
+
import { buildPdfRegistry } from '@lupinum/nuxt-pdf/build'
|
|
31
|
+
|
|
32
|
+
await buildPdfRegistry({
|
|
33
|
+
rootDir: process.cwd(),
|
|
34
|
+
outDir: './generated/pdfs',
|
|
35
|
+
fonts: [{ family: 'Invoice Sans', src: 'Roboto-Regular.ttf' }],
|
|
36
|
+
limits: { maxPages: 100, maxOutputBytes: 8_000_000 },
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Put the configured font in `pdfs/fonts/`. Put local images in `pdfs/assets/`.
|
|
41
|
+
Remove the `fonts` option if the templates use only built-in fonts.
|
|
42
|
+
|
|
43
|
+
Run this script before the backend build:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node scripts/build-pdfs.mjs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The output directory contains `index.mjs`, `index.d.mts`, a `types/` declaration
|
|
50
|
+
tree, and an ownership marker. Deploy the generated runtime and its package
|
|
51
|
+
dependencies. Keep the declarations for backend type checking. No Vue source,
|
|
52
|
+
test loader, or build compiler is needed to execute this generated registry.
|
|
53
|
+
The build tools remain package dependencies but are not imported by the runtime.
|
|
54
|
+
|
|
55
|
+
Use a dedicated output directory inside the application. Never put hand-written
|
|
56
|
+
files in it. Successful rebuilds replace its generated contents. Compilation
|
|
57
|
+
failures preserve the previous output. An existing nonempty directory without
|
|
58
|
+
the ownership marker is rejected. Choose another directory after this error.
|
|
59
|
+
Do not run concurrent builds into one output directory.
|
|
60
|
+
|
|
61
|
+
## Render validated data
|
|
62
|
+
|
|
63
|
+
The generated `pdf`, `renderPdf`, `getPdfTemplate`, and `pdfTemplateKeys` exports
|
|
64
|
+
have the same behavior as the [Nitro registry](/raw/docs/reference/registry.md).
|
|
65
|
+
Import the `.mjs` path. Ordinary TypeScript resolves its adjacent declarations.
|
|
66
|
+
|
|
67
|
+
```ts [workers/render-invoice.ts]
|
|
68
|
+
import { pdf } from '../generated/pdfs/index.mjs'
|
|
69
|
+
|
|
70
|
+
export async function renderInvoice() {
|
|
71
|
+
const result = await pdf.invoice.render({
|
|
72
|
+
customer: 'Example customer',
|
|
73
|
+
number: '2026-001',
|
|
74
|
+
lines: [],
|
|
75
|
+
})
|
|
76
|
+
return result.toUint8Array()
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Match the props to your own invoice template. Validate request data and check
|
|
81
|
+
authorization before rendering. Props are statically typed, not a runtime
|
|
82
|
+
validation schema. Never accept template source, filesystem paths, build
|
|
83
|
+
options, or unrestricted image URLs from a caller.
|
|
84
|
+
|
|
85
|
+
Local assets and configured fonts are validated and embedded during the build.
|
|
86
|
+
Remote images remain denied unless the build supplies an explicit `remote`
|
|
87
|
+
policy. Existing resource limits and error codes apply to each render. Fix the
|
|
88
|
+
named resource or lower input size after an admission or limit error.
|
|
89
|
+
|
|
90
|
+
The application owns jobs, snapshots, storage, document issuance, and delivery.
|
|
91
|
+
For an issued document, store and reuse completed bytes. Two fresh renders are
|
|
92
|
+
not promised to be byte-identical.
|
|
93
|
+
|
|
94
|
+
## Verify the deployment
|
|
95
|
+
|
|
96
|
+
Standalone Node rendering is tested locally with embedded fonts and images,
|
|
97
|
+
Unicode text, pagination, and concurrent requests. That is not a deployed
|
|
98
|
+
Convex or other serverless-runtime certification. Edge workers remain unsupported.
|
|
99
|
+
|
|
100
|
+
Before adoption, measure your largest expected document on the target runtime.
|
|
101
|
+
Check cold and warm duration, memory use, output limits, failed asset requests,
|
|
102
|
+
and concurrent isolation. Keep the previously tested Node route available for
|
|
103
|
+
new renders until this proof passes. Do not regenerate issued PDFs during a
|
|
104
|
+
runtime migration.
|
|
105
|
+
|
|
106
|
+
See [deployment guidance](/raw/docs/guides/deployment.md) for the runtime matrix and
|
|
107
|
+
operational limits.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Draw SVG graphics"
|
|
3
|
+
description: "The SVG primitive set, direct presentation props, scoped definitions, SVG text, and the supported numeric boundary."
|
|
4
|
+
url: "https://nuxt-pdf.lupinum.com/docs/guides/svg-graphics"
|
|
5
|
+
route: "/docs/guides/svg-graphics"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Draw SVG graphics
|
|
13
|
+
|
|
14
|
+
> The SVG primitive set, direct presentation props, scoped definitions, SVG text, and the supported numeric boundary.
|
|
15
|
+
|
|
16
|
+
Nuxt PDF ships a set of SVG drawing primitives that render through the same
|
|
17
|
+
layout and render engine as the document primitives. A `PdfSvg` participates in
|
|
18
|
+
normal page flow as a flex leaf, measured from its `viewBox` aspect ratio.
|
|
19
|
+
|
|
20
|
+
## The primitive set
|
|
21
|
+
|
|
22
|
+
| Component | Role |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `PdfSvg` | SVG root; a flex leaf in page flow |
|
|
25
|
+
| `PdfG` | A group; presentation attributes cascade to children |
|
|
26
|
+
| `PdfPath` | A path from a `d` string |
|
|
27
|
+
| `PdfRect` | A rectangle (`x`, `y`, `width`, `height`, `rx`, `ry`) |
|
|
28
|
+
| `PdfCircle` | A circle (`cx`, `cy`, `r`) |
|
|
29
|
+
| `PdfEllipse` | An ellipse (`cx`, `cy`, `rx`, `ry`) |
|
|
30
|
+
| `PdfLine` | A line (`x1`, `y1`, `x2`, `y2`) |
|
|
31
|
+
| `PdfPolyline` | An open polyline from a `points` string |
|
|
32
|
+
| `PdfPolygon` | A closed polygon from a `points` string |
|
|
33
|
+
| `PdfDefs` | Holds referenceable defs (gradients, clip paths) |
|
|
34
|
+
| `PdfClipPath` | A clip path referenced by `clipPath="url(#id)"` |
|
|
35
|
+
| `PdfLinearGradient` / `PdfRadialGradient` | Gradients referenced by `fill="url(#id)"` |
|
|
36
|
+
| `PdfStop` | A gradient color stop |
|
|
37
|
+
| `PdfTspan` | A text span inside an SVG `PdfText` |
|
|
38
|
+
|
|
39
|
+
Full prop tables are in the [primitive reference](/raw/docs/reference/primitives.md).
|
|
40
|
+
|
|
41
|
+
## A basic drawing
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<PdfSvg viewBox="0 0 120 120" :style="{ width: 120, height: 120 }">
|
|
45
|
+
<PdfRect x="8" y="8" width="104" height="104" rx="12" fill="#eef2ee" />
|
|
46
|
+
<PdfCircle cx="60" cy="60" r="34" fill="#315d3b" />
|
|
47
|
+
<PdfPath d="M44 62 l12 12 l22 -26" stroke="#ffffff" stroke-width="6" fill="none" />
|
|
48
|
+
</PdfSvg>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Presentation is direct, not a style cascade
|
|
52
|
+
|
|
53
|
+
Set SVG paint and geometry with direct props. `PdfG` and the shape primitives
|
|
54
|
+
do not accept a generic `style` prop, so there is no prop-versus-style
|
|
55
|
+
precedence to remember. `transform` is also a direct SVG prop; page-flow
|
|
56
|
+
primitives such as `PdfView` put transforms in `PdfStyle` instead.
|
|
57
|
+
|
|
58
|
+
`PdfSvg` is the exception only at the page-flow boundary: its `style` sizes and
|
|
59
|
+
positions the SVG root in the surrounding PDF layout. It does not become a bag
|
|
60
|
+
of SVG presentation attributes. SVG `PdfText` may use `style` for text metrics
|
|
61
|
+
such as `fontFamily` and `fontSize`, but its paint color is the direct `fill`
|
|
62
|
+
prop.
|
|
63
|
+
|
|
64
|
+
Kebab-case attributes written in Vue templates (`stroke-width`, `stop-color`)
|
|
65
|
+
are coerced to the camelCase keys the renderer reads, so both forms work:
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<!-- stroke-width and strokeWidth are equivalent on SVG nodes -->
|
|
69
|
+
<PdfLine x1="0" y1="0" x2="100" y2="0" stroke="#333" stroke-width="2" />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Presentation attributes set on a `PdfG` cascade to its children:
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<PdfG fill="#315d3b" transform="translate(10 10) rotate(15)">
|
|
76
|
+
<PdfRect x="0" y="0" width="20" height="20" />
|
|
77
|
+
<PdfRect x="30" y="0" width="20" height="20" />
|
|
78
|
+
</PdfG>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Numbers and transforms
|
|
82
|
+
|
|
83
|
+
Geometry accepts finite numbers or numeric strings; percentage strings are
|
|
84
|
+
accepted only by props typed as `PdfSvgLength`. `strokeWidth` does not accept a
|
|
85
|
+
percentage. Widths and radii must be non-negative, opacity and gradient-stop
|
|
86
|
+
values must be between `0` and `1` (or `0%` and `100%`), and a `viewBox` must
|
|
87
|
+
contain four finite numbers with positive width and height.
|
|
88
|
+
|
|
89
|
+
The transform surface accepts one to three space-separated
|
|
90
|
+
`translate(...)` or `rotate(...)` operations with unitless numeric arguments.
|
|
91
|
+
Scale, matrix, skew, angle units, and arbitrary transform strings are rejected.
|
|
92
|
+
|
|
93
|
+
Explicit zeroes keep SVG meaning despite truthiness fallbacks in the pinned
|
|
94
|
+
serializer:
|
|
95
|
+
|
|
96
|
+
- `fillOpacity="0"` paints fully transparent;
|
|
97
|
+
- `strokeWidth="0"` paints no stroke (not a PDF hairline); and
|
|
98
|
+
- a zero linear-gradient `x2`, or zero radial-gradient `cx`, `cy`, `fx`, `fy`,
|
|
99
|
+
or `r`, remains zero.
|
|
100
|
+
|
|
101
|
+
Nuxt PDF repairs those values after layout on the disposable resolved tree,
|
|
102
|
+
immediately before serialization. This is an engine-boundary correction, not a
|
|
103
|
+
second authoring format.
|
|
104
|
+
|
|
105
|
+
## Gradients and clip paths
|
|
106
|
+
|
|
107
|
+
Define a gradient or clip path inside a `PdfDefs` and reference it by `url(#id)`:
|
|
108
|
+
|
|
109
|
+
```vue
|
|
110
|
+
<PdfSvg viewBox="0 0 200 100" :style="{ width: 200, height: 100 }">
|
|
111
|
+
<PdfDefs>
|
|
112
|
+
<PdfLinearGradient id="brand" x1="0" y1="0" x2="1" y2="0">
|
|
113
|
+
<PdfStop offset="0" stop-color="#315d3b" />
|
|
114
|
+
<PdfStop offset="1" stop-color="#7bbf88" />
|
|
115
|
+
</PdfLinearGradient>
|
|
116
|
+
</PdfDefs>
|
|
117
|
+
<PdfRect x="0" y="0" width="200" height="100" fill="url(#brand)" />
|
|
118
|
+
</PdfSvg>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
<info icon="lucide:info">
|
|
122
|
+
A `url(#id)` reference resolves only against a `PdfDefs` in the same `PdfSvg`
|
|
123
|
+
subtree. Definition ids must be safe and unique within that SVG, and each SVG
|
|
124
|
+
accepts at most one `PdfDefs`. A missing, malformed, or incompatible reference
|
|
125
|
+
(for example a clip path used as a fill) fails with `PDF_TREE_INVALID`; it is
|
|
126
|
+
never rendered as an accidental fallback. The same definition id may be reused
|
|
127
|
+
in another `PdfSvg` because scopes are independent.
|
|
128
|
+
</info>
|
|
129
|
+
|
|
130
|
+
## SVG text
|
|
131
|
+
|
|
132
|
+
`PdfText` has two context-specific contracts. In page flow it accepts wrapping,
|
|
133
|
+
pagination, bookmark, destination, and dynamic-text props. Inside `PdfSvg`, both
|
|
134
|
+
`x` and `y` are required, `fill` is the direct paint prop, and page-flow-only
|
|
135
|
+
props are rejected. SVG text may hold `PdfTspan` children, which join and chain
|
|
136
|
+
along the x-axis:
|
|
137
|
+
|
|
138
|
+
```vue
|
|
139
|
+
<PdfSvg viewBox="0 0 200 40" :style="{ width: 200, height: 40 }">
|
|
140
|
+
<PdfText :x="0" :y="24" fill="#18251d">
|
|
141
|
+
<PdfTspan>Total </PdfTspan>
|
|
142
|
+
<PdfTspan fill="#315d3b">EUR 1,250.00</PdfTspan>
|
|
143
|
+
</PdfText>
|
|
144
|
+
</PdfSvg>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The text content is preserved in the extracted page text, so it remains
|
|
148
|
+
selectable and testable. `PdfTspan` accepts only
|
|
149
|
+
`x`, `y`, and `fill`; it has no generic `style`, stroke, transform, or other
|
|
150
|
+
shape presentation props.
|
|
151
|
+
|
|
152
|
+
## Nesting rules and what is not claimed
|
|
153
|
+
|
|
154
|
+
`PdfSvg` is a valid child of `PdfPage` and `PdfView` but is rejected directly
|
|
155
|
+
inside `PdfText`. Leaf shapes stay childless. Each container accepts only its
|
|
156
|
+
valid children; invalid nesting fails with a targeted diagnostic.
|
|
157
|
+
|
|
158
|
+
Within SVG, the following are **not** claimed in the current alpha:
|
|
159
|
+
|
|
160
|
+
- `Marker` (`markerStart` / `markerMid` / `markerEnd`);
|
|
161
|
+
- alternate `gradientUnits`, `gradientTransform`, gradient inheritance, and
|
|
162
|
+
`preserveAspectRatio` modes;
|
|
163
|
+
- radial-gradient inner radius (`fr`), which the pinned renderer hardcodes to
|
|
164
|
+
zero; and
|
|
165
|
+
- SVG image _files_ as an image source.
|