@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CONFORMANCE.md +4 -4
  3. package/README.md +54 -12
  4. package/dist/agent/AGENTS.md +44 -0
  5. package/dist/agent/pages/docs/examples/supported-behavior.md +75 -0
  6. package/dist/agent/pages/docs/examples.md +163 -0
  7. package/dist/agent/pages/docs/getting-started/installation.md +75 -0
  8. package/dist/agent/pages/docs/getting-started/quickstart.md +126 -0
  9. package/dist/agent/pages/docs/getting-started.md +63 -0
  10. package/dist/agent/pages/docs/guides/contents-links-bookmarks.md +123 -0
  11. package/dist/agent/pages/docs/guides/deployment.md +118 -0
  12. package/dist/agent/pages/docs/guides/errors-and-debugging.md +90 -0
  13. package/dist/agent/pages/docs/guides/images-and-fonts.md +102 -0
  14. package/dist/agent/pages/docs/guides/recipes.md +258 -0
  15. package/dist/agent/pages/docs/guides/remote-images.md +72 -0
  16. package/dist/agent/pages/docs/guides/reusable-components.md +81 -0
  17. package/dist/agent/pages/docs/guides/standalone-node.md +107 -0
  18. package/dist/agent/pages/docs/guides/svg-graphics.md +165 -0
  19. package/dist/agent/pages/docs/guides/testing.md +119 -0
  20. package/dist/agent/pages/docs/learn/document-tree.md +91 -0
  21. package/dist/agent/pages/docs/learn/runtime-and-data.md +63 -0
  22. package/dist/agent/pages/docs/learn/styling-and-layout.md +79 -0
  23. package/dist/agent/pages/docs/learn/templates-and-registry.md +96 -0
  24. package/dist/agent/pages/docs/reference/composables.md +55 -0
  25. package/dist/agent/pages/docs/reference/define-pdf.md +96 -0
  26. package/dist/agent/pages/docs/reference/errors-and-limits.md +89 -0
  27. package/dist/agent/pages/docs/reference/module-options.md +104 -0
  28. package/dist/agent/pages/docs/reference/page-sizes.md +54 -0
  29. package/dist/agent/pages/docs/reference/primitives.md +216 -0
  30. package/dist/agent/pages/docs/reference/registry.md +185 -0
  31. package/dist/agent/pages/docs/reference/standalone.md +53 -0
  32. package/dist/agent/pages/docs/reference/styles.md +176 -0
  33. package/dist/agent/pages/docs/reference/test-utilities.md +128 -0
  34. package/dist/module.json +1 -1
  35. package/dist/module.mjs +1 -1
  36. package/dist/test.mjs +13 -1
  37. 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.