@lupinum/nuxt-pdf 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API_REPORT.md +262 -0
- package/CHANGELOG.md +168 -0
- package/CONFORMANCE.md +496 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/THIRD_PARTY_NOTICES.md +75 -0
- package/dist/module.d.mts +24 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +368 -0
- package/dist/runtime/authoring.d.ts +141 -0
- package/dist/runtime/authoring.js +25 -0
- package/dist/runtime/components/_props.d.ts +196 -0
- package/dist/runtime/components/_props.js +63 -0
- package/dist/runtime/components/document.d.ts +9 -0
- package/dist/runtime/components/document.js +26 -0
- package/dist/runtime/components/index.d.ts +4 -0
- package/dist/runtime/components/index.js +27 -0
- package/dist/runtime/components/svg.d.ts +17 -0
- package/dist/runtime/components/svg.js +50 -0
- package/dist/runtime/composables/index.d.ts +2 -0
- package/dist/runtime/composables/index.js +3 -0
- package/dist/runtime/composables/use-pdf-page-numbers.d.ts +39 -0
- package/dist/runtime/composables/use-pdf-page-numbers.js +17 -0
- package/dist/runtime/define-pdf.d.ts +6 -0
- package/dist/runtime/define-pdf.js +5 -0
- package/dist/runtime/fonts.d.ts +16 -0
- package/dist/runtime/fonts.js +0 -0
- package/dist/runtime/renderer/index.d.ts +3 -0
- package/dist/runtime/renderer/index.js +1 -0
- package/dist/runtime/renderer/node-ops.d.ts +5 -0
- package/dist/runtime/renderer/node-ops.js +281 -0
- package/dist/runtime/renderer/patch-prop.d.ts +3 -0
- package/dist/runtime/renderer/patch-prop.js +223 -0
- package/dist/runtime/renderer/render-component.d.ts +14 -0
- package/dist/runtime/renderer/render-component.js +201 -0
- package/dist/runtime/renderer/types.d.ts +33 -0
- package/dist/runtime/renderer/types.js +56 -0
- package/dist/runtime/renderer/validate-tree.d.ts +3 -0
- package/dist/runtime/renderer/validate-tree.js +428 -0
- package/dist/runtime/server/assets/errors.d.ts +12 -0
- package/dist/runtime/server/assets/errors.js +15 -0
- package/dist/runtime/server/assets/remote.d.ts +48 -0
- package/dist/runtime/server/assets/remote.js +234 -0
- package/dist/runtime/server/assets/resolve-asset.d.ts +53 -0
- package/dist/runtime/server/assets/resolve-asset.js +482 -0
- package/dist/runtime/server/engine/CONTRACTS.md +386 -0
- package/dist/runtime/server/engine/fonts.d.ts +8 -0
- package/dist/runtime/server/engine/fonts.js +19 -0
- package/dist/runtime/server/engine/layout-passes.d.ts +42 -0
- package/dist/runtime/server/engine/layout-passes.js +58 -0
- package/dist/runtime/server/engine/react-pdf-pdfkit.d.ts +7 -0
- package/dist/runtime/server/engine/render-document.d.ts +31 -0
- package/dist/runtime/server/engine/render-document.js +236 -0
- package/dist/runtime/server/index.d.ts +5 -0
- package/dist/runtime/server/index.js +9 -0
- package/dist/runtime/server/preview.d.ts +17 -0
- package/dist/runtime/server/preview.js +332 -0
- package/dist/runtime/server/registry.d.ts +41 -0
- package/dist/runtime/server/registry.js +270 -0
- package/dist/runtime/server/render-limits.d.ts +51 -0
- package/dist/runtime/server/render-limits.js +143 -0
- package/dist/runtime/server/result.d.ts +6 -0
- package/dist/runtime/server/result.js +81 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/shared/errors.d.ts +22 -0
- package/dist/runtime/shared/errors.js +22 -0
- package/dist/runtime/shared/index.d.ts +4 -0
- package/dist/runtime/shared/index.js +7 -0
- package/dist/runtime/shared/template.d.ts +63 -0
- package/dist/runtime/shared/template.js +1 -0
- package/dist/shared/nuxt-pdf.D2ZziYn4.mjs +1132 -0
- package/dist/test.d.mts +198 -0
- package/dist/test.mjs +696 -0
- package/dist/types.d.mts +13 -0
- package/package.json +135 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { mountPdfComponent } from "../renderer/index.js";
|
|
2
|
+
import {
|
|
3
|
+
NuxtPdfError,
|
|
4
|
+
PDF_ERROR_CODES
|
|
5
|
+
} from "../shared/errors.js";
|
|
6
|
+
import {
|
|
7
|
+
PDF_DEFINITION_PROPERTY
|
|
8
|
+
} from "../shared/template.js";
|
|
9
|
+
import {
|
|
10
|
+
createPdfImageResolutionState,
|
|
11
|
+
resolvePdfImageAssets
|
|
12
|
+
} from "./assets/resolve-asset.js";
|
|
13
|
+
import { countPages, renderDocument } from "./engine/render-document.js";
|
|
14
|
+
import { renderDocumentMultiPass } from "./engine/layout-passes.js";
|
|
15
|
+
import {
|
|
16
|
+
createRenderLimits,
|
|
17
|
+
enforceTreeLimits,
|
|
18
|
+
resolvePdfRenderLimits
|
|
19
|
+
} from "./render-limits.js";
|
|
20
|
+
import { createPdfFontStore } from "./engine/fonts.js";
|
|
21
|
+
import { createPdfRenderResult } from "./result.js";
|
|
22
|
+
const EMPTY_SCENARIOS = Object.freeze({});
|
|
23
|
+
const EMPTY_ASSETS = Object.freeze({});
|
|
24
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
const templateError = (ref, message, cause) => new NuxtPdfError(
|
|
26
|
+
PDF_ERROR_CODES.TemplateInvalid,
|
|
27
|
+
`Invalid ${templatePrefix(ref.key, ref.file)}: ${message}`,
|
|
28
|
+
{ cause, templateKey: ref.key, templateFile: ref.file }
|
|
29
|
+
);
|
|
30
|
+
const templatePrefix = (key, file) => file === void 0 ? `PDF template "${key}"` : `PDF template "${key}" (${file})`;
|
|
31
|
+
const enrichTemplateError = (error, key, file) => {
|
|
32
|
+
const prefix = templatePrefix(key, file);
|
|
33
|
+
if (error instanceof NuxtPdfError) {
|
|
34
|
+
if (error.templateKey === key) return error;
|
|
35
|
+
return new NuxtPdfError(error.code, `${prefix}: ${error.message}`, {
|
|
36
|
+
cause: error,
|
|
37
|
+
templateKey: key,
|
|
38
|
+
templateFile: file
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return new NuxtPdfError(
|
|
42
|
+
PDF_ERROR_CODES.RenderError,
|
|
43
|
+
`${prefix}: ${error instanceof Error ? error.message : String(error)}`,
|
|
44
|
+
{ cause: error, templateKey: key, templateFile: file }
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
const validateDefinition = (ref, definition) => {
|
|
48
|
+
if (!isObject(definition)) {
|
|
49
|
+
throw templateError(
|
|
50
|
+
ref,
|
|
51
|
+
"definePdf metadata is missing. Add one top-level definePdf({...}) call."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
for (const field of ["title", "filename"]) {
|
|
55
|
+
const value = definition[field];
|
|
56
|
+
if (value !== void 0 && typeof value !== "string" && typeof value !== "function") {
|
|
57
|
+
throw templateError(ref, `${field} must be a string or function.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (definition.language !== void 0 && typeof definition.language !== "string") {
|
|
61
|
+
throw templateError(ref, "language must be a string.");
|
|
62
|
+
}
|
|
63
|
+
const maxPasses = definition.maxPasses;
|
|
64
|
+
if (maxPasses !== void 0 && (typeof maxPasses !== "number" || !Number.isInteger(maxPasses) || maxPasses < 1)) {
|
|
65
|
+
throw templateError(ref, "maxPasses must be a positive integer.");
|
|
66
|
+
}
|
|
67
|
+
if (definition.sampleData !== void 0 && !isObject(definition.sampleData)) {
|
|
68
|
+
throw templateError(ref, "sampleData must be an object.");
|
|
69
|
+
}
|
|
70
|
+
if (definition.scenarios !== void 0) {
|
|
71
|
+
if (!isObject(definition.scenarios)) {
|
|
72
|
+
throw templateError(ref, "scenarios must be an object.");
|
|
73
|
+
}
|
|
74
|
+
for (const [name, props] of Object.entries(definition.scenarios)) {
|
|
75
|
+
if (!name || !isObject(props)) {
|
|
76
|
+
throw templateError(ref, `scenario "${name}" must contain a props object.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return definition;
|
|
81
|
+
};
|
|
82
|
+
const resolveMetadataValue = (ref, field, value, props) => {
|
|
83
|
+
let resolved;
|
|
84
|
+
try {
|
|
85
|
+
resolved = typeof value === "function" ? value(props) : value;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw templateError(ref, `${field} metadata could not be evaluated.`, error);
|
|
88
|
+
}
|
|
89
|
+
if (resolved !== void 0 && typeof resolved !== "string") {
|
|
90
|
+
throw templateError(ref, `${field} metadata must resolve to a string.`);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
};
|
|
94
|
+
const resolveMetadata = (ref, definition, props) => ({
|
|
95
|
+
title: resolveMetadataValue(ref, "title", definition.title, props),
|
|
96
|
+
filename: resolveMetadataValue(ref, "filename", definition.filename, props),
|
|
97
|
+
language: definition.language
|
|
98
|
+
});
|
|
99
|
+
const applyDocumentMetadata = (document, metadata) => {
|
|
100
|
+
if (metadata.title !== void 0) document.props.title = metadata.title;
|
|
101
|
+
if (metadata.language !== void 0) document.props.language = metadata.language;
|
|
102
|
+
};
|
|
103
|
+
const completedMetadata = (document, definition) => ({
|
|
104
|
+
filename: definition.filename,
|
|
105
|
+
language: typeof document.props.language === "string" ? document.props.language : void 0,
|
|
106
|
+
title: typeof document.props.title === "string" ? document.props.title : void 0
|
|
107
|
+
});
|
|
108
|
+
const renderTemplate = async (component, props, metadata, options, maxPasses, limits) => {
|
|
109
|
+
let mounted;
|
|
110
|
+
const imageState = createPdfImageResolutionState(limits);
|
|
111
|
+
const admitDocument = async (document) => {
|
|
112
|
+
enforceTreeLimits(document, limits);
|
|
113
|
+
applyDocumentMetadata(document, metadata);
|
|
114
|
+
await resolvePdfImageAssets(document, {
|
|
115
|
+
assets: options.assets ?? EMPTY_ASSETS,
|
|
116
|
+
limits,
|
|
117
|
+
remote: options.remote,
|
|
118
|
+
state: imageState
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
mounted = await mountPdfComponent(
|
|
123
|
+
component,
|
|
124
|
+
props
|
|
125
|
+
);
|
|
126
|
+
const fontStore = createPdfFontStore(options.fonts);
|
|
127
|
+
if (mounted.usesPageNumbers) {
|
|
128
|
+
const live = mounted;
|
|
129
|
+
const result2 = await renderDocumentMultiPass(
|
|
130
|
+
{
|
|
131
|
+
get document() {
|
|
132
|
+
return live.document;
|
|
133
|
+
},
|
|
134
|
+
feed: async (pages) => {
|
|
135
|
+
await live.feedPageNumbers(pages);
|
|
136
|
+
await admitDocument(live.document);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{ fontStore, maxPasses, limits }
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
bytes: result2.bytes,
|
|
143
|
+
metadata: completedMetadata(live.document, metadata),
|
|
144
|
+
passes: result2.passes,
|
|
145
|
+
pageCount: countPages(result2.layout)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
await admitDocument(mounted.document);
|
|
149
|
+
const document = mounted.document;
|
|
150
|
+
const result = await renderDocument(document, { fontStore, limits });
|
|
151
|
+
return {
|
|
152
|
+
bytes: result.bytes,
|
|
153
|
+
metadata: completedMetadata(mounted.document, metadata),
|
|
154
|
+
passes: 1,
|
|
155
|
+
pageCount: countPages(result.layout)
|
|
156
|
+
};
|
|
157
|
+
} finally {
|
|
158
|
+
mounted?.unmount();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
export const createPdfTemplate = (key, component, options = {}) => {
|
|
162
|
+
const ref = { key, file: options.file };
|
|
163
|
+
if (!key.trim()) {
|
|
164
|
+
throw templateError(ref, "template key must not be empty.");
|
|
165
|
+
}
|
|
166
|
+
const definition = validateDefinition(
|
|
167
|
+
ref,
|
|
168
|
+
component[PDF_DEFINITION_PROPERTY]
|
|
169
|
+
);
|
|
170
|
+
const renderWithDiagnostics = async (props) => {
|
|
171
|
+
if (!isObject(props)) {
|
|
172
|
+
throw templateError(ref, "render props must be an object.");
|
|
173
|
+
}
|
|
174
|
+
const start = performance.now();
|
|
175
|
+
const limits = createRenderLimits(resolvePdfRenderLimits(options.limits));
|
|
176
|
+
const definitionMetadata = resolveMetadata(ref, definition, props);
|
|
177
|
+
const {
|
|
178
|
+
bytes,
|
|
179
|
+
metadata,
|
|
180
|
+
passes,
|
|
181
|
+
pageCount
|
|
182
|
+
} = await renderTemplate(
|
|
183
|
+
component,
|
|
184
|
+
props,
|
|
185
|
+
definitionMetadata,
|
|
186
|
+
options,
|
|
187
|
+
definition.maxPasses,
|
|
188
|
+
limits
|
|
189
|
+
);
|
|
190
|
+
const result = createPdfRenderResult(bytes, metadata, {
|
|
191
|
+
durationMs: performance.now() - start,
|
|
192
|
+
pageCount,
|
|
193
|
+
passes,
|
|
194
|
+
registeredFontFaces: (options.fonts ?? []).map((font) => ({
|
|
195
|
+
family: font.family,
|
|
196
|
+
fontStyle: font.fontStyle,
|
|
197
|
+
fontWeight: font.fontWeight
|
|
198
|
+
}))
|
|
199
|
+
});
|
|
200
|
+
return result;
|
|
201
|
+
};
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
key,
|
|
204
|
+
resolveMetadata(props) {
|
|
205
|
+
if (!isObject(props)) {
|
|
206
|
+
throw templateError(ref, "metadata props must be an object.");
|
|
207
|
+
}
|
|
208
|
+
return resolveMetadata(ref, definition, props);
|
|
209
|
+
},
|
|
210
|
+
async render(props) {
|
|
211
|
+
try {
|
|
212
|
+
return await renderWithDiagnostics(props);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw enrichTemplateError(error, key, options.file);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
export const createPdfPreviewEntry = (template, component, options = {}) => {
|
|
220
|
+
const ref = { key: template.key, file: options.file };
|
|
221
|
+
const definition = validateDefinition(
|
|
222
|
+
ref,
|
|
223
|
+
component[PDF_DEFINITION_PROPERTY]
|
|
224
|
+
);
|
|
225
|
+
const scenarios = definition.scenarios ?? EMPTY_SCENARIOS;
|
|
226
|
+
const scenarioNames = Object.freeze(Object.keys(scenarios).sort());
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
template,
|
|
229
|
+
file: options.file,
|
|
230
|
+
scenarioNames,
|
|
231
|
+
getPreviewProps(scenario) {
|
|
232
|
+
if (scenario === void 0) return definition.sampleData;
|
|
233
|
+
if (!Object.prototype.hasOwnProperty.call(scenarios, scenario)) {
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
return scenarios[scenario];
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
export const createPdfRegistry = (entries) => {
|
|
241
|
+
for (const [key, template] of Object.entries(entries)) {
|
|
242
|
+
if (!template?.key) {
|
|
243
|
+
throw new TypeError(`PDF registry entry "${key}" is invalid.`);
|
|
244
|
+
}
|
|
245
|
+
if (template.key !== key) {
|
|
246
|
+
throw new TypeError(
|
|
247
|
+
`PDF registry entry "${key}" must use the same key as createPdfTemplate ("${template.key}").`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const pdf = Object.freeze(entries);
|
|
252
|
+
const pdfTemplateKeys = Object.freeze(Object.keys(pdf));
|
|
253
|
+
const getTemplate = (key) => Object.hasOwn(pdf, key) ? pdf[key] : void 0;
|
|
254
|
+
return Object.freeze({
|
|
255
|
+
pdf,
|
|
256
|
+
pdfTemplateKeys,
|
|
257
|
+
getPdfTemplate: getTemplate,
|
|
258
|
+
async renderPdf(key, props) {
|
|
259
|
+
const template = getTemplate(key);
|
|
260
|
+
if (!template) {
|
|
261
|
+
throw new NuxtPdfError(
|
|
262
|
+
PDF_ERROR_CODES.TemplateNotFound,
|
|
263
|
+
`PDF template "${key}" was not found.`,
|
|
264
|
+
{ templateKey: key }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return template.render(props);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PdfDocumentNode } from '../renderer/index.js';
|
|
2
|
+
export declare const DEFAULT_PDF_TIMEOUT_MS = 30000;
|
|
3
|
+
export declare const DEFAULT_PDF_MAX_PAGES = 2000;
|
|
4
|
+
export declare const DEFAULT_PDF_MAX_NODES = 50000;
|
|
5
|
+
export declare const DEFAULT_PDF_MAX_TREE_DEPTH = 128;
|
|
6
|
+
export declare const DEFAULT_PDF_MAX_TEXT_CHARACTERS = 2000000;
|
|
7
|
+
export declare const DEFAULT_PDF_MAX_IMAGES = 256;
|
|
8
|
+
export declare const DEFAULT_PDF_MAX_IMAGE_BYTES: number;
|
|
9
|
+
export declare const DEFAULT_PDF_MAX_TOTAL_IMAGE_BYTES: number;
|
|
10
|
+
export declare const DEFAULT_PDF_MAX_IMAGE_PIXELS = 25000000;
|
|
11
|
+
export declare const DEFAULT_PDF_MAX_TOTAL_IMAGE_PIXELS = 100000000;
|
|
12
|
+
export declare const DEFAULT_PDF_MAX_REMOTE_REQUESTS = 32;
|
|
13
|
+
export declare const DEFAULT_PDF_MAX_REMOTE_CONCURRENCY = 4;
|
|
14
|
+
export declare const DEFAULT_PDF_MAX_OUTPUT_BYTES: number;
|
|
15
|
+
/** Operator-facing `pdf.limits` module option. */
|
|
16
|
+
export interface PdfLimitsOptions {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
maxPages?: number;
|
|
19
|
+
maxNodes?: number;
|
|
20
|
+
maxTreeDepth?: number;
|
|
21
|
+
maxTextCharacters?: number;
|
|
22
|
+
maxImages?: number;
|
|
23
|
+
maxImageBytes?: number;
|
|
24
|
+
maxTotalImageBytes?: number;
|
|
25
|
+
maxImagePixels?: number;
|
|
26
|
+
maxTotalImagePixels?: number;
|
|
27
|
+
maxRemoteRequests?: number;
|
|
28
|
+
maxRemoteConcurrency?: number;
|
|
29
|
+
maxOutputBytes?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Fully resolved render limits threaded through the registry runtime options. */
|
|
32
|
+
export type PdfRenderLimits = Required<PdfLimitsOptions>;
|
|
33
|
+
export declare const DEFAULT_PDF_RENDER_LIMITS: Readonly<PdfRenderLimits>;
|
|
34
|
+
/** A monotonic checked deadline shared by the whole render. */
|
|
35
|
+
export interface RenderDeadline {
|
|
36
|
+
check(): void;
|
|
37
|
+
remainingMs(): number;
|
|
38
|
+
}
|
|
39
|
+
export declare const createRenderDeadline: (timeoutMs: number) => RenderDeadline;
|
|
40
|
+
export declare const enforceMaxPages: (pageCount: number, maxPages: number) => void;
|
|
41
|
+
/** Everything needed to enforce one render's resolved budgets. */
|
|
42
|
+
export interface RenderLimits extends PdfRenderLimits {
|
|
43
|
+
readonly abortController: AbortController;
|
|
44
|
+
readonly deadline: RenderDeadline;
|
|
45
|
+
}
|
|
46
|
+
export declare const createRenderLimits: (limits: PdfRenderLimits) => RenderLimits;
|
|
47
|
+
/** Fail before layout when the mounted canonical tree exceeds admission limits. */
|
|
48
|
+
export declare const enforceTreeLimits: (document: PdfDocumentNode, limits: RenderLimits) => void;
|
|
49
|
+
export declare const resolvePdfRenderLimits: (limits: PdfRenderLimits | undefined) => PdfRenderLimits;
|
|
50
|
+
/** Validate operator limits once and fill every omitted field from one default. */
|
|
51
|
+
export declare const normalizePdfLimits: (options: PdfLimitsOptions | undefined) => PdfRenderLimits | undefined;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { NuxtPdfError, PDF_ERROR_CODES } from "../shared/errors.js";
|
|
2
|
+
import { PDF_PRIMITIVES } from "../authoring.js";
|
|
3
|
+
export const DEFAULT_PDF_TIMEOUT_MS = 3e4;
|
|
4
|
+
export const DEFAULT_PDF_MAX_PAGES = 2e3;
|
|
5
|
+
export const DEFAULT_PDF_MAX_NODES = 5e4;
|
|
6
|
+
export const DEFAULT_PDF_MAX_TREE_DEPTH = 128;
|
|
7
|
+
export const DEFAULT_PDF_MAX_TEXT_CHARACTERS = 2e6;
|
|
8
|
+
export const DEFAULT_PDF_MAX_IMAGES = 256;
|
|
9
|
+
export const DEFAULT_PDF_MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
export const DEFAULT_PDF_MAX_TOTAL_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
11
|
+
export const DEFAULT_PDF_MAX_IMAGE_PIXELS = 25e6;
|
|
12
|
+
export const DEFAULT_PDF_MAX_TOTAL_IMAGE_PIXELS = 1e8;
|
|
13
|
+
export const DEFAULT_PDF_MAX_REMOTE_REQUESTS = 32;
|
|
14
|
+
export const DEFAULT_PDF_MAX_REMOTE_CONCURRENCY = 4;
|
|
15
|
+
export const DEFAULT_PDF_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
16
|
+
export const DEFAULT_PDF_RENDER_LIMITS = Object.freeze({
|
|
17
|
+
timeoutMs: DEFAULT_PDF_TIMEOUT_MS,
|
|
18
|
+
maxPages: DEFAULT_PDF_MAX_PAGES,
|
|
19
|
+
maxNodes: DEFAULT_PDF_MAX_NODES,
|
|
20
|
+
maxTreeDepth: DEFAULT_PDF_MAX_TREE_DEPTH,
|
|
21
|
+
maxTextCharacters: DEFAULT_PDF_MAX_TEXT_CHARACTERS,
|
|
22
|
+
maxImages: DEFAULT_PDF_MAX_IMAGES,
|
|
23
|
+
maxImageBytes: DEFAULT_PDF_MAX_IMAGE_BYTES,
|
|
24
|
+
maxTotalImageBytes: DEFAULT_PDF_MAX_TOTAL_IMAGE_BYTES,
|
|
25
|
+
maxImagePixels: DEFAULT_PDF_MAX_IMAGE_PIXELS,
|
|
26
|
+
maxTotalImagePixels: DEFAULT_PDF_MAX_TOTAL_IMAGE_PIXELS,
|
|
27
|
+
maxRemoteRequests: DEFAULT_PDF_MAX_REMOTE_REQUESTS,
|
|
28
|
+
maxRemoteConcurrency: DEFAULT_PDF_MAX_REMOTE_CONCURRENCY,
|
|
29
|
+
maxOutputBytes: DEFAULT_PDF_MAX_OUTPUT_BYTES
|
|
30
|
+
});
|
|
31
|
+
export const createRenderDeadline = (timeoutMs) => {
|
|
32
|
+
const start = performance.now();
|
|
33
|
+
const remainingMs = () => Math.max(0, timeoutMs - (performance.now() - start));
|
|
34
|
+
return {
|
|
35
|
+
check() {
|
|
36
|
+
const elapsed = performance.now() - start;
|
|
37
|
+
if (elapsed > timeoutMs) {
|
|
38
|
+
throw new NuxtPdfError(
|
|
39
|
+
PDF_ERROR_CODES.LimitExceeded,
|
|
40
|
+
`PDF render exceeded its ${timeoutMs}ms time budget (elapsed ${Math.round(elapsed)}ms). Raise pdf.limits.timeoutMs if this document is legitimately this slow. The budget is checked between engine stages and passes, not mid-step, so a single layout stage can overshoot it.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
remainingMs
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const limitExceeded = (message) => {
|
|
48
|
+
throw new NuxtPdfError(PDF_ERROR_CODES.LimitExceeded, message);
|
|
49
|
+
};
|
|
50
|
+
export const enforceMaxPages = (pageCount, maxPages) => {
|
|
51
|
+
if (pageCount > maxPages) {
|
|
52
|
+
limitExceeded(
|
|
53
|
+
`PDF laid out ${pageCount} pages, exceeding the ${maxPages}-page limit. Raise pdf.limits.maxPages if this document is legitimately this long.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const createRenderLimits = (limits) => ({
|
|
58
|
+
...limits,
|
|
59
|
+
abortController: new AbortController(),
|
|
60
|
+
deadline: createRenderDeadline(limits.timeoutMs)
|
|
61
|
+
});
|
|
62
|
+
const textLength = (value) => {
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const _character of value) count += 1;
|
|
65
|
+
return count;
|
|
66
|
+
};
|
|
67
|
+
export const enforceTreeLimits = (document, limits) => {
|
|
68
|
+
const pending = [{
|
|
69
|
+
depth: 1,
|
|
70
|
+
node: document
|
|
71
|
+
}];
|
|
72
|
+
const seen = /* @__PURE__ */ new Set();
|
|
73
|
+
let images = 0;
|
|
74
|
+
let nodes = 0;
|
|
75
|
+
let textCharacters = 0;
|
|
76
|
+
while (pending.length > 0) {
|
|
77
|
+
const { depth, node } = pending.pop();
|
|
78
|
+
nodes += 1;
|
|
79
|
+
if (nodes > limits.maxNodes) {
|
|
80
|
+
limitExceeded(
|
|
81
|
+
`PDF mounted more than ${limits.maxNodes} nodes. Raise pdf.limits.maxNodes only for a legitimately larger document.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (depth > limits.maxTreeDepth) {
|
|
85
|
+
limitExceeded(
|
|
86
|
+
`PDF tree depth exceeded ${limits.maxTreeDepth}. Raise pdf.limits.maxTreeDepth only for a legitimately deeper document.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (node.type === PDF_PRIMITIVES.TextInstance) {
|
|
90
|
+
textCharacters += textLength(node.value);
|
|
91
|
+
if (textCharacters > limits.maxTextCharacters) {
|
|
92
|
+
limitExceeded(
|
|
93
|
+
`PDF text exceeded ${limits.maxTextCharacters} characters. Raise pdf.limits.maxTextCharacters only for legitimate content.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (seen.has(node)) {
|
|
99
|
+
throw new NuxtPdfError(
|
|
100
|
+
PDF_ERROR_CODES.TreeInvalid,
|
|
101
|
+
"PDF tree contains a repeated or circular element node."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
seen.add(node);
|
|
105
|
+
if (node.type === PDF_PRIMITIVES.Image) {
|
|
106
|
+
images += 1;
|
|
107
|
+
if (images > limits.maxImages) {
|
|
108
|
+
limitExceeded(
|
|
109
|
+
`PDF mounted more than ${limits.maxImages} images. Raise pdf.limits.maxImages only for a legitimate document.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) {
|
|
114
|
+
const child = node.children[index];
|
|
115
|
+
if (child) pending.push({ depth: depth + 1, node: child });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
limits.deadline.check();
|
|
119
|
+
};
|
|
120
|
+
export const resolvePdfRenderLimits = (limits) => ({
|
|
121
|
+
...DEFAULT_PDF_RENDER_LIMITS,
|
|
122
|
+
...limits
|
|
123
|
+
});
|
|
124
|
+
const validatePositiveSafeInteger = (value, key) => {
|
|
125
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
126
|
+
throw new TypeError(`pdf.limits.${key} must be a positive safe integer.`);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
};
|
|
130
|
+
export const normalizePdfLimits = (options) => {
|
|
131
|
+
if (options === void 0) return void 0;
|
|
132
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
133
|
+
throw new TypeError("pdf.limits must be an object.");
|
|
134
|
+
}
|
|
135
|
+
const result = { ...DEFAULT_PDF_RENDER_LIMITS };
|
|
136
|
+
const unknownKey = Object.keys(options).find((key) => !(key in result));
|
|
137
|
+
if (unknownKey) throw new TypeError(`pdf.limits.${unknownKey} is not supported.`);
|
|
138
|
+
for (const key of Object.keys(result)) {
|
|
139
|
+
const value = options[key];
|
|
140
|
+
if (value !== void 0) result[key] = validatePositiveSafeInteger(value, key);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PdfDisposition, PdfRenderDiagnostics, PdfRenderResult, ResolvedPdfMetadata } from '../shared/template.js';
|
|
2
|
+
export declare const sanitizePdfFilename: (value: string) => string;
|
|
3
|
+
export declare const createContentDisposition: (disposition: PdfDisposition, filename: string) => string;
|
|
4
|
+
type CompletedRenderDiagnostics = Omit<PdfRenderDiagnostics, 'byteLength'>;
|
|
5
|
+
export declare const createPdfRenderResult: (source: Uint8Array, metadata: ResolvedPdfMetadata, diagnostics: CompletedRenderDiagnostics) => PdfRenderResult;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
const DEFAULT_BASENAME = "document";
|
|
3
|
+
const DEFAULT_FILENAME = `${DEFAULT_BASENAME}.pdf`;
|
|
4
|
+
const PDF_EXTENSION = ".pdf";
|
|
5
|
+
const MAX_FILENAME_CODE_POINTS = 180;
|
|
6
|
+
const MAX_ENCODED_FILENAME_LENGTH = 600;
|
|
7
|
+
const MAX_ASCII_FILENAME_LENGTH = 180;
|
|
8
|
+
const UNSAFE_FILENAME_CHARACTERS = /[\p{Cc}"\\/:*?<>|\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu;
|
|
9
|
+
const EDGE_DOTS_OR_SPACES = /^[.\s]+|[.\s]+$/g;
|
|
10
|
+
const encodeFilename = (filename) => encodeURIComponent(filename).replace(
|
|
11
|
+
/[!'()*]/g,
|
|
12
|
+
(character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`
|
|
13
|
+
);
|
|
14
|
+
const truncateFilenameBase = (base) => {
|
|
15
|
+
const maxBaseCodePoints = MAX_FILENAME_CODE_POINTS - PDF_EXTENSION.length;
|
|
16
|
+
let encodedLength = encodeFilename(PDF_EXTENSION).length;
|
|
17
|
+
let points = 0;
|
|
18
|
+
let result = "";
|
|
19
|
+
for (const character of base) {
|
|
20
|
+
const characterLength = encodeFilename(character).length;
|
|
21
|
+
if (points >= maxBaseCodePoints || encodedLength + characterLength > MAX_ENCODED_FILENAME_LENGTH) break;
|
|
22
|
+
result += character;
|
|
23
|
+
encodedLength += characterLength;
|
|
24
|
+
points += 1;
|
|
25
|
+
}
|
|
26
|
+
return result.replace(EDGE_DOTS_OR_SPACES, "") || DEFAULT_BASENAME;
|
|
27
|
+
};
|
|
28
|
+
export const sanitizePdfFilename = (value) => {
|
|
29
|
+
const normalized = value.toWellFormed().normalize("NFKC").replace(UNSAFE_FILENAME_CHARACTERS, "_").replace(/\s+/g, " ").replace(EDGE_DOTS_OR_SPACES, "");
|
|
30
|
+
const base = normalized.toLowerCase().endsWith(PDF_EXTENSION) ? normalized.slice(0, -PDF_EXTENSION.length) : normalized;
|
|
31
|
+
return `${truncateFilenameBase(base)}${PDF_EXTENSION}`;
|
|
32
|
+
};
|
|
33
|
+
const asciiFilename = (filename) => {
|
|
34
|
+
const base = filename.slice(0, -PDF_EXTENSION.length).normalize("NFKD").replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "_").replace(EDGE_DOTS_OR_SPACES, "").slice(0, MAX_ASCII_FILENAME_LENGTH - PDF_EXTENSION.length).replace(EDGE_DOTS_OR_SPACES, "");
|
|
35
|
+
return `${base || DEFAULT_BASENAME}${PDF_EXTENSION}`;
|
|
36
|
+
};
|
|
37
|
+
export const createContentDisposition = (disposition, filename) => {
|
|
38
|
+
const safeFilename = sanitizePdfFilename(filename);
|
|
39
|
+
return `${disposition}; filename="${asciiFilename(safeFilename)}"; filename*=UTF-8''${encodeFilename(safeFilename)}`;
|
|
40
|
+
};
|
|
41
|
+
export const createPdfRenderResult = (source, metadata, diagnostics) => {
|
|
42
|
+
const bytes = new Uint8Array(source);
|
|
43
|
+
const completedDiagnostics = Object.freeze({
|
|
44
|
+
byteLength: bytes.byteLength,
|
|
45
|
+
durationMs: diagnostics.durationMs,
|
|
46
|
+
pageCount: diagnostics.pageCount,
|
|
47
|
+
passes: diagnostics.passes,
|
|
48
|
+
registeredFontFaces: Object.freeze(diagnostics.registeredFontFaces.map((face) => Object.freeze({ ...face })))
|
|
49
|
+
});
|
|
50
|
+
const completedMetadata = Object.freeze({
|
|
51
|
+
filename: metadata.filename,
|
|
52
|
+
language: metadata.language,
|
|
53
|
+
title: metadata.title
|
|
54
|
+
});
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
metadata: completedMetadata,
|
|
57
|
+
diagnostics: completedDiagnostics,
|
|
58
|
+
async toUint8Array() {
|
|
59
|
+
return new Uint8Array(bytes);
|
|
60
|
+
},
|
|
61
|
+
async toBuffer() {
|
|
62
|
+
return Buffer.from(bytes);
|
|
63
|
+
},
|
|
64
|
+
async response(init = {}) {
|
|
65
|
+
const {
|
|
66
|
+
disposition = "attachment",
|
|
67
|
+
filename = completedMetadata.filename ?? DEFAULT_FILENAME,
|
|
68
|
+
headers: initialHeaders,
|
|
69
|
+
...responseInit
|
|
70
|
+
} = init;
|
|
71
|
+
const headers = new Headers(initialHeaders);
|
|
72
|
+
headers.set("content-type", "application/pdf");
|
|
73
|
+
headers.set("content-length", String(bytes.byteLength));
|
|
74
|
+
headers.set(
|
|
75
|
+
"content-disposition",
|
|
76
|
+
createContentDisposition(disposition, filename)
|
|
77
|
+
);
|
|
78
|
+
return new Response(new Uint8Array(bytes), { ...responseInit, headers });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const PDF_ERROR_CODES: {
|
|
2
|
+
readonly AssetBlocked: "PDF_ASSET_BLOCKED";
|
|
3
|
+
readonly AssetInvalid: "PDF_ASSET_INVALID";
|
|
4
|
+
readonly LayoutError: "PDF_LAYOUT_ERROR";
|
|
5
|
+
readonly LimitExceeded: "PDF_LIMIT_EXCEEDED";
|
|
6
|
+
readonly TreeInvalid: "PDF_TREE_INVALID";
|
|
7
|
+
readonly TemplateNotFound: "PDF_TEMPLATE_NOT_FOUND";
|
|
8
|
+
readonly TemplateInvalid: "PDF_TEMPLATE_INVALID";
|
|
9
|
+
readonly RenderError: "PDF_RENDER_ERROR";
|
|
10
|
+
};
|
|
11
|
+
export type PdfErrorCode = (typeof PDF_ERROR_CODES)[keyof typeof PDF_ERROR_CODES];
|
|
12
|
+
export interface NuxtPdfErrorOptions {
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
templateKey?: string;
|
|
15
|
+
templateFile?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class NuxtPdfError extends Error {
|
|
18
|
+
readonly code: PdfErrorCode;
|
|
19
|
+
readonly templateKey?: string;
|
|
20
|
+
readonly templateFile?: string;
|
|
21
|
+
constructor(code: PdfErrorCode, message: string, options?: NuxtPdfErrorOptions);
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const PDF_ERROR_CODES = {
|
|
2
|
+
AssetBlocked: "PDF_ASSET_BLOCKED",
|
|
3
|
+
AssetInvalid: "PDF_ASSET_INVALID",
|
|
4
|
+
LayoutError: "PDF_LAYOUT_ERROR",
|
|
5
|
+
LimitExceeded: "PDF_LIMIT_EXCEEDED",
|
|
6
|
+
TreeInvalid: "PDF_TREE_INVALID",
|
|
7
|
+
TemplateNotFound: "PDF_TEMPLATE_NOT_FOUND",
|
|
8
|
+
TemplateInvalid: "PDF_TEMPLATE_INVALID",
|
|
9
|
+
RenderError: "PDF_RENDER_ERROR"
|
|
10
|
+
};
|
|
11
|
+
export class NuxtPdfError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
templateKey;
|
|
14
|
+
templateFile;
|
|
15
|
+
constructor(code, message, options = {}) {
|
|
16
|
+
super(message, { cause: options.cause });
|
|
17
|
+
this.name = "NuxtPdfError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.templateKey = options.templateKey;
|
|
20
|
+
this.templateFile = options.templateFile;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { NuxtPdfError, PDF_ERROR_CODES, } from './errors.js';
|
|
2
|
+
export type { NuxtPdfErrorOptions, PdfErrorCode, } from './errors.js';
|
|
3
|
+
export { PDF_DEFINITION_PROPERTY, } from './template.js';
|
|
4
|
+
export type { PdfComponent, PdfComponentProps, PdfDefinition, PdfDisposition, PdfMetadataValue, PdfRenderDiagnostics, PdfRenderResult, PdfResponseInit, PdfTemplate, ResolvedPdfMetadata, } from './template.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AllowedComponentProps, Component, ComponentCustomProps, FunctionalComponent, VNodeProps } from 'vue';
|
|
2
|
+
export declare const PDF_DEFINITION_PROPERTY: "__nuxtPdf";
|
|
3
|
+
export type PdfMetadataValue<Props extends object> = string | ((props: Readonly<Props>) => string);
|
|
4
|
+
export interface PdfDefinition<Props extends object = Record<string, unknown>> {
|
|
5
|
+
title?: PdfMetadataValue<Props>;
|
|
6
|
+
filename?: PdfMetadataValue<Props>;
|
|
7
|
+
language?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Maximum number of layout passes the multi-pass (table-of-contents) loop runs
|
|
10
|
+
* before it declares the page numbers non-convergent and throws
|
|
11
|
+
* `PDF_LIMIT_EXCEEDED`. A positive integer; defaults to 5. Only relevant to
|
|
12
|
+
* documents that read `usePdfPageNumbers()` — internal `#id` links alone
|
|
13
|
+
* resolve in a single pass and never enter the loop.
|
|
14
|
+
*/
|
|
15
|
+
maxPasses?: number;
|
|
16
|
+
sampleData?: Props;
|
|
17
|
+
scenarios?: Readonly<Record<string, Props>>;
|
|
18
|
+
}
|
|
19
|
+
export interface ResolvedPdfMetadata {
|
|
20
|
+
readonly title?: string;
|
|
21
|
+
readonly filename?: string;
|
|
22
|
+
readonly language?: string;
|
|
23
|
+
}
|
|
24
|
+
export type PdfComponent<Props extends object = Record<string, unknown>> = Component & {
|
|
25
|
+
readonly [PDF_DEFINITION_PROPERTY]?: PdfDefinition<Props>;
|
|
26
|
+
};
|
|
27
|
+
type VueFrameworkProps = VNodeProps & AllowedComponentProps & ComponentCustomProps;
|
|
28
|
+
/** Props authored by a component, excluding Vue's renderer-level vnode props. */
|
|
29
|
+
export type PdfComponentProps<C extends Component> = C extends abstract new (...args: never[]) => {
|
|
30
|
+
$props: infer Props extends object;
|
|
31
|
+
} ? Omit<Props, keyof VueFrameworkProps> : C extends FunctionalComponent<infer Props extends object> ? Props : Record<string, unknown>;
|
|
32
|
+
export type PdfDisposition = 'attachment' | 'inline';
|
|
33
|
+
export interface PdfResponseInit extends Omit<ResponseInit, 'headers'> {
|
|
34
|
+
disposition?: PdfDisposition;
|
|
35
|
+
filename?: string;
|
|
36
|
+
headers?: HeadersInit;
|
|
37
|
+
}
|
|
38
|
+
/** Safe, content-free measurements from one completed PDF render. */
|
|
39
|
+
export interface PdfRenderDiagnostics {
|
|
40
|
+
readonly durationMs: number;
|
|
41
|
+
readonly byteLength: number;
|
|
42
|
+
readonly pageCount: number;
|
|
43
|
+
readonly passes: number;
|
|
44
|
+
readonly registeredFontFaces: readonly PdfRegisteredFontFace[];
|
|
45
|
+
}
|
|
46
|
+
export interface PdfRegisteredFontFace {
|
|
47
|
+
readonly family: string;
|
|
48
|
+
readonly fontStyle?: string;
|
|
49
|
+
readonly fontWeight?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface PdfRenderResult {
|
|
52
|
+
readonly metadata: Readonly<ResolvedPdfMetadata>;
|
|
53
|
+
readonly diagnostics: PdfRenderDiagnostics;
|
|
54
|
+
toUint8Array(): Promise<Uint8Array>;
|
|
55
|
+
toBuffer(): Promise<Buffer>;
|
|
56
|
+
response(init?: PdfResponseInit): Promise<Response>;
|
|
57
|
+
}
|
|
58
|
+
export interface PdfTemplate<Props extends object = Record<string, unknown>> {
|
|
59
|
+
readonly key: string;
|
|
60
|
+
resolveMetadata(props: Props): ResolvedPdfMetadata;
|
|
61
|
+
render(props: Props): Promise<PdfRenderResult>;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PDF_DEFINITION_PROPERTY = "__nuxtPdf";
|