modern-pdf-lib 0.38.0 → 0.40.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/README.md +3 -3
- package/dist/browser.cjs +23 -3
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.mts +2 -2
- package/dist/browser.mjs +2 -2
- package/dist/{index-CNtfRlyP.d.cts → index-CHhHRD6q.d.cts} +804 -5
- package/dist/index-CHhHRD6q.d.cts.map +1 -0
- package/dist/{index-CDg5gwYg.d.mts → index-CzmWmPxV.d.mts} +804 -5
- package/dist/index-CzmWmPxV.d.mts.map +1 -0
- package/dist/index.cjs +23 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-B0gmgVEs.cjs → src-aDYhSnal.cjs} +2224 -254
- package/dist/{src-DlRTrvWg.mjs → src-wZnfMPwi.mjs} +1867 -17
- package/package.json +1 -1
- package/dist/index-CDg5gwYg.d.mts.map +0 -1
- package/dist/index-CNtfRlyP.d.cts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $r as beginText, A as encodeUTCTime, C as encodeInteger, Dn as formatPdfDate, E as encodeOctetString, F as parseDerTlv, Gr as rectangle, Gt as PageSizes, I as toBuffer, Kn as saveState, Kr as setDashPattern, M as extractIssuerAndSerial, N as extractSubjectPublicKeyInfo, O as encodeSequence, P as getSubtle, Qn as applyStrokeColor, Qr as stroke, S as encodeContextTag, T as encodeOID, Tn as buildInfoDict, Un as restoreState, Ur as lineTo, Ut as serializePdf, V as findSignatures, W as mergePdfs, Wr as moveTo, Xr as setLineWidth, Zn as applyFillColor, ai as setFont, b as detectKeyAlgorithm, bt as PdfLexer, di as setWordSpacing, ei as endText, er as cmykToRgb, fi as showText$1, k as encodeSet, n as StandardFonts, nt as loadPdf, r as createPdf, rt as PdfEncryptionHandler, s as setPageLabels, st as sha256, t as PdfDocument, ti as moveText, vt as XrefParser, x as detectNamedCurve, xn as buildAnnotationDict, xt as PdfParseError, y as decodeOidBytes, yn as PdfAnnotation, yt as PdfObjectParser, z as computeSignatureHash, zr as fill } from "./pdfDocument-Q0wrwKEZ.mjs";
|
|
1
|
+
import { $r as beginText, A as encodeUTCTime, C as encodeInteger, Dn as formatPdfDate, E as encodeOctetString, F as parseDerTlv, Gr as rectangle, Gt as PageSizes, I as toBuffer, Kn as saveState, Kr as setDashPattern, M as extractIssuerAndSerial, N as extractSubjectPublicKeyInfo, O as encodeSequence, P as getSubtle, Qn as applyStrokeColor, Qr as stroke, S as encodeContextTag, T as encodeOID, Tn as buildInfoDict, Un as restoreState, Ur as lineTo, Ut as serializePdf, V as findSignatures, W as mergePdfs, Wr as moveTo, Xr as setLineWidth, Zn as applyFillColor, ai as setFont, b as detectKeyAlgorithm, bt as PdfLexer, cr as rgb, di as setWordSpacing, ei as endText, er as cmykToRgb, fi as showText$1, k as encodeSet, n as StandardFonts, nt as loadPdf, r as createPdf, rt as PdfEncryptionHandler, s as setPageLabels, st as sha256, t as PdfDocument, ti as moveText, vt as XrefParser, x as detectNamedCurve, xn as buildAnnotationDict, xt as PdfParseError, y as decodeOidBytes, yn as PdfAnnotation, yt as PdfObjectParser, z as computeSignatureHash, zr as fill } from "./pdfDocument-Q0wrwKEZ.mjs";
|
|
2
2
|
import { c as PdfRef, i as PdfName, l as PdfStream, n as PdfBool, o as PdfNumber, r as PdfDict, s as PdfObjectRegistry, t as PdfArray, u as PdfString } from "./pdfObjects-CB7TEdbW.mjs";
|
|
3
3
|
import { n as decodeStreamData, r as getStreamFilters } from "./streamDecode-DCpgTocN.mjs";
|
|
4
4
|
import { g as FieldFlags, v as numVal } from "./pdfForm-CfLnlsj-.mjs";
|
|
@@ -13223,7 +13223,7 @@ const DEFAULT_HEIGHT = 50;
|
|
|
13223
13223
|
const DEFAULT_MODULE_WIDTH = 1;
|
|
13224
13224
|
const DEFAULT_QUIET_ZONE = 10;
|
|
13225
13225
|
const DEFAULT_FONT_NAME = "Helvetica";
|
|
13226
|
-
const DEFAULT_FONT_SIZE$
|
|
13226
|
+
const DEFAULT_FONT_SIZE$3 = 10;
|
|
13227
13227
|
const DEFAULT_BORDER_WIDTH = .5;
|
|
13228
13228
|
const DEFAULT_PADDING = 4;
|
|
13229
13229
|
const DEFAULT_BAR_COLOR = {
|
|
@@ -13254,7 +13254,7 @@ function calculateBarcodeDimensions(matrix, options) {
|
|
|
13254
13254
|
const height = options?.height ?? DEFAULT_HEIGHT;
|
|
13255
13255
|
const padding = options?.padding ?? (options?.border ? DEFAULT_PADDING : 0);
|
|
13256
13256
|
const borderW = options?.border ? options.borderWidth ?? DEFAULT_BORDER_WIDTH : 0;
|
|
13257
|
-
const textHeight = options?.showText ? (options.fontSize ?? DEFAULT_FONT_SIZE$
|
|
13257
|
+
const textHeight = options?.showText ? (options.fontSize ?? DEFAULT_FONT_SIZE$3) + 4 : 0;
|
|
13258
13258
|
return {
|
|
13259
13259
|
width: (matrix.width + quietZone * 2) * moduleWidth + padding * 2 + borderW * 2,
|
|
13260
13260
|
height: height + textHeight + padding * 2 + borderW * 2
|
|
@@ -13284,7 +13284,7 @@ function renderStyledBarcode(matrix, x, y, text, options) {
|
|
|
13284
13284
|
const bgColor = options?.backgroundColor ?? DEFAULT_BG_COLOR;
|
|
13285
13285
|
const showTextBelow = options?.showText ?? false;
|
|
13286
13286
|
const fontName = options?.fontName ?? DEFAULT_FONT_NAME;
|
|
13287
|
-
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE$
|
|
13287
|
+
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE$3;
|
|
13288
13288
|
const textColor = options?.textColor ?? barColor;
|
|
13289
13289
|
const hasBorder = options?.border ?? false;
|
|
13290
13290
|
const borderWidth = options?.borderWidth ?? DEFAULT_BORDER_WIDTH;
|
|
@@ -15530,7 +15530,7 @@ const DEFAULT_MARGINS = {
|
|
|
15530
15530
|
left: 50,
|
|
15531
15531
|
right: 50
|
|
15532
15532
|
};
|
|
15533
|
-
const DEFAULT_FONT_SIZE$
|
|
15533
|
+
const DEFAULT_FONT_SIZE$2 = 10;
|
|
15534
15534
|
const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
|
|
15535
15535
|
/** Convert an integer to a lowercase Roman numeral string. */
|
|
15536
15536
|
function toRoman(num) {
|
|
@@ -15618,7 +15618,7 @@ function applyHeaderFooterToPage(page, options, pageNumber, totalPages, title) {
|
|
|
15618
15618
|
if (options.header && options.header.length > 0) {
|
|
15619
15619
|
const headerY = pageHeight - margins.top;
|
|
15620
15620
|
for (const item of options.header) {
|
|
15621
|
-
const fontSize = item.fontSize ?? DEFAULT_FONT_SIZE$
|
|
15621
|
+
const fontSize = item.fontSize ?? DEFAULT_FONT_SIZE$2;
|
|
15622
15622
|
const resolvedText = replaceTemplateVariables(item.text, pageNumber, totalPages, now, dateFormat, effectiveTitle);
|
|
15623
15623
|
const textWidth = estimateWidth(resolvedText, item.font, fontSize);
|
|
15624
15624
|
let x;
|
|
@@ -15634,7 +15634,7 @@ function applyHeaderFooterToPage(page, options, pageNumber, totalPages, title) {
|
|
|
15634
15634
|
});
|
|
15635
15635
|
}
|
|
15636
15636
|
if (options.separatorLine) {
|
|
15637
|
-
const lineY = headerY - (options.header[0]?.fontSize ?? DEFAULT_FONT_SIZE$
|
|
15637
|
+
const lineY = headerY - (options.header[0]?.fontSize ?? DEFAULT_FONT_SIZE$2) - 4;
|
|
15638
15638
|
page.drawLine({
|
|
15639
15639
|
start: {
|
|
15640
15640
|
x: margins.left,
|
|
@@ -15653,7 +15653,7 @@ function applyHeaderFooterToPage(page, options, pageNumber, totalPages, title) {
|
|
|
15653
15653
|
if (options.footer && options.footer.length > 0) {
|
|
15654
15654
|
const footerY = margins.bottom;
|
|
15655
15655
|
for (const item of options.footer) {
|
|
15656
|
-
const fontSize = item.fontSize ?? DEFAULT_FONT_SIZE$
|
|
15656
|
+
const fontSize = item.fontSize ?? DEFAULT_FONT_SIZE$2;
|
|
15657
15657
|
const resolvedText = replaceTemplateVariables(item.text, pageNumber, totalPages, now, dateFormat, effectiveTitle);
|
|
15658
15658
|
const textWidth = estimateWidth(resolvedText, item.font, fontSize);
|
|
15659
15659
|
let x;
|
|
@@ -15669,7 +15669,7 @@ function applyHeaderFooterToPage(page, options, pageNumber, totalPages, title) {
|
|
|
15669
15669
|
});
|
|
15670
15670
|
}
|
|
15671
15671
|
if (options.separatorLine) {
|
|
15672
|
-
const lineY = footerY + (options.footer[0]?.fontSize ?? DEFAULT_FONT_SIZE$
|
|
15672
|
+
const lineY = footerY + (options.footer[0]?.fontSize ?? DEFAULT_FONT_SIZE$2) + 4;
|
|
15673
15673
|
page.drawLine({
|
|
15674
15674
|
start: {
|
|
15675
15675
|
x: margins.left,
|
|
@@ -21869,7 +21869,7 @@ function createRangeFetcher(url, options) {
|
|
|
21869
21869
|
* @param children Child nodes for `document` / `page` containers.
|
|
21870
21870
|
* @returns A frozen-shaped {@link VNode}.
|
|
21871
21871
|
*/
|
|
21872
|
-
function h(type, props, ...children) {
|
|
21872
|
+
function h$1(type, props, ...children) {
|
|
21873
21873
|
switch (type) {
|
|
21874
21874
|
case "document": return {
|
|
21875
21875
|
type: "document",
|
|
@@ -21905,7 +21905,7 @@ function asText(value) {
|
|
|
21905
21905
|
return typeof value === "string" ? value : "";
|
|
21906
21906
|
}
|
|
21907
21907
|
/** Internal layout constants. */
|
|
21908
|
-
const DEFAULT_FONT_SIZE = 12;
|
|
21908
|
+
const DEFAULT_FONT_SIZE$1 = 12;
|
|
21909
21909
|
const DEFAULT_MARGIN = 50;
|
|
21910
21910
|
const LINE_SPACING = 1.35;
|
|
21911
21911
|
const HEADING_SCALE = [
|
|
@@ -21927,13 +21927,13 @@ const HEADING_SCALE = [
|
|
|
21927
21927
|
* @param options Optional layout overrides.
|
|
21928
21928
|
* @returns A promise resolving to the saved PDF bytes (starting `%PDF-`).
|
|
21929
21929
|
*/
|
|
21930
|
-
async function renderToPdf(root, options) {
|
|
21931
|
-
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
21930
|
+
async function renderToPdf$1(root, options) {
|
|
21931
|
+
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE$1;
|
|
21932
21932
|
const margin = options?.margin ?? DEFAULT_MARGIN;
|
|
21933
21933
|
const doc = createPdf();
|
|
21934
21934
|
const body = await doc.embedFont(StandardFonts.Helvetica);
|
|
21935
21935
|
const bold = await doc.embedFont(StandardFonts.HelveticaBold);
|
|
21936
|
-
for (const pageNode of collectPages(root)) renderPage(doc, body, bold, pageNode, fontSize, margin);
|
|
21936
|
+
for (const pageNode of collectPages$1(root)) renderPage(doc, body, bold, pageNode, fontSize, margin);
|
|
21937
21937
|
return doc.save();
|
|
21938
21938
|
}
|
|
21939
21939
|
/**
|
|
@@ -21943,7 +21943,7 @@ async function renderToPdf(root, options) {
|
|
|
21943
21943
|
* are gathered into a synthesized `page`. A non-`document` root is
|
|
21944
21944
|
* itself treated as a page (wrapping a leaf if necessary).
|
|
21945
21945
|
*/
|
|
21946
|
-
function collectPages(root) {
|
|
21946
|
+
function collectPages$1(root) {
|
|
21947
21947
|
if (root.type === "document") {
|
|
21948
21948
|
const pages = [];
|
|
21949
21949
|
const loose = [];
|
|
@@ -32167,6 +32167,1856 @@ function feBlend(a, b, mode) {
|
|
|
32167
32167
|
return out;
|
|
32168
32168
|
}
|
|
32169
32169
|
//#endregion
|
|
32170
|
+
//#region src/runtime/sharedConcurrency.ts
|
|
32171
|
+
/**
|
|
32172
|
+
* @module runtime/sharedConcurrency
|
|
32173
|
+
*
|
|
32174
|
+
* `SharedArrayBuffer` + `Atomics` concurrency primitives for coordinating
|
|
32175
|
+
* work across Web Workers / `worker_threads`. These are thin, correctness
|
|
32176
|
+
* focused wrappers around the standard atomic operations — they add no
|
|
32177
|
+
* acceleration of their own and make no use of SIMD or any other hardware
|
|
32178
|
+
* feature. They simply expose race-free counters, flags, and a byte ring
|
|
32179
|
+
* so multiple agents can share state through a single `SharedArrayBuffer`.
|
|
32180
|
+
*
|
|
32181
|
+
* Availability caveats (feature-detected, never assumed):
|
|
32182
|
+
*
|
|
32183
|
+
* - `SharedArrayBuffer` and `Atomics` must both exist. In browsers, shared
|
|
32184
|
+
* memory additionally requires the page to be *cross-origin isolated*
|
|
32185
|
+
* (COOP+COEP headers); we treat `crossOriginIsolated === false` as
|
|
32186
|
+
* "unavailable". See {@link isSharedMemoryAvailable}.
|
|
32187
|
+
* - `Atomics.wait` is only permitted on a non-main agent (it throws on the
|
|
32188
|
+
* main browser thread and would deadlock the event loop everywhere). The
|
|
32189
|
+
* {@link SharedFlag} `wait` method feature-detects this and degrades to a
|
|
32190
|
+
* non-blocking poll rather than throwing — see its docs.
|
|
32191
|
+
*
|
|
32192
|
+
* All references below are to the ECMA-262 `Atomics` semantics and MDN:
|
|
32193
|
+
*
|
|
32194
|
+
* - `Atomics.add(ta, i, v)` atomically adds `v` and returns the **previous**
|
|
32195
|
+
* value at index `i` (the value before the addition).
|
|
32196
|
+
* - `Atomics.compareExchange(ta, i, expected, replacement)` writes
|
|
32197
|
+
* `replacement` iff the current value equals `expected`, and returns the
|
|
32198
|
+
* value that was at index `i` **before** the call (so success is detected
|
|
32199
|
+
* by `result === expected`).
|
|
32200
|
+
* - `Atomics.notify(ta, i, count)` wakes up to `count` agents blocked in
|
|
32201
|
+
* `Atomics.wait` on index `i` and returns the number actually woken (0 if
|
|
32202
|
+
* none are waiting). It is safe to call from any thread, including the
|
|
32203
|
+
* main thread.
|
|
32204
|
+
* - `Atomics.wait(ta, i, value, timeout)` blocks while the slot equals
|
|
32205
|
+
* `value`, returning `'ok'`, `'timed-out'`, or `'not-equal'`.
|
|
32206
|
+
*/
|
|
32207
|
+
/**
|
|
32208
|
+
* Bytes per `Int32Array` element. Atomics operate on 32-bit integer views.
|
|
32209
|
+
*/
|
|
32210
|
+
const INT32_BYTES = 4;
|
|
32211
|
+
/**
|
|
32212
|
+
* Reference to the global `SharedArrayBuffer` constructor, or `undefined`
|
|
32213
|
+
* when the runtime does not provide one. Captured via `globalThis` so the
|
|
32214
|
+
* module loads without a `ReferenceError` on platforms that omit it.
|
|
32215
|
+
*/
|
|
32216
|
+
const SharedArrayBufferCtor = globalThis.SharedArrayBuffer;
|
|
32217
|
+
/**
|
|
32218
|
+
* Allocate a fresh zero-initialised `SharedArrayBuffer` of `byteLength`
|
|
32219
|
+
* bytes, throwing a clear error if shared memory is not available.
|
|
32220
|
+
*/
|
|
32221
|
+
function allocShared(byteLength) {
|
|
32222
|
+
if (SharedArrayBufferCtor === void 0) throw new Error("sharedConcurrency: SharedArrayBuffer is not available in this runtime. Call isSharedMemoryAvailable() before constructing these primitives.");
|
|
32223
|
+
return new SharedArrayBufferCtor(byteLength);
|
|
32224
|
+
}
|
|
32225
|
+
/**
|
|
32226
|
+
* Report whether shared-memory concurrency is usable in this runtime.
|
|
32227
|
+
*
|
|
32228
|
+
* Returns `true` only when **both** `SharedArrayBuffer` and `Atomics`
|
|
32229
|
+
* exist, and — in a browser context that exposes `crossOriginIsolated` —
|
|
32230
|
+
* that flag is not `false`. The check is fully defensive: it performs only
|
|
32231
|
+
* `typeof` probes and never throws, so it is safe to call as a guard before
|
|
32232
|
+
* touching any other export here.
|
|
32233
|
+
*
|
|
32234
|
+
* Note: `crossOriginIsolated` is only consulted when it is a boolean. On
|
|
32235
|
+
* Node / Deno / Bun / Workers (where it is typically `undefined`) we do not
|
|
32236
|
+
* treat its absence as a failure, since those runtimes allow shared memory
|
|
32237
|
+
* without the browser's COOP/COEP isolation requirement.
|
|
32238
|
+
*
|
|
32239
|
+
* @returns `true` iff shared-memory primitives can be constructed and used.
|
|
32240
|
+
*/
|
|
32241
|
+
function isSharedMemoryAvailable() {
|
|
32242
|
+
try {
|
|
32243
|
+
if (typeof SharedArrayBuffer === "undefined") return false;
|
|
32244
|
+
if (typeof Atomics === "undefined") return false;
|
|
32245
|
+
if (globalThis.crossOriginIsolated === false) return false;
|
|
32246
|
+
return true;
|
|
32247
|
+
} catch {
|
|
32248
|
+
return false;
|
|
32249
|
+
}
|
|
32250
|
+
}
|
|
32251
|
+
/**
|
|
32252
|
+
* An atomic 32-bit integer counter backed by a single slot of an
|
|
32253
|
+
* `Int32Array` view over a `SharedArrayBuffer`. Multiple `SharedCounter`
|
|
32254
|
+
* instances constructed over the **same** buffer and index observe each
|
|
32255
|
+
* other's writes, so the counter can be shared across workers by passing
|
|
32256
|
+
* its {@link SharedCounter.buffer} through `postMessage`.
|
|
32257
|
+
*
|
|
32258
|
+
* All mutations use `Atomics`, so increments from concurrent agents never
|
|
32259
|
+
* lose updates.
|
|
32260
|
+
*/
|
|
32261
|
+
var SharedCounter = class {
|
|
32262
|
+
/** Int32 view over the backing buffer. */
|
|
32263
|
+
#view;
|
|
32264
|
+
/** Element index of this counter's slot within {@link #view}. */
|
|
32265
|
+
#index;
|
|
32266
|
+
/** The `SharedArrayBuffer` backing this counter. */
|
|
32267
|
+
buffer;
|
|
32268
|
+
/**
|
|
32269
|
+
* @param buffer - Existing shared buffer to attach to. When omitted, a
|
|
32270
|
+
* fresh single-slot (`4`-byte) `SharedArrayBuffer` is
|
|
32271
|
+
* allocated and zero-initialised.
|
|
32272
|
+
* @param index - Element index (in `Int32` units) of the counter slot.
|
|
32273
|
+
* Defaults to `0`. Must be a non-negative integer that
|
|
32274
|
+
* fits within `buffer`.
|
|
32275
|
+
* @throws If shared memory is unavailable (when allocating), or if
|
|
32276
|
+
* `index` is out of range for the supplied `buffer`.
|
|
32277
|
+
*/
|
|
32278
|
+
constructor(buffer, index) {
|
|
32279
|
+
const slot = index ?? 0;
|
|
32280
|
+
if (!Number.isInteger(slot) || slot < 0) throw new Error(`SharedCounter: index must be a non-negative integer, received ${slot}`);
|
|
32281
|
+
const buf = buffer ?? allocShared(INT32_BYTES);
|
|
32282
|
+
if ((slot + 1) * INT32_BYTES > buf.byteLength) throw new Error(`SharedCounter: index ${slot} is out of range for a ${buf.byteLength}-byte buffer`);
|
|
32283
|
+
this.buffer = buf;
|
|
32284
|
+
this.#view = new Int32Array(buf);
|
|
32285
|
+
this.#index = slot;
|
|
32286
|
+
}
|
|
32287
|
+
/**
|
|
32288
|
+
* The current counter value, read atomically via `Atomics.load`.
|
|
32289
|
+
*/
|
|
32290
|
+
get value() {
|
|
32291
|
+
return Atomics.load(this.#view, this.#index);
|
|
32292
|
+
}
|
|
32293
|
+
/**
|
|
32294
|
+
* Atomically add `n` to the counter.
|
|
32295
|
+
*
|
|
32296
|
+
* Per `Atomics.add` semantics this returns the **previous** value — the
|
|
32297
|
+
* value the slot held *before* `n` was added — not the new total. Read
|
|
32298
|
+
* {@link SharedCounter.value} afterwards for the updated total.
|
|
32299
|
+
*
|
|
32300
|
+
* @param n - Integer addend (may be negative to subtract).
|
|
32301
|
+
* @returns The value that was stored before the addition.
|
|
32302
|
+
*/
|
|
32303
|
+
add(n) {
|
|
32304
|
+
return Atomics.add(this.#view, this.#index, n | 0);
|
|
32305
|
+
}
|
|
32306
|
+
/**
|
|
32307
|
+
* Atomically increment the counter by one.
|
|
32308
|
+
*
|
|
32309
|
+
* As with {@link SharedCounter.add}, the returned number is the
|
|
32310
|
+
* **pre-increment** value.
|
|
32311
|
+
*
|
|
32312
|
+
* @returns The value that was stored before incrementing.
|
|
32313
|
+
*/
|
|
32314
|
+
increment() {
|
|
32315
|
+
return Atomics.add(this.#view, this.#index, 1);
|
|
32316
|
+
}
|
|
32317
|
+
/**
|
|
32318
|
+
* Atomically set the counter to `next` iff it currently equals
|
|
32319
|
+
* `expected`, via `Atomics.compareExchange`.
|
|
32320
|
+
*
|
|
32321
|
+
* @param expected - The value the swap is conditional on.
|
|
32322
|
+
* @param next - The value to store when `expected` matches.
|
|
32323
|
+
* @returns The value that was at the slot **before** the call. The swap
|
|
32324
|
+
* succeeded iff this equals `expected`.
|
|
32325
|
+
*/
|
|
32326
|
+
compareExchange(expected, next) {
|
|
32327
|
+
return Atomics.compareExchange(this.#view, this.#index, expected | 0, next | 0);
|
|
32328
|
+
}
|
|
32329
|
+
};
|
|
32330
|
+
/** Slot value meaning "flag is clear / unset". */
|
|
32331
|
+
const FLAG_UNSET = 0;
|
|
32332
|
+
/** Slot value meaning "flag is set". */
|
|
32333
|
+
const FLAG_SET = 1;
|
|
32334
|
+
/**
|
|
32335
|
+
* A one-bit synchronisation gate over a single `Int32Array` slot
|
|
32336
|
+
* (`0` = unset, `1` = set), supporting `Atomics.wait` / `Atomics.notify`.
|
|
32337
|
+
*
|
|
32338
|
+
* Producers call {@link SharedFlag.set} then {@link SharedFlag.notify} to
|
|
32339
|
+
* release agents blocked in {@link SharedFlag.wait}.
|
|
32340
|
+
*
|
|
32341
|
+
* IMPORTANT — `Atomics.wait` may only block on a *non-main* agent. On the
|
|
32342
|
+
* main browser thread it throws `TypeError`, and on any main thread it
|
|
32343
|
+
* would freeze the event loop. {@link SharedFlag.wait} feature-detects
|
|
32344
|
+
* blocking support and, when blocking is not permitted, degrades to a
|
|
32345
|
+
* single non-blocking check (returning `'ok'` if already set, otherwise
|
|
32346
|
+
* `'not-equal'`) instead of throwing. {@link SharedFlag.notify} is always
|
|
32347
|
+
* safe to call from any thread.
|
|
32348
|
+
*/
|
|
32349
|
+
var SharedFlag = class {
|
|
32350
|
+
/** Int32 view over the backing buffer; the flag lives at index 0. */
|
|
32351
|
+
#view;
|
|
32352
|
+
/** The `SharedArrayBuffer` backing this flag. */
|
|
32353
|
+
buffer;
|
|
32354
|
+
/**
|
|
32355
|
+
* @param buffer - Existing shared buffer to attach to (its first `Int32`
|
|
32356
|
+
* slot is used). When omitted, a fresh `4`-byte
|
|
32357
|
+
* `SharedArrayBuffer` is allocated, starting cleared.
|
|
32358
|
+
* @throws If shared memory is unavailable when allocating, or `buffer`
|
|
32359
|
+
* is too small to hold one `Int32`.
|
|
32360
|
+
*/
|
|
32361
|
+
constructor(buffer) {
|
|
32362
|
+
const buf = buffer ?? allocShared(INT32_BYTES);
|
|
32363
|
+
if (buf.byteLength < INT32_BYTES) throw new Error(`SharedFlag: buffer must be at least ${INT32_BYTES} bytes, got ${buf.byteLength}`);
|
|
32364
|
+
this.buffer = buf;
|
|
32365
|
+
this.#view = new Int32Array(buf);
|
|
32366
|
+
}
|
|
32367
|
+
/**
|
|
32368
|
+
* Atomically set the flag. Does not itself wake waiters — call
|
|
32369
|
+
* {@link SharedFlag.notify} afterwards to release any blocked agents.
|
|
32370
|
+
*/
|
|
32371
|
+
set() {
|
|
32372
|
+
Atomics.store(this.#view, 0, FLAG_SET);
|
|
32373
|
+
}
|
|
32374
|
+
/**
|
|
32375
|
+
* Atomically clear the flag back to its unset state.
|
|
32376
|
+
*/
|
|
32377
|
+
clear() {
|
|
32378
|
+
Atomics.store(this.#view, 0, FLAG_UNSET);
|
|
32379
|
+
}
|
|
32380
|
+
/**
|
|
32381
|
+
* @returns `true` iff the flag is currently set, read via `Atomics.load`.
|
|
32382
|
+
*/
|
|
32383
|
+
isSet() {
|
|
32384
|
+
return Atomics.load(this.#view, 0) === FLAG_SET;
|
|
32385
|
+
}
|
|
32386
|
+
/**
|
|
32387
|
+
* Block until the flag becomes set, or until `timeoutMs` elapses.
|
|
32388
|
+
*
|
|
32389
|
+
* Implemented with `Atomics.wait` on the unset value: while the slot
|
|
32390
|
+
* still reads `0` (unset) the agent sleeps; a {@link SharedFlag.set} +
|
|
32391
|
+
* {@link SharedFlag.notify} from another agent wakes it.
|
|
32392
|
+
*
|
|
32393
|
+
* Blocking is only legal off the main thread. When `Atomics.wait` is not
|
|
32394
|
+
* allowed here (e.g. the main browser thread), this method does **not**
|
|
32395
|
+
* throw: it performs a single non-blocking check and returns `'ok'` if
|
|
32396
|
+
* the flag is already set, or `'not-equal'` otherwise. Callers that need
|
|
32397
|
+
* to truly block must run on a dedicated worker.
|
|
32398
|
+
*
|
|
32399
|
+
* @param timeoutMs - Optional timeout in milliseconds. Omit (or pass
|
|
32400
|
+
* `Infinity`) to wait indefinitely.
|
|
32401
|
+
* @returns `'ok'` when woken with the flag still unset at entry,
|
|
32402
|
+
* `'timed-out'` if the timeout expired, or `'not-equal'` if the
|
|
32403
|
+
* flag was already set on entry (nothing to wait for).
|
|
32404
|
+
*/
|
|
32405
|
+
wait(timeoutMs) {
|
|
32406
|
+
if (Atomics.load(this.#view, 0) === FLAG_SET) return "not-equal";
|
|
32407
|
+
if (typeof Atomics.wait !== "function") return Atomics.load(this.#view, 0) === FLAG_SET ? "ok" : "not-equal";
|
|
32408
|
+
try {
|
|
32409
|
+
const timeout = timeoutMs ?? Infinity;
|
|
32410
|
+
return Atomics.wait(this.#view, 0, FLAG_UNSET, timeout);
|
|
32411
|
+
} catch {
|
|
32412
|
+
return Atomics.load(this.#view, 0) === FLAG_SET ? "ok" : "not-equal";
|
|
32413
|
+
}
|
|
32414
|
+
}
|
|
32415
|
+
/**
|
|
32416
|
+
* Wake up to `count` agents blocked in {@link SharedFlag.wait} on this
|
|
32417
|
+
* flag, via `Atomics.notify`. Safe to call from any thread.
|
|
32418
|
+
*
|
|
32419
|
+
* @param count - Maximum number of waiters to wake. Defaults to
|
|
32420
|
+
* `Infinity` (wake all).
|
|
32421
|
+
* @returns The number of agents actually woken — `0` when none were
|
|
32422
|
+
* waiting.
|
|
32423
|
+
*/
|
|
32424
|
+
notify(count) {
|
|
32425
|
+
return Atomics.notify(this.#view, 0, count ?? Infinity);
|
|
32426
|
+
}
|
|
32427
|
+
};
|
|
32428
|
+
/**
|
|
32429
|
+
* Control-region layout, in `Int32` slots, preceding the data region:
|
|
32430
|
+
*
|
|
32431
|
+
* - slot 0 (`HEAD_SLOT`): read cursor — byte offset into the data region of
|
|
32432
|
+
* the next frame to be popped by the (single) consumer.
|
|
32433
|
+
* - slot 1 (`TAIL_SLOT`): write cursor — byte offset into the data region
|
|
32434
|
+
* at which the next frame will be pushed by the (single) producer.
|
|
32435
|
+
*
|
|
32436
|
+
* Both cursors range over `[0, dataBytes)` and wrap modulo the data size.
|
|
32437
|
+
* The buffer is empty when `head === tail` and considered full when the
|
|
32438
|
+
* next write would catch up to `head` (one frame slot is kept free to
|
|
32439
|
+
* disambiguate the full/empty states).
|
|
32440
|
+
*/
|
|
32441
|
+
const HEAD_SLOT = 0;
|
|
32442
|
+
const TAIL_SLOT = 1;
|
|
32443
|
+
/** Number of Int32 control slots ([head, tail]) ahead of the data region. */
|
|
32444
|
+
const CONTROL_SLOTS = 2;
|
|
32445
|
+
/** Byte size of the control region. */
|
|
32446
|
+
const CONTROL_BYTES = CONTROL_SLOTS * INT32_BYTES;
|
|
32447
|
+
/** Bytes used by each frame's little-endian `uint32` length header. */
|
|
32448
|
+
const LENGTH_HEADER_BYTES = 4;
|
|
32449
|
+
/**
|
|
32450
|
+
* A lock-free **single-producer / single-consumer** (SPSC) byte ring buffer
|
|
32451
|
+
* over a `SharedArrayBuffer`, suitable for streaming chunks between exactly
|
|
32452
|
+
* one producing worker and one consuming worker.
|
|
32453
|
+
*
|
|
32454
|
+
* ## Contract
|
|
32455
|
+
*
|
|
32456
|
+
* - **SPSC only.** Correctness relies on there being at most one concurrent
|
|
32457
|
+
* {@link SharedRingBuffer.push} caller (the producer) and at most one
|
|
32458
|
+
* concurrent {@link SharedRingBuffer.pop} caller (the consumer). Multiple
|
|
32459
|
+
* producers or multiple consumers are **not** supported and will corrupt
|
|
32460
|
+
* the cursors. (The producer and consumer may be different agents.)
|
|
32461
|
+
* - {@link SharedRingBuffer.push} returns `false` (writing nothing) when the
|
|
32462
|
+
* frame would not fit in the currently free space.
|
|
32463
|
+
* - {@link SharedRingBuffer.pop} returns `null` when the ring is empty, or
|
|
32464
|
+
* when the next frame is larger than the caller's `maxBytes` budget (the
|
|
32465
|
+
* frame is left intact for a later, larger pop).
|
|
32466
|
+
*
|
|
32467
|
+
* ## SAB layout
|
|
32468
|
+
*
|
|
32469
|
+
* `[ head:int32 | tail:int32 | ...dataBytes ]`
|
|
32470
|
+
*
|
|
32471
|
+
* The `head`/`tail` cursors are published with `Atomics.store` and read with
|
|
32472
|
+
* `Atomics.load`, which establishes the happens-before edge that makes the
|
|
32473
|
+
* non-atomic byte writes into the data region visible to the consumer. Each
|
|
32474
|
+
* frame is encoded as a 4-byte little-endian length header followed by that
|
|
32475
|
+
* many payload bytes; both header and payload wrap around the data region.
|
|
32476
|
+
*/
|
|
32477
|
+
var SharedRingBuffer = class SharedRingBuffer {
|
|
32478
|
+
/** Int32 view over the control slots (head/tail). */
|
|
32479
|
+
#control;
|
|
32480
|
+
/** Byte view over the data region (excludes the control slots). */
|
|
32481
|
+
#data;
|
|
32482
|
+
/** Size, in bytes, of the data region. */
|
|
32483
|
+
#size;
|
|
32484
|
+
/** The `SharedArrayBuffer` backing this ring. */
|
|
32485
|
+
buffer;
|
|
32486
|
+
/**
|
|
32487
|
+
* Allocate a new ring whose data region holds `capacityBytes` bytes
|
|
32488
|
+
* (the backing `SharedArrayBuffer` is `capacityBytes` + control overhead).
|
|
32489
|
+
*
|
|
32490
|
+
* @param capacityBytes - Size of the data region in bytes. Must be a
|
|
32491
|
+
* positive integer.
|
|
32492
|
+
* @param existingBuffer - Internal: when supplied, the ring attaches to
|
|
32493
|
+
* this already-allocated buffer instead of creating
|
|
32494
|
+
* a fresh one (used by {@link SharedRingBuffer.fromBuffer}).
|
|
32495
|
+
* Its data region must equal `capacityBytes`.
|
|
32496
|
+
* @throws If `capacityBytes` is not a positive integer, or shared memory
|
|
32497
|
+
* is unavailable.
|
|
32498
|
+
*/
|
|
32499
|
+
constructor(capacityBytes, existingBuffer) {
|
|
32500
|
+
if (!Number.isInteger(capacityBytes) || capacityBytes <= 0) throw new Error(`SharedRingBuffer: capacityBytes must be a positive integer, received ${capacityBytes}`);
|
|
32501
|
+
const buf = existingBuffer ?? allocShared(CONTROL_BYTES + capacityBytes);
|
|
32502
|
+
this.buffer = buf;
|
|
32503
|
+
this.#control = new Int32Array(buf, 0, CONTROL_SLOTS);
|
|
32504
|
+
this.#data = new Uint8Array(buf, CONTROL_BYTES, capacityBytes);
|
|
32505
|
+
this.#size = capacityBytes;
|
|
32506
|
+
}
|
|
32507
|
+
/**
|
|
32508
|
+
* Attach a second `SharedRingBuffer` view to an existing buffer — e.g.
|
|
32509
|
+
* the consumer side wrapping a buffer received from the producer via
|
|
32510
|
+
* `postMessage`. No cursors are reset; the view observes whatever the
|
|
32511
|
+
* other side has already published.
|
|
32512
|
+
*
|
|
32513
|
+
* @param buffer - A `SharedArrayBuffer` previously created by a
|
|
32514
|
+
* `SharedRingBuffer` constructor.
|
|
32515
|
+
* @returns A `SharedRingBuffer` sharing `buffer`'s cursors and data.
|
|
32516
|
+
* @throws If `buffer` is too small to contain the control region.
|
|
32517
|
+
*/
|
|
32518
|
+
static fromBuffer(buffer) {
|
|
32519
|
+
if (buffer.byteLength <= CONTROL_BYTES) throw new Error(`SharedRingBuffer.fromBuffer: buffer too small (${buffer.byteLength} bytes)`);
|
|
32520
|
+
return new SharedRingBuffer(buffer.byteLength - CONTROL_BYTES, buffer);
|
|
32521
|
+
}
|
|
32522
|
+
/** @internal Free bytes available for a new frame's payload + header. */
|
|
32523
|
+
#freeBytes(head, tail) {
|
|
32524
|
+
if (tail >= head) return this.#size - (tail - head) - 1;
|
|
32525
|
+
return head - tail - 1;
|
|
32526
|
+
}
|
|
32527
|
+
/**
|
|
32528
|
+
* Push a single byte frame onto the ring (producer side).
|
|
32529
|
+
*
|
|
32530
|
+
* The frame is stored as a length-prefixed record. Returns `false`
|
|
32531
|
+
* without modifying the ring if there is not enough free space for the
|
|
32532
|
+
* header plus payload.
|
|
32533
|
+
*
|
|
32534
|
+
* @param bytes - Payload to enqueue (may be empty).
|
|
32535
|
+
* @returns `true` if enqueued, `false` if the ring was too full.
|
|
32536
|
+
*/
|
|
32537
|
+
push(bytes) {
|
|
32538
|
+
const head = Atomics.load(this.#control, HEAD_SLOT);
|
|
32539
|
+
const tail = Atomics.load(this.#control, TAIL_SLOT);
|
|
32540
|
+
if (LENGTH_HEADER_BYTES + bytes.length > this.#freeBytes(head, tail)) return false;
|
|
32541
|
+
let cursor = tail;
|
|
32542
|
+
const len = bytes.length;
|
|
32543
|
+
cursor = this.#writeByte(cursor, len & 255);
|
|
32544
|
+
cursor = this.#writeByte(cursor, len >>> 8 & 255);
|
|
32545
|
+
cursor = this.#writeByte(cursor, len >>> 16 & 255);
|
|
32546
|
+
cursor = this.#writeByte(cursor, len >>> 24 & 255);
|
|
32547
|
+
for (let i = 0; i < len; i += 1) cursor = this.#writeByte(cursor, bytes[i] ?? 0);
|
|
32548
|
+
Atomics.store(this.#control, TAIL_SLOT, cursor);
|
|
32549
|
+
return true;
|
|
32550
|
+
}
|
|
32551
|
+
/**
|
|
32552
|
+
* Pop the next byte frame from the ring (consumer side).
|
|
32553
|
+
*
|
|
32554
|
+
* @param maxBytes - Maximum payload size the caller is willing to receive.
|
|
32555
|
+
* If the next frame's payload exceeds this, the frame is
|
|
32556
|
+
* left in place and `null` is returned.
|
|
32557
|
+
* @returns The dequeued payload as a fresh `Uint8Array`, or `null` when
|
|
32558
|
+
* the ring is empty or the next frame exceeds `maxBytes`.
|
|
32559
|
+
*/
|
|
32560
|
+
pop(maxBytes) {
|
|
32561
|
+
const head = Atomics.load(this.#control, HEAD_SLOT);
|
|
32562
|
+
if (head === Atomics.load(this.#control, TAIL_SLOT)) return null;
|
|
32563
|
+
let cursor = head;
|
|
32564
|
+
const b0 = this.#readByte(cursor);
|
|
32565
|
+
cursor = (cursor + 1) % this.#size;
|
|
32566
|
+
const b1 = this.#readByte(cursor);
|
|
32567
|
+
cursor = (cursor + 1) % this.#size;
|
|
32568
|
+
const b2 = this.#readByte(cursor);
|
|
32569
|
+
cursor = (cursor + 1) % this.#size;
|
|
32570
|
+
const b3 = this.#readByte(cursor);
|
|
32571
|
+
cursor = (cursor + 1) % this.#size;
|
|
32572
|
+
const len = (b0 | b1 << 8 | b2 << 16 | b3 << 24) >>> 0;
|
|
32573
|
+
if (len > maxBytes) return null;
|
|
32574
|
+
const out = new Uint8Array(len);
|
|
32575
|
+
for (let i = 0; i < len; i += 1) {
|
|
32576
|
+
out[i] = this.#readByte(cursor);
|
|
32577
|
+
cursor = (cursor + 1) % this.#size;
|
|
32578
|
+
}
|
|
32579
|
+
Atomics.store(this.#control, HEAD_SLOT, cursor);
|
|
32580
|
+
return out;
|
|
32581
|
+
}
|
|
32582
|
+
/**
|
|
32583
|
+
* Write one byte at `cursor` (already within `[0, size)`) and return the
|
|
32584
|
+
* next cursor position, wrapping around the data region.
|
|
32585
|
+
*/
|
|
32586
|
+
#writeByte(cursor, value) {
|
|
32587
|
+
this.#data[cursor] = value;
|
|
32588
|
+
return (cursor + 1) % this.#size;
|
|
32589
|
+
}
|
|
32590
|
+
/**
|
|
32591
|
+
* Read one byte at `cursor` (already within `[0, size)`). The `?? 0` only
|
|
32592
|
+
* satisfies `noUncheckedIndexedAccess`; `cursor` is always in range.
|
|
32593
|
+
*/
|
|
32594
|
+
#readByte(cursor) {
|
|
32595
|
+
return this.#data[cursor] ?? 0;
|
|
32596
|
+
}
|
|
32597
|
+
};
|
|
32598
|
+
//#endregion
|
|
32599
|
+
//#region src/runtime/memoryBudget.ts
|
|
32600
|
+
/**
|
|
32601
|
+
* @module runtime/memoryBudget
|
|
32602
|
+
*
|
|
32603
|
+
* A memory budget / cap used to bound the processing of *untrusted* PDFs as a
|
|
32604
|
+
* defence against decompression bombs and out-of-memory (OOM) attacks.
|
|
32605
|
+
*
|
|
32606
|
+
* ## What this is — and what it is NOT
|
|
32607
|
+
*
|
|
32608
|
+
* {@link MemoryBudget} is a **pure accounting guard**. It is a plain integer
|
|
32609
|
+
* counter with a ceiling. It does **NOT** measure real process memory (RSS),
|
|
32610
|
+
* heap usage, `performance.memory`, or anything reported by the host. It makes
|
|
32611
|
+
* **no** claim of measuring actual memory and performs **no** acceleration of
|
|
32612
|
+
* any kind — there are no workers, SIMD, threads, `Atomics`, or
|
|
32613
|
+
* `SharedArrayBuffer` involved.
|
|
32614
|
+
*
|
|
32615
|
+
* The intended usage pattern is *predictive*: before a caller allocates a
|
|
32616
|
+
* large buffer (for example, the expected output length of an inflated /
|
|
32617
|
+
* decoded stream, which a malicious PDF can declare as enormous), it reports
|
|
32618
|
+
* that size to the budget via {@link MemoryBudget.allocate} (or
|
|
32619
|
+
* {@link MemoryBudget.tryAllocate}). If the reported size would push total
|
|
32620
|
+
* tracked usage past the configured limit, the allocation is rejected
|
|
32621
|
+
* **before** the real memory is ever requested — so a bomb is stopped early
|
|
32622
|
+
* instead of being materialised and crashing the runtime.
|
|
32623
|
+
*
|
|
32624
|
+
* Because it is pure accounting, its accuracy is exactly as good as the sizes
|
|
32625
|
+
* callers report. It is a budget, not a profiler.
|
|
32626
|
+
*
|
|
32627
|
+
* ## Runtime
|
|
32628
|
+
*
|
|
32629
|
+
* This module uses only ECMAScript primitives (numbers, classes, closures).
|
|
32630
|
+
* It assumes no Node-only globals and runs unchanged on Node, Deno, Bun,
|
|
32631
|
+
* Cloudflare Workers, and browsers.
|
|
32632
|
+
*/
|
|
32633
|
+
/**
|
|
32634
|
+
* Thrown by {@link MemoryBudget.allocate} (and {@link MemoryBudget.withAllocation})
|
|
32635
|
+
* when a requested allocation would exceed the configured limit.
|
|
32636
|
+
*
|
|
32637
|
+
* Carries the numbers involved so callers can surface a precise diagnostic:
|
|
32638
|
+
*
|
|
32639
|
+
* - {@link requested} — the size that was being allocated.
|
|
32640
|
+
* - {@link used} — the tracked usage *before* the rejected allocation.
|
|
32641
|
+
* - {@link limit} — the configured ceiling.
|
|
32642
|
+
*/
|
|
32643
|
+
var MemoryBudgetExceededError = class extends Error {
|
|
32644
|
+
/** The allocation size, in bytes, that was rejected. */
|
|
32645
|
+
requested;
|
|
32646
|
+
/** The configured budget ceiling, in bytes. */
|
|
32647
|
+
limit;
|
|
32648
|
+
/** The tracked usage, in bytes, at the moment of rejection. */
|
|
32649
|
+
used;
|
|
32650
|
+
/**
|
|
32651
|
+
* @param requested - The size, in bytes, that was being allocated.
|
|
32652
|
+
* @param used - The tracked usage, in bytes, before the allocation.
|
|
32653
|
+
* @param limit - The configured ceiling, in bytes.
|
|
32654
|
+
*/
|
|
32655
|
+
constructor(requested, used, limit) {
|
|
32656
|
+
super(`Memory budget exceeded: requested ${requested} byte(s) with ${used} of ${limit} byte(s) already in use (would total ${used + requested}).`);
|
|
32657
|
+
this.name = "MemoryBudgetExceededError";
|
|
32658
|
+
this.requested = requested;
|
|
32659
|
+
this.used = used;
|
|
32660
|
+
this.limit = limit;
|
|
32661
|
+
}
|
|
32662
|
+
};
|
|
32663
|
+
/**
|
|
32664
|
+
* Validate a configured limit. The limit must be a finite number strictly
|
|
32665
|
+
* greater than zero; `NaN`, negative values, and `Infinity` are rejected.
|
|
32666
|
+
*
|
|
32667
|
+
* @throws {TypeError} If the limit is not a finite number `> 0`.
|
|
32668
|
+
*/
|
|
32669
|
+
function assertValidLimit(limitBytes) {
|
|
32670
|
+
if (typeof limitBytes !== "number" || !Number.isFinite(limitBytes) || limitBytes <= 0) throw new TypeError(`MemoryBudget: limitBytes must be a finite number greater than 0, received ${String(limitBytes)}.`);
|
|
32671
|
+
}
|
|
32672
|
+
/**
|
|
32673
|
+
* Validate a size passed to {@link MemoryBudget.allocate},
|
|
32674
|
+
* {@link MemoryBudget.tryAllocate}, {@link MemoryBudget.release}, or
|
|
32675
|
+
* {@link MemoryBudget.withAllocation}. The size must be a finite,
|
|
32676
|
+
* non-negative number; `NaN`, negative values, and `Infinity` are rejected.
|
|
32677
|
+
*
|
|
32678
|
+
* @throws {TypeError} If the size is not a finite number `>= 0`.
|
|
32679
|
+
*/
|
|
32680
|
+
function assertValidSize(bytes, method) {
|
|
32681
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) throw new TypeError(`MemoryBudget.${method}: bytes must be a finite, non-negative number, received ${String(bytes)}.`);
|
|
32682
|
+
}
|
|
32683
|
+
/**
|
|
32684
|
+
* A bounded memory accounting guard. See the {@link module:runtime/memoryBudget module documentation}
|
|
32685
|
+
* for the precise semantics — in particular, that this tracks *reported*
|
|
32686
|
+
* sizes and does not measure real RSS.
|
|
32687
|
+
*/
|
|
32688
|
+
var MemoryBudget = class {
|
|
32689
|
+
/** The configured ceiling, in bytes. */
|
|
32690
|
+
#limit;
|
|
32691
|
+
/** The optional pre-throw notification callback. */
|
|
32692
|
+
#onExceed;
|
|
32693
|
+
/** Currently tracked usage, in bytes. Always `0 <= used <= limit`. */
|
|
32694
|
+
#used = 0;
|
|
32695
|
+
/**
|
|
32696
|
+
* @param options - The {@link MemoryBudgetOptions}. `limitBytes` must be a
|
|
32697
|
+
* finite number greater than `0`.
|
|
32698
|
+
* @throws {TypeError} If `limitBytes` is not a finite number `> 0`.
|
|
32699
|
+
*/
|
|
32700
|
+
constructor(options) {
|
|
32701
|
+
assertValidLimit(options.limitBytes);
|
|
32702
|
+
this.#limit = options.limitBytes;
|
|
32703
|
+
this.#onExceed = options.onExceed;
|
|
32704
|
+
}
|
|
32705
|
+
/** Currently tracked usage, in bytes. */
|
|
32706
|
+
get used() {
|
|
32707
|
+
return this.#used;
|
|
32708
|
+
}
|
|
32709
|
+
/** The configured ceiling, in bytes. */
|
|
32710
|
+
get limit() {
|
|
32711
|
+
return this.#limit;
|
|
32712
|
+
}
|
|
32713
|
+
/** Bytes still available before the limit is reached (never negative). */
|
|
32714
|
+
get remaining() {
|
|
32715
|
+
return this.#limit - this.#used;
|
|
32716
|
+
}
|
|
32717
|
+
/**
|
|
32718
|
+
* Reserve `bytes` against the budget.
|
|
32719
|
+
*
|
|
32720
|
+
* If `used + bytes` would exceed the limit, {@link onExceed} (if supplied)
|
|
32721
|
+
* is invoked first and then a {@link MemoryBudgetExceededError} is thrown;
|
|
32722
|
+
* tracked usage is left unchanged in that case.
|
|
32723
|
+
*
|
|
32724
|
+
* @param bytes - The size to reserve. Must be a finite number `>= 0`.
|
|
32725
|
+
* @throws {TypeError} If `bytes` is negative, `NaN`, or `Infinity`.
|
|
32726
|
+
* @throws {MemoryBudgetExceededError} If the allocation would exceed the limit.
|
|
32727
|
+
*/
|
|
32728
|
+
allocate(bytes) {
|
|
32729
|
+
assertValidSize(bytes, "allocate");
|
|
32730
|
+
if (this.#used + bytes > this.#limit) {
|
|
32731
|
+
this.#onExceed?.({
|
|
32732
|
+
requested: bytes,
|
|
32733
|
+
used: this.#used,
|
|
32734
|
+
limit: this.#limit
|
|
32735
|
+
});
|
|
32736
|
+
throw new MemoryBudgetExceededError(bytes, this.#used, this.#limit);
|
|
32737
|
+
}
|
|
32738
|
+
this.#used += bytes;
|
|
32739
|
+
}
|
|
32740
|
+
/**
|
|
32741
|
+
* Non-throwing variant of {@link allocate}. Reserves `bytes` and returns
|
|
32742
|
+
* `true` on success, or returns `false` (leaving usage unchanged) if the
|
|
32743
|
+
* allocation would exceed the limit. {@link onExceed} is **not** invoked.
|
|
32744
|
+
*
|
|
32745
|
+
* @param bytes - The size to reserve. Must be a finite number `>= 0`.
|
|
32746
|
+
* @returns `true` if reserved, `false` if it would exceed the limit.
|
|
32747
|
+
* @throws {TypeError} If `bytes` is negative, `NaN`, or `Infinity`.
|
|
32748
|
+
*/
|
|
32749
|
+
tryAllocate(bytes) {
|
|
32750
|
+
assertValidSize(bytes, "tryAllocate");
|
|
32751
|
+
if (this.#used + bytes > this.#limit) return false;
|
|
32752
|
+
this.#used += bytes;
|
|
32753
|
+
return true;
|
|
32754
|
+
}
|
|
32755
|
+
/**
|
|
32756
|
+
* Return `bytes` to the budget. Usage is clamped at `0`, so over-releasing
|
|
32757
|
+
* (or releasing more than was reserved) never produces a negative value.
|
|
32758
|
+
*
|
|
32759
|
+
* @param bytes - The size to release. Must be a finite number `>= 0`.
|
|
32760
|
+
* @throws {TypeError} If `bytes` is negative, `NaN`, or `Infinity`.
|
|
32761
|
+
*/
|
|
32762
|
+
release(bytes) {
|
|
32763
|
+
assertValidSize(bytes, "release");
|
|
32764
|
+
this.#used = Math.max(0, this.#used - bytes);
|
|
32765
|
+
}
|
|
32766
|
+
/** Reset tracked usage to zero. The limit is unchanged. */
|
|
32767
|
+
reset() {
|
|
32768
|
+
this.#used = 0;
|
|
32769
|
+
}
|
|
32770
|
+
/**
|
|
32771
|
+
* Reserve `bytes`, run `fn`, and release `bytes` in a `finally` block so the
|
|
32772
|
+
* reservation is returned even if `fn` throws.
|
|
32773
|
+
*
|
|
32774
|
+
* If the reservation itself would exceed the limit, `fn` is never invoked
|
|
32775
|
+
* and a {@link MemoryBudgetExceededError} is thrown (nothing to release).
|
|
32776
|
+
*
|
|
32777
|
+
* @typeParam T - The return type of `fn`.
|
|
32778
|
+
* @param bytes - The size to reserve for the duration of `fn`.
|
|
32779
|
+
* @param fn - The work to run while the reservation is held.
|
|
32780
|
+
* @returns Whatever `fn` returns.
|
|
32781
|
+
* @throws {TypeError} If `bytes` is negative, `NaN`, or `Infinity`.
|
|
32782
|
+
* @throws {MemoryBudgetExceededError} If the reservation would exceed the limit.
|
|
32783
|
+
*/
|
|
32784
|
+
withAllocation(bytes, fn) {
|
|
32785
|
+
this.allocate(bytes);
|
|
32786
|
+
try {
|
|
32787
|
+
return fn();
|
|
32788
|
+
} finally {
|
|
32789
|
+
this.release(bytes);
|
|
32790
|
+
}
|
|
32791
|
+
}
|
|
32792
|
+
};
|
|
32793
|
+
/**
|
|
32794
|
+
* Create a {@link MemoryBudget} with the given ceiling.
|
|
32795
|
+
*
|
|
32796
|
+
* The limit must be a finite number strictly greater than `0`; `NaN`,
|
|
32797
|
+
* negative values, and `Infinity` are all rejected with a {@link TypeError}.
|
|
32798
|
+
* (An infinite budget is intentionally disallowed: the whole point is to cap
|
|
32799
|
+
* untrusted input, and `Infinity` would silently disable the guard.)
|
|
32800
|
+
*
|
|
32801
|
+
* @param limitBytes - The ceiling, in bytes. Must be finite and `> 0`.
|
|
32802
|
+
* @returns A new {@link MemoryBudget}.
|
|
32803
|
+
* @throws {TypeError} If `limitBytes` is not a finite number `> 0`.
|
|
32804
|
+
*/
|
|
32805
|
+
function createMemoryBudget(limitBytes) {
|
|
32806
|
+
return new MemoryBudget({ limitBytes });
|
|
32807
|
+
}
|
|
32808
|
+
//#endregion
|
|
32809
|
+
//#region src/runtime/runtimeCapabilities.ts
|
|
32810
|
+
/**
|
|
32811
|
+
* SIMD probe: a function `() -> v128` whose body is `i32.const 0; i32x4.splat;
|
|
32812
|
+
* i8x16.popcnt`. The `v128` result type and the `0xfd …` SIMD opcodes only
|
|
32813
|
+
* validate on a host implementing the SIMD MVP. Verified to validate on a
|
|
32814
|
+
* SIMD-capable host and to be rejected by a host lacking SIMD.
|
|
32815
|
+
*/
|
|
32816
|
+
const WASM_SIMD_MODULE = new Uint8Array([
|
|
32817
|
+
0,
|
|
32818
|
+
97,
|
|
32819
|
+
115,
|
|
32820
|
+
109,
|
|
32821
|
+
1,
|
|
32822
|
+
0,
|
|
32823
|
+
0,
|
|
32824
|
+
0,
|
|
32825
|
+
1,
|
|
32826
|
+
5,
|
|
32827
|
+
1,
|
|
32828
|
+
96,
|
|
32829
|
+
0,
|
|
32830
|
+
1,
|
|
32831
|
+
123,
|
|
32832
|
+
3,
|
|
32833
|
+
2,
|
|
32834
|
+
1,
|
|
32835
|
+
0,
|
|
32836
|
+
10,
|
|
32837
|
+
10,
|
|
32838
|
+
1,
|
|
32839
|
+
8,
|
|
32840
|
+
0,
|
|
32841
|
+
65,
|
|
32842
|
+
0,
|
|
32843
|
+
253,
|
|
32844
|
+
15,
|
|
32845
|
+
253,
|
|
32846
|
+
98,
|
|
32847
|
+
11
|
|
32848
|
+
]);
|
|
32849
|
+
/**
|
|
32850
|
+
* Threads/atomics probe: a function whose body executes
|
|
32851
|
+
* `memory.atomic.notify` (`0xfe 0x00`) against a **shared** memory
|
|
32852
|
+
* (`flags = 0x03` ⇒ shared with a maximum). The shared-memory limits and the
|
|
32853
|
+
* `0xfe` atomic opcode only validate on a threads-capable host. Verified to
|
|
32854
|
+
* validate on such a host.
|
|
32855
|
+
*/
|
|
32856
|
+
const WASM_THREADS_MODULE = new Uint8Array([
|
|
32857
|
+
0,
|
|
32858
|
+
97,
|
|
32859
|
+
115,
|
|
32860
|
+
109,
|
|
32861
|
+
1,
|
|
32862
|
+
0,
|
|
32863
|
+
0,
|
|
32864
|
+
0,
|
|
32865
|
+
1,
|
|
32866
|
+
4,
|
|
32867
|
+
1,
|
|
32868
|
+
96,
|
|
32869
|
+
0,
|
|
32870
|
+
0,
|
|
32871
|
+
3,
|
|
32872
|
+
2,
|
|
32873
|
+
1,
|
|
32874
|
+
0,
|
|
32875
|
+
5,
|
|
32876
|
+
4,
|
|
32877
|
+
1,
|
|
32878
|
+
3,
|
|
32879
|
+
1,
|
|
32880
|
+
1,
|
|
32881
|
+
10,
|
|
32882
|
+
13,
|
|
32883
|
+
1,
|
|
32884
|
+
11,
|
|
32885
|
+
0,
|
|
32886
|
+
65,
|
|
32887
|
+
0,
|
|
32888
|
+
65,
|
|
32889
|
+
0,
|
|
32890
|
+
254,
|
|
32891
|
+
0,
|
|
32892
|
+
2,
|
|
32893
|
+
0,
|
|
32894
|
+
26,
|
|
32895
|
+
11
|
|
32896
|
+
]);
|
|
32897
|
+
/**
|
|
32898
|
+
* Bulk-memory probe: a function whose body executes `memory.fill`
|
|
32899
|
+
* (`0xfc 0x0b`) against a defined memory. The `0xfc 0x0b` opcode only validates
|
|
32900
|
+
* on a bulk-memory-capable host. Verified to validate with the memory section
|
|
32901
|
+
* present and to be rejected when the memory section is removed (proving
|
|
32902
|
+
* `memory.fill` is the discriminating instruction).
|
|
32903
|
+
*/
|
|
32904
|
+
const WASM_BULK_MEMORY_MODULE = new Uint8Array([
|
|
32905
|
+
0,
|
|
32906
|
+
97,
|
|
32907
|
+
115,
|
|
32908
|
+
109,
|
|
32909
|
+
1,
|
|
32910
|
+
0,
|
|
32911
|
+
0,
|
|
32912
|
+
0,
|
|
32913
|
+
1,
|
|
32914
|
+
4,
|
|
32915
|
+
1,
|
|
32916
|
+
96,
|
|
32917
|
+
0,
|
|
32918
|
+
0,
|
|
32919
|
+
3,
|
|
32920
|
+
2,
|
|
32921
|
+
1,
|
|
32922
|
+
0,
|
|
32923
|
+
5,
|
|
32924
|
+
3,
|
|
32925
|
+
1,
|
|
32926
|
+
0,
|
|
32927
|
+
1,
|
|
32928
|
+
10,
|
|
32929
|
+
13,
|
|
32930
|
+
1,
|
|
32931
|
+
11,
|
|
32932
|
+
0,
|
|
32933
|
+
65,
|
|
32934
|
+
0,
|
|
32935
|
+
65,
|
|
32936
|
+
0,
|
|
32937
|
+
65,
|
|
32938
|
+
0,
|
|
32939
|
+
252,
|
|
32940
|
+
11,
|
|
32941
|
+
0,
|
|
32942
|
+
11
|
|
32943
|
+
]);
|
|
32944
|
+
/**
|
|
32945
|
+
* `true` iff `WebAssembly` is an object exposing a callable `validate`.
|
|
32946
|
+
* `WebAssembly.validate` itself never throws for a well-formed buffer, so this
|
|
32947
|
+
* gate is what makes every WASM probe safe.
|
|
32948
|
+
*/
|
|
32949
|
+
function hasWasm() {
|
|
32950
|
+
try {
|
|
32951
|
+
return typeof WebAssembly === "object" && WebAssembly !== null && typeof WebAssembly.validate === "function";
|
|
32952
|
+
} catch {
|
|
32953
|
+
return false;
|
|
32954
|
+
}
|
|
32955
|
+
}
|
|
32956
|
+
/**
|
|
32957
|
+
* Validate a feature-detect module, returning `false` on any failure. Never
|
|
32958
|
+
* throws — a non-WASM host or a malformed buffer simply yields `false`.
|
|
32959
|
+
*/
|
|
32960
|
+
function validatesModule(bytes) {
|
|
32961
|
+
try {
|
|
32962
|
+
if (!hasWasm()) return false;
|
|
32963
|
+
return WebAssembly.validate(bytes);
|
|
32964
|
+
} catch {
|
|
32965
|
+
return false;
|
|
32966
|
+
}
|
|
32967
|
+
}
|
|
32968
|
+
/** `true` iff the named global is defined and not `undefined`. */
|
|
32969
|
+
function hasGlobal(name) {
|
|
32970
|
+
try {
|
|
32971
|
+
return globalThis[name] !== void 0;
|
|
32972
|
+
} catch {
|
|
32973
|
+
return false;
|
|
32974
|
+
}
|
|
32975
|
+
}
|
|
32976
|
+
/**
|
|
32977
|
+
* Best-effort logical-core count. Prefers `navigator.hardwareConcurrency`
|
|
32978
|
+
* (browsers, Workers, Deno, Bun, modern Node), then Node's `os.cpus().length`
|
|
32979
|
+
* if a synchronously-resolvable `os` module is reachable, then `1`. Always
|
|
32980
|
+
* returns a positive integer and never throws.
|
|
32981
|
+
*
|
|
32982
|
+
* Note: `os` is *not* statically imported — that would assume a Node global on
|
|
32983
|
+
* non-Node runtimes. We only consult `navigator`, which is feature-detected,
|
|
32984
|
+
* and otherwise fall back to `1`. (A dynamic `import('node:os')` is async and
|
|
32985
|
+
* cannot feed a synchronous detector, so it is intentionally not attempted
|
|
32986
|
+
* here; `navigator.hardwareConcurrency` is present on Node ≥ 21.)
|
|
32987
|
+
*/
|
|
32988
|
+
function resolveHardwareConcurrency() {
|
|
32989
|
+
try {
|
|
32990
|
+
const reported = globalThis.navigator?.hardwareConcurrency;
|
|
32991
|
+
if (typeof reported === "number" && Number.isInteger(reported) && reported > 0) return reported;
|
|
32992
|
+
} catch {}
|
|
32993
|
+
return 1;
|
|
32994
|
+
}
|
|
32995
|
+
/**
|
|
32996
|
+
* Honestly detect the perf-relevant capabilities of the current runtime.
|
|
32997
|
+
*
|
|
32998
|
+
* Every probe is feature-detected and throw-free; on a host missing a feature
|
|
32999
|
+
* the corresponding flag is `false` (or `1` for {@link
|
|
33000
|
+
* RuntimeCapabilities.hardwareConcurrency}). The result is **not** cached so a
|
|
33001
|
+
* test harness (or a host that gains a feature, e.g. cross-origin isolation
|
|
33002
|
+
* toggling) sees the live state.
|
|
33003
|
+
*
|
|
33004
|
+
* @returns A fresh {@link RuntimeCapabilities} snapshot.
|
|
33005
|
+
*/
|
|
33006
|
+
function detectRuntimeCapabilities() {
|
|
33007
|
+
const wasm = hasWasm();
|
|
33008
|
+
const sharedArrayBuffer = hasGlobal("SharedArrayBuffer");
|
|
33009
|
+
return {
|
|
33010
|
+
wasm,
|
|
33011
|
+
wasmSimd: wasm && validatesModule(WASM_SIMD_MODULE),
|
|
33012
|
+
wasmThreads: wasm && sharedArrayBuffer && validatesModule(WASM_THREADS_MODULE),
|
|
33013
|
+
wasmBulkMemory: wasm && validatesModule(WASM_BULK_MEMORY_MODULE),
|
|
33014
|
+
sharedArrayBuffer,
|
|
33015
|
+
atomics: hasGlobal("Atomics"),
|
|
33016
|
+
bigInt64Array: hasGlobal("BigInt64Array"),
|
|
33017
|
+
crossOriginIsolated: globalThis.crossOriginIsolated === true,
|
|
33018
|
+
hardwareConcurrency: resolveHardwareConcurrency()
|
|
33019
|
+
};
|
|
33020
|
+
}
|
|
33021
|
+
/**
|
|
33022
|
+
* Convenience predicate: does the host implement the WebAssembly SIMD proposal?
|
|
33023
|
+
*
|
|
33024
|
+
* Equivalent to `detectRuntimeCapabilities().wasmSimd`. A `true` result means a
|
|
33025
|
+
* SIMD-enabled module would validate here — **not** that modern-pdf-lib's
|
|
33026
|
+
* shipped WASM uses SIMD (see {@link SIMD_NOTE}).
|
|
33027
|
+
*
|
|
33028
|
+
* @returns `true` iff the SIMD feature-detect module validates.
|
|
33029
|
+
*/
|
|
33030
|
+
function isWasmSimdSupported() {
|
|
33031
|
+
return validatesModule(WASM_SIMD_MODULE);
|
|
33032
|
+
}
|
|
33033
|
+
/**
|
|
33034
|
+
* Honesty disclaimer about SIMD acceleration.
|
|
33035
|
+
*
|
|
33036
|
+
* modern-pdf-lib's bundled WebAssembly binaries are built **without** SIMD
|
|
33037
|
+
* today. {@link isWasmSimdSupported} / {@link RuntimeCapabilities.wasmSimd}
|
|
33038
|
+
* report only whether the *host* could run SIMD code — actually benefiting from
|
|
33039
|
+
* SIMD requires a SIMD-enabled rebuild of the WASM modules. This constant
|
|
33040
|
+
* exists so callers and docs do not mistake capability detection for active
|
|
33041
|
+
* acceleration.
|
|
33042
|
+
*/
|
|
33043
|
+
const SIMD_NOTE = "modern-pdf-lib's bundled WASM is currently built WITHOUT SIMD; isWasmSimdSupported()/wasmSimd report only that the host could run SIMD code. Realising SIMD acceleration requires a SIMD-enabled rebuild of the WASM modules — it is not used today.";
|
|
33044
|
+
//#endregion
|
|
33045
|
+
//#region src/jsx/jsxRuntime.ts
|
|
33046
|
+
/**
|
|
33047
|
+
* @module jsx/jsxRuntime
|
|
33048
|
+
*
|
|
33049
|
+
* A framework-agnostic **JSX / hyperscript renderer** that builds a real PDF
|
|
33050
|
+
* from a declarative element tree. It supports both the classic `h()` pragma
|
|
33051
|
+
* and the React-17+ *automatic runtime* (`jsx` / `jsxs` / `Fragment`), so you
|
|
33052
|
+
* can drive it from any JSX toolchain:
|
|
33053
|
+
*
|
|
33054
|
+
* ```jsonc
|
|
33055
|
+
* // tsconfig.json (automatic runtime)
|
|
33056
|
+
* { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "modern-pdf-lib/jsx" } }
|
|
33057
|
+
* ```
|
|
33058
|
+
* ```jsonc
|
|
33059
|
+
* // tsconfig.json (classic runtime)
|
|
33060
|
+
* { "compilerOptions": { "jsx": "react", "jsxFactory": "h", "jsxFragmentFactory": "Fragment" } }
|
|
33061
|
+
* ```
|
|
33062
|
+
*
|
|
33063
|
+
* @example Classic pragma
|
|
33064
|
+
* ```ts
|
|
33065
|
+
* import { h, renderToPdf } from 'modern-pdf-lib/jsx';
|
|
33066
|
+
* import { PageSizes } from 'modern-pdf-lib';
|
|
33067
|
+
*
|
|
33068
|
+
* const bytes = await renderToPdf(
|
|
33069
|
+
* h('document', { size: PageSizes.A4 },
|
|
33070
|
+
* h('page', null,
|
|
33071
|
+
* h('text', { size: 24, bold: true }, 'Invoice'),
|
|
33072
|
+
* h('text', null, 'Thank you for your business.'),
|
|
33073
|
+
* ),
|
|
33074
|
+
* ),
|
|
33075
|
+
* );
|
|
33076
|
+
* ```
|
|
33077
|
+
*
|
|
33078
|
+
* ## Intrinsic elements
|
|
33079
|
+
*
|
|
33080
|
+
* | Tag | Props | Meaning |
|
|
33081
|
+
* |-------------|--------------------------------------------------------------------|---------|
|
|
33082
|
+
* | `document` | `{ size? }` | Root container. `size` is the default page size. |
|
|
33083
|
+
* | `page` | `{ size? }` | Starts a new PDF page (overrides the document size). |
|
|
33084
|
+
* | `text` | `{ x?, y?, size?, color?, bold? }` | Draws one text line. |
|
|
33085
|
+
* | `view` | `{ x?, y?, width?, height?, padding?, background? }` | A block container; indents/pads its children. |
|
|
33086
|
+
* | `rect` | `{ x, y, width, height, color?, border? }` | Draws a rectangle. |
|
|
33087
|
+
*
|
|
33088
|
+
* Function components are plain `(props) => PdfNode` functions — they are
|
|
33089
|
+
* called with their props (children supplied as `props.children`) and their
|
|
33090
|
+
* return value is rendered in place. {@link Fragment} groups children without
|
|
33091
|
+
* adding any layout box.
|
|
33092
|
+
*
|
|
33093
|
+
* ## Layout model (simple, by design)
|
|
33094
|
+
*
|
|
33095
|
+
* This is **not** a flexbox engine. Layout is a deterministic *top-down block
|
|
33096
|
+
* flow* per page:
|
|
33097
|
+
*
|
|
33098
|
+
* - Each `page` maintains a running **y cursor** that starts at
|
|
33099
|
+
* `pageHeight - MARGIN_TOP` and a running **x cursor** at `MARGIN_LEFT`.
|
|
33100
|
+
* Because PDF coordinates are y-up but documents read top-down, the cursor
|
|
33101
|
+
* *decreases* as successive blocks are placed.
|
|
33102
|
+
* - A flowed `text` is drawn at the current `(x, y - ascent)` and then advances
|
|
33103
|
+
* the y cursor down by its line height (`size * LINE_HEIGHT_FACTOR`).
|
|
33104
|
+
* - A flowed `view` optionally paints its `background`, indents the x cursor by
|
|
33105
|
+
* its `padding`, lays its children out in the same top-down flow, then
|
|
33106
|
+
* restores x and advances y past the consumed block (plus padding).
|
|
33107
|
+
* - **Absolute positioning:** if an element supplies an explicit `x` *and* `y`,
|
|
33108
|
+
* it is placed at exactly that PDF coordinate (y-up, measured from the page
|
|
33109
|
+
* bottom) and does **not** consume or advance the flow cursor. A `rect`
|
|
33110
|
+
* (which requires explicit `x`/`y`) is therefore always absolute.
|
|
33111
|
+
* - Each `page` element resets both cursors and starts a fresh PDF page.
|
|
33112
|
+
*
|
|
33113
|
+
* Anything the simple model cannot express (wrapping across pages, automatic
|
|
33114
|
+
* text reflow, percentage sizing, z-index) is intentionally out of scope.
|
|
33115
|
+
*
|
|
33116
|
+
* @packageDocumentation
|
|
33117
|
+
*/
|
|
33118
|
+
/** Top margin (points) where the flow cursor starts on every page. */
|
|
33119
|
+
const MARGIN_TOP = 50;
|
|
33120
|
+
/** Left margin (points) where the flow cursor starts on every page. */
|
|
33121
|
+
const MARGIN_LEFT = 50;
|
|
33122
|
+
/** Default font size (points) for `text` when `size` is not supplied. */
|
|
33123
|
+
const DEFAULT_FONT_SIZE = 12;
|
|
33124
|
+
/** Line-height multiplier applied to a text line's font size. */
|
|
33125
|
+
const LINE_HEIGHT_FACTOR = 1.2;
|
|
33126
|
+
/**
|
|
33127
|
+
* Recursively flatten a children argument list into a flat `PdfNode[]`,
|
|
33128
|
+
* splatting nested arrays (which JSX produces for `{items.map(...)}`).
|
|
33129
|
+
*/
|
|
33130
|
+
function flattenChildren(children) {
|
|
33131
|
+
const out = [];
|
|
33132
|
+
const visit = (node) => {
|
|
33133
|
+
if (Array.isArray(node)) for (const item of node) visit(item);
|
|
33134
|
+
else out.push(node);
|
|
33135
|
+
};
|
|
33136
|
+
visit(children);
|
|
33137
|
+
return out;
|
|
33138
|
+
}
|
|
33139
|
+
/**
|
|
33140
|
+
* Classic hyperscript pragma (`jsxFactory: "h"`).
|
|
33141
|
+
*
|
|
33142
|
+
* @param type Intrinsic tag name or a function component.
|
|
33143
|
+
* @param props Props object, or `null` for no props.
|
|
33144
|
+
* @param children Zero or more child nodes (nested arrays are flattened).
|
|
33145
|
+
* @returns A {@link PdfElement}.
|
|
33146
|
+
*/
|
|
33147
|
+
function h(type, props, ...children) {
|
|
33148
|
+
return {
|
|
33149
|
+
type,
|
|
33150
|
+
props: props ?? {},
|
|
33151
|
+
children: flattenChildren(children)
|
|
33152
|
+
};
|
|
33153
|
+
}
|
|
33154
|
+
/**
|
|
33155
|
+
* Extract the flattened children from an automatic-runtime props object.
|
|
33156
|
+
* The JSX automatic runtime passes children inside `props.children`.
|
|
33157
|
+
*/
|
|
33158
|
+
function childrenFromProps(props) {
|
|
33159
|
+
if (!("children" in props)) return {
|
|
33160
|
+
rest: props,
|
|
33161
|
+
children: []
|
|
33162
|
+
};
|
|
33163
|
+
const { children, ...rest } = props;
|
|
33164
|
+
return {
|
|
33165
|
+
rest,
|
|
33166
|
+
children: flattenChildren(children)
|
|
33167
|
+
};
|
|
33168
|
+
}
|
|
33169
|
+
/**
|
|
33170
|
+
* Automatic-runtime factory for elements with a single (or no) child.
|
|
33171
|
+
*
|
|
33172
|
+
* @param type Intrinsic tag name or a function component.
|
|
33173
|
+
* @param props Props object; children (if any) live under `props.children`.
|
|
33174
|
+
* @returns A {@link PdfElement}.
|
|
33175
|
+
*/
|
|
33176
|
+
function jsx(type, props) {
|
|
33177
|
+
const { rest, children } = childrenFromProps(props);
|
|
33178
|
+
return {
|
|
33179
|
+
type,
|
|
33180
|
+
props: rest,
|
|
33181
|
+
children
|
|
33182
|
+
};
|
|
33183
|
+
}
|
|
33184
|
+
/**
|
|
33185
|
+
* Automatic-runtime factory for elements with multiple static children.
|
|
33186
|
+
*
|
|
33187
|
+
* Behaviour is identical to {@link jsx}; the JSX transform calls `jsxs` when
|
|
33188
|
+
* the children are a static array.
|
|
33189
|
+
*
|
|
33190
|
+
* @param type Intrinsic tag name or a function component.
|
|
33191
|
+
* @param props Props object; children live under `props.children`.
|
|
33192
|
+
* @returns A {@link PdfElement}.
|
|
33193
|
+
*/
|
|
33194
|
+
function jsxs(type, props) {
|
|
33195
|
+
return jsx(type, props);
|
|
33196
|
+
}
|
|
33197
|
+
/**
|
|
33198
|
+
* Fragment component — groups children without adding a layout box. Used by
|
|
33199
|
+
* the JSX transform for `<>...</>` and directly via `h(Fragment, null, ...)`.
|
|
33200
|
+
*
|
|
33201
|
+
* @param props Props whose `children` are returned for rendering.
|
|
33202
|
+
* @returns The children as a {@link PdfNode} (an array, which the
|
|
33203
|
+
* renderer flattens transparently).
|
|
33204
|
+
*/
|
|
33205
|
+
const Fragment = (props) => {
|
|
33206
|
+
return props["children"] ?? null;
|
|
33207
|
+
};
|
|
33208
|
+
/** Number prop reader that tolerates absent / non-number values. */
|
|
33209
|
+
function numProp(props, key) {
|
|
33210
|
+
const v = props[key];
|
|
33211
|
+
return typeof v === "number" ? v : void 0;
|
|
33212
|
+
}
|
|
33213
|
+
/** Resolve a `PageSize`-ish value into a concrete `[width, height]`. */
|
|
33214
|
+
function resolvePageSize(value, fallback) {
|
|
33215
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") return [value[0], value[1]];
|
|
33216
|
+
if (value && typeof value === "object" && typeof value.width === "number" && typeof value.height === "number") return value;
|
|
33217
|
+
return fallback;
|
|
33218
|
+
}
|
|
33219
|
+
/**
|
|
33220
|
+
* Normalize an element/component (or a raw array produced by a component such
|
|
33221
|
+
* as {@link Fragment}) into a concrete, flattened array of child nodes.
|
|
33222
|
+
*/
|
|
33223
|
+
function resolveNode(input) {
|
|
33224
|
+
if (Array.isArray(input)) return flattenChildren(input);
|
|
33225
|
+
const node = input;
|
|
33226
|
+
if (node === null || node === void 0 || typeof node === "boolean") return [];
|
|
33227
|
+
if (typeof node === "string" || typeof node === "number") return [node];
|
|
33228
|
+
if (typeof node.type === "function") {
|
|
33229
|
+
const componentProps = {
|
|
33230
|
+
...node.props,
|
|
33231
|
+
children: node.children
|
|
33232
|
+
};
|
|
33233
|
+
return flattenChildren(node.type(componentProps));
|
|
33234
|
+
}
|
|
33235
|
+
return [node];
|
|
33236
|
+
}
|
|
33237
|
+
/**
|
|
33238
|
+
* Render context shared across a single document render: holds the document,
|
|
33239
|
+
* a lazily-embedded default + bold font, and the active page/cursor.
|
|
33240
|
+
*/
|
|
33241
|
+
var RenderContext = class {
|
|
33242
|
+
doc;
|
|
33243
|
+
regular;
|
|
33244
|
+
bold;
|
|
33245
|
+
constructor(doc) {
|
|
33246
|
+
this.doc = doc;
|
|
33247
|
+
}
|
|
33248
|
+
async init() {
|
|
33249
|
+
this.regular = await this.doc.embedFont("Helvetica");
|
|
33250
|
+
this.bold = await this.doc.embedFont("Helvetica-Bold");
|
|
33251
|
+
}
|
|
33252
|
+
font(bold) {
|
|
33253
|
+
const f = bold ? this.bold : this.regular;
|
|
33254
|
+
if (f === void 0) throw new Error("RenderContext.init() must be awaited before rendering.");
|
|
33255
|
+
return f;
|
|
33256
|
+
}
|
|
33257
|
+
};
|
|
33258
|
+
/**
|
|
33259
|
+
* Draw a single text line. Returns the height consumed in the flow (0 when the
|
|
33260
|
+
* element was absolutely positioned and therefore does not advance the flow).
|
|
33261
|
+
*/
|
|
33262
|
+
function renderText(ctx, page, cursor, el) {
|
|
33263
|
+
const props = el.props;
|
|
33264
|
+
const size = numProp(props, "size") ?? DEFAULT_FONT_SIZE;
|
|
33265
|
+
const bold = props["bold"] === true;
|
|
33266
|
+
const color = props["color"];
|
|
33267
|
+
const font = ctx.font(bold);
|
|
33268
|
+
const text = el.children.filter((c) => typeof c === "string" || typeof c === "number").map((c) => String(c)).join("");
|
|
33269
|
+
const absX = numProp(props, "x");
|
|
33270
|
+
const absY = numProp(props, "y");
|
|
33271
|
+
const absolute = absX !== void 0 && absY !== void 0;
|
|
33272
|
+
const lineHeight = size * LINE_HEIGHT_FACTOR;
|
|
33273
|
+
if (absolute) {
|
|
33274
|
+
page.drawText(text, {
|
|
33275
|
+
x: absX,
|
|
33276
|
+
y: absY,
|
|
33277
|
+
size,
|
|
33278
|
+
font,
|
|
33279
|
+
...color !== void 0 ? { color } : {}
|
|
33280
|
+
});
|
|
33281
|
+
return 0;
|
|
33282
|
+
}
|
|
33283
|
+
const baseline = cursor.y - size;
|
|
33284
|
+
page.drawText(text, {
|
|
33285
|
+
x: cursor.x,
|
|
33286
|
+
y: baseline,
|
|
33287
|
+
size,
|
|
33288
|
+
font,
|
|
33289
|
+
...color !== void 0 ? { color } : {}
|
|
33290
|
+
});
|
|
33291
|
+
cursor.y -= lineHeight;
|
|
33292
|
+
return lineHeight;
|
|
33293
|
+
}
|
|
33294
|
+
/**
|
|
33295
|
+
* Draw a rectangle. `rect` is always absolutely positioned (x/y required) and
|
|
33296
|
+
* therefore never advances the flow cursor.
|
|
33297
|
+
*/
|
|
33298
|
+
function renderRect(page, el) {
|
|
33299
|
+
const props = el.props;
|
|
33300
|
+
const x = numProp(props, "x") ?? 0;
|
|
33301
|
+
const y = numProp(props, "y") ?? 0;
|
|
33302
|
+
const width = numProp(props, "width") ?? 0;
|
|
33303
|
+
const height = numProp(props, "height") ?? 0;
|
|
33304
|
+
const color = props["color"];
|
|
33305
|
+
const border = props["border"];
|
|
33306
|
+
page.drawRectangle({
|
|
33307
|
+
x,
|
|
33308
|
+
y,
|
|
33309
|
+
width,
|
|
33310
|
+
height,
|
|
33311
|
+
...color !== void 0 ? { color } : {},
|
|
33312
|
+
...border !== void 0 ? {
|
|
33313
|
+
borderColor: border,
|
|
33314
|
+
borderWidth: 1
|
|
33315
|
+
} : {}
|
|
33316
|
+
});
|
|
33317
|
+
}
|
|
33318
|
+
/**
|
|
33319
|
+
* Render a `view` block container. Returns the height consumed in the flow
|
|
33320
|
+
* (0 when absolutely positioned).
|
|
33321
|
+
*/
|
|
33322
|
+
function renderView(ctx, page, cursor, el) {
|
|
33323
|
+
const props = el.props;
|
|
33324
|
+
const padding = numProp(props, "padding") ?? 0;
|
|
33325
|
+
const background = props["background"];
|
|
33326
|
+
const absX = numProp(props, "x");
|
|
33327
|
+
const absY = numProp(props, "y");
|
|
33328
|
+
const absolute = absX !== void 0 && absY !== void 0;
|
|
33329
|
+
const explicitWidth = numProp(props, "width");
|
|
33330
|
+
const explicitHeight = numProp(props, "height");
|
|
33331
|
+
if (absolute) {
|
|
33332
|
+
const localTop = absY + (explicitHeight ?? 0);
|
|
33333
|
+
const local = {
|
|
33334
|
+
x: absX + padding,
|
|
33335
|
+
y: localTop - padding
|
|
33336
|
+
};
|
|
33337
|
+
if (background !== void 0 && explicitWidth !== void 0 && explicitHeight !== void 0) page.drawRectangle({
|
|
33338
|
+
x: absX,
|
|
33339
|
+
y: absY,
|
|
33340
|
+
width: explicitWidth,
|
|
33341
|
+
height: explicitHeight,
|
|
33342
|
+
color: background
|
|
33343
|
+
});
|
|
33344
|
+
renderChildren(ctx, page, local, el.children);
|
|
33345
|
+
return 0;
|
|
33346
|
+
}
|
|
33347
|
+
const blockTop = cursor.y;
|
|
33348
|
+
const innerCursor = {
|
|
33349
|
+
x: cursor.x + padding,
|
|
33350
|
+
y: cursor.y - padding
|
|
33351
|
+
};
|
|
33352
|
+
renderChildren(ctx, page, innerCursor, el.children);
|
|
33353
|
+
const contentConsumed = cursor.y - padding - innerCursor.y;
|
|
33354
|
+
const blockHeight = explicitHeight ?? contentConsumed + padding * 2;
|
|
33355
|
+
if (background !== void 0) {
|
|
33356
|
+
const blockWidth = explicitWidth ?? page.width - cursor.x - MARGIN_LEFT;
|
|
33357
|
+
page.drawRectangle({
|
|
33358
|
+
x: cursor.x,
|
|
33359
|
+
y: blockTop - blockHeight,
|
|
33360
|
+
width: blockWidth,
|
|
33361
|
+
height: blockHeight,
|
|
33362
|
+
color: background
|
|
33363
|
+
});
|
|
33364
|
+
}
|
|
33365
|
+
cursor.y = blockTop - blockHeight;
|
|
33366
|
+
return blockHeight;
|
|
33367
|
+
}
|
|
33368
|
+
/** Render a flat list of children in top-down flow on the current page. */
|
|
33369
|
+
function renderChildren(ctx, page, cursor, children) {
|
|
33370
|
+
for (const child of children) for (const resolved of resolveNode(child)) renderFlowNode(ctx, page, cursor, resolved);
|
|
33371
|
+
}
|
|
33372
|
+
/** Render one already-resolved node into the current page's flow. */
|
|
33373
|
+
function renderFlowNode(ctx, page, cursor, node) {
|
|
33374
|
+
if (node === null || node === void 0 || typeof node === "boolean") return;
|
|
33375
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
33376
|
+
const size = DEFAULT_FONT_SIZE;
|
|
33377
|
+
const baseline = cursor.y - size;
|
|
33378
|
+
page.drawText(String(node), {
|
|
33379
|
+
x: cursor.x,
|
|
33380
|
+
y: baseline,
|
|
33381
|
+
size,
|
|
33382
|
+
font: ctx.font(false)
|
|
33383
|
+
});
|
|
33384
|
+
cursor.y -= size * LINE_HEIGHT_FACTOR;
|
|
33385
|
+
return;
|
|
33386
|
+
}
|
|
33387
|
+
if (typeof node.type === "function") {
|
|
33388
|
+
for (const resolved of resolveNode(node)) renderFlowNode(ctx, page, cursor, resolved);
|
|
33389
|
+
return;
|
|
33390
|
+
}
|
|
33391
|
+
switch (node.type) {
|
|
33392
|
+
case "text":
|
|
33393
|
+
renderText(ctx, page, cursor, node);
|
|
33394
|
+
return;
|
|
33395
|
+
case "view":
|
|
33396
|
+
renderView(ctx, page, cursor, node);
|
|
33397
|
+
return;
|
|
33398
|
+
case "rect":
|
|
33399
|
+
renderRect(page, node);
|
|
33400
|
+
return;
|
|
33401
|
+
case "page":
|
|
33402
|
+
case "document":
|
|
33403
|
+
renderChildren(ctx, page, cursor, node.children);
|
|
33404
|
+
return;
|
|
33405
|
+
default:
|
|
33406
|
+
renderChildren(ctx, page, cursor, node.children);
|
|
33407
|
+
return;
|
|
33408
|
+
}
|
|
33409
|
+
}
|
|
33410
|
+
/**
|
|
33411
|
+
* Collect the `page` elements from a `document`'s children, resolving any
|
|
33412
|
+
* function components / fragments that wrap them.
|
|
33413
|
+
*/
|
|
33414
|
+
function collectPages(children) {
|
|
33415
|
+
const pages = [];
|
|
33416
|
+
const visit = (node) => {
|
|
33417
|
+
for (const resolved of resolveNode(node)) {
|
|
33418
|
+
if (resolved === null || resolved === void 0 || typeof resolved === "boolean" || typeof resolved === "string" || typeof resolved === "number") continue;
|
|
33419
|
+
if (typeof resolved.type === "function") {
|
|
33420
|
+
visit(resolved);
|
|
33421
|
+
continue;
|
|
33422
|
+
}
|
|
33423
|
+
if (resolved.type === "page") pages.push(resolved);
|
|
33424
|
+
else for (const c of resolved.children) visit(c);
|
|
33425
|
+
}
|
|
33426
|
+
};
|
|
33427
|
+
for (const child of children) visit(child);
|
|
33428
|
+
return pages;
|
|
33429
|
+
}
|
|
33430
|
+
/**
|
|
33431
|
+
* Render a declarative element tree to a PDF document.
|
|
33432
|
+
*
|
|
33433
|
+
* The `root` should be a `document` element (or a component / fragment that
|
|
33434
|
+
* resolves to one). Each `page` child becomes a PDF page; content flows
|
|
33435
|
+
* top-down within each page per the layout model documented in the module
|
|
33436
|
+
* header.
|
|
33437
|
+
*
|
|
33438
|
+
* @param root The root {@link PdfNode} (typically a `document` element).
|
|
33439
|
+
* @returns The serialized PDF as a `Uint8Array` (begins with `"%PDF-"`).
|
|
33440
|
+
*
|
|
33441
|
+
* @example
|
|
33442
|
+
* ```ts
|
|
33443
|
+
* const bytes = await renderToPdf(
|
|
33444
|
+
* h('document', { size: PageSizes.A4 },
|
|
33445
|
+
* h('page', null, h('text', { size: 24 }, 'Hello'), h('text', null, 'World')),
|
|
33446
|
+
* ),
|
|
33447
|
+
* );
|
|
33448
|
+
* ```
|
|
33449
|
+
*/
|
|
33450
|
+
async function renderToPdf(root) {
|
|
33451
|
+
let documentEl;
|
|
33452
|
+
const stack = resolveNode(root);
|
|
33453
|
+
while (stack.length > 0) {
|
|
33454
|
+
const node = stack.shift();
|
|
33455
|
+
if (node === null || node === void 0 || typeof node === "boolean" || typeof node === "string" || typeof node === "number") continue;
|
|
33456
|
+
if (typeof node.type === "function") {
|
|
33457
|
+
stack.unshift(...resolveNode(node));
|
|
33458
|
+
continue;
|
|
33459
|
+
}
|
|
33460
|
+
if (node.type === "document") {
|
|
33461
|
+
documentEl = node;
|
|
33462
|
+
break;
|
|
33463
|
+
}
|
|
33464
|
+
documentEl = {
|
|
33465
|
+
type: "document",
|
|
33466
|
+
props: {},
|
|
33467
|
+
children: [node]
|
|
33468
|
+
};
|
|
33469
|
+
break;
|
|
33470
|
+
}
|
|
33471
|
+
const doc = createPdf();
|
|
33472
|
+
const ctx = new RenderContext(doc);
|
|
33473
|
+
await ctx.init();
|
|
33474
|
+
const docSize = resolvePageSize(documentEl?.props["size"], PageSizes.A4);
|
|
33475
|
+
const pageEls = documentEl ? collectPages(documentEl.children) : [];
|
|
33476
|
+
if (pageEls.length === 0) doc.addPage(docSize);
|
|
33477
|
+
for (const pageEl of pageEls) {
|
|
33478
|
+
const pageSize = resolvePageSize(pageEl.props["size"], docSize);
|
|
33479
|
+
const page = doc.addPage(pageSize);
|
|
33480
|
+
renderChildren(ctx, page, {
|
|
33481
|
+
x: MARGIN_LEFT,
|
|
33482
|
+
y: page.height - MARGIN_TOP
|
|
33483
|
+
}, pageEl.children);
|
|
33484
|
+
}
|
|
33485
|
+
return doc.save();
|
|
33486
|
+
}
|
|
33487
|
+
//#endregion
|
|
33488
|
+
//#region src/form/schemaForm.ts
|
|
33489
|
+
/**
|
|
33490
|
+
* @module form/schemaForm
|
|
33491
|
+
*
|
|
33492
|
+
* Generate a fillable AcroForm PDF directly from a JSON-Schema-like
|
|
33493
|
+
* description. Only the subset of JSON Schema that maps cleanly onto
|
|
33494
|
+
* PDF interactive form (AcroForm) field types is supported:
|
|
33495
|
+
*
|
|
33496
|
+
* | JSON Schema | AcroForm field |
|
|
33497
|
+
* | --------------------------------- | ------------------------- |
|
|
33498
|
+
* | `type: 'string'` | text field |
|
|
33499
|
+
* | `type: 'string'` + `enum: [...]` | dropdown (combo box) |
|
|
33500
|
+
* | `type: 'boolean'` | checkbox |
|
|
33501
|
+
* | `type: 'number'` / `'integer'` | text field |
|
|
33502
|
+
* | `type: 'object'` / `'array'` | text field (placeholder) |
|
|
33503
|
+
* | anything else / no `type` | text field (placeholder) |
|
|
33504
|
+
*
|
|
33505
|
+
* ### Documented limitations (intentional, honest scope)
|
|
33506
|
+
*
|
|
33507
|
+
* - **Nested objects and arrays are NOT recursed.** A property whose
|
|
33508
|
+
* `type` is `'object'` or `'array'` becomes a single flat text-field
|
|
33509
|
+
* placeholder labelled with the property name. Sub-properties of a
|
|
33510
|
+
* nested object are ignored. (Recursive / repeatable structures have
|
|
33511
|
+
* no first-class AcroForm representation.)
|
|
33512
|
+
* - **Unsupported / unknown `type` keywords** (and properties with no
|
|
33513
|
+
* `type` at all) also degrade gracefully to a plain text field, so the
|
|
33514
|
+
* generated document is always a valid, fillable PDF.
|
|
33515
|
+
* - Schema validation keywords (`minLength`, `pattern`, `minimum`,
|
|
33516
|
+
* `multipleOf`, `oneOf`, `anyOf`, `$ref`, …) are **not** enforced; they
|
|
33517
|
+
* are simply ignored. Only `type`, `properties`, `required`, `enum`,
|
|
33518
|
+
* and `title` influence the output.
|
|
33519
|
+
* - `enum` is honoured only for `type: 'string'`. Enum values of other
|
|
33520
|
+
* types fall through to their normal mapping.
|
|
33521
|
+
*
|
|
33522
|
+
* The returned {@link PdfDocument} is **not** saved — call `doc.save()`
|
|
33523
|
+
* (or any of the `saveAs*` variants) on it yourself.
|
|
33524
|
+
*
|
|
33525
|
+
* @example
|
|
33526
|
+
* ```ts
|
|
33527
|
+
* import { buildFormFromJsonSchema } from 'modern-pdf-lib/form/schemaForm';
|
|
33528
|
+
*
|
|
33529
|
+
* const { doc, fields } = buildFormFromJsonSchema({
|
|
33530
|
+
* type: 'object',
|
|
33531
|
+
* title: 'Signup',
|
|
33532
|
+
* properties: {
|
|
33533
|
+
* fullName: { type: 'string', title: 'Full Name' },
|
|
33534
|
+
* subscribe: { type: 'boolean' },
|
|
33535
|
+
* country: { type: 'string', enum: ['US', 'CA', 'MX'] },
|
|
33536
|
+
* },
|
|
33537
|
+
* required: ['fullName'],
|
|
33538
|
+
* });
|
|
33539
|
+
*
|
|
33540
|
+
* const bytes = await doc.save();
|
|
33541
|
+
* ```
|
|
33542
|
+
*/
|
|
33543
|
+
/** Vertical space (points) allotted to each row (field + gap). */
|
|
33544
|
+
const ROW_HEIGHT = 34;
|
|
33545
|
+
/** Height (points) of a text / dropdown widget. */
|
|
33546
|
+
const FIELD_HEIGHT = 20;
|
|
33547
|
+
/** Side margin (points). */
|
|
33548
|
+
const MARGIN = 48;
|
|
33549
|
+
/** Top margin (points) below which fields start on each page. */
|
|
33550
|
+
const TOP_MARGIN = 56;
|
|
33551
|
+
/** Bottom margin (points); fields below this trigger a page break. */
|
|
33552
|
+
const BOTTOM_MARGIN = 48;
|
|
33553
|
+
/** Default label-column width (points). */
|
|
33554
|
+
const DEFAULT_LABEL_WIDTH = 160;
|
|
33555
|
+
/** Body font size for labels. */
|
|
33556
|
+
const LABEL_SIZE = 11;
|
|
33557
|
+
/** Title font size. */
|
|
33558
|
+
const TITLE_SIZE = 18;
|
|
33559
|
+
/**
|
|
33560
|
+
* Determine which AcroForm field kind a property schema maps to.
|
|
33561
|
+
*
|
|
33562
|
+
* @returns The field kind plus, for dropdowns, the list of options.
|
|
33563
|
+
*/
|
|
33564
|
+
function mapPropertyToKind(prop) {
|
|
33565
|
+
const type = prop.type;
|
|
33566
|
+
if (type === "boolean") return {
|
|
33567
|
+
kind: "checkbox",
|
|
33568
|
+
options: []
|
|
33569
|
+
};
|
|
33570
|
+
if (type === "string" && Array.isArray(prop.enum) && prop.enum.length > 0) return {
|
|
33571
|
+
kind: "dropdown",
|
|
33572
|
+
options: prop.enum.map((v) => String(v))
|
|
33573
|
+
};
|
|
33574
|
+
return {
|
|
33575
|
+
kind: "text",
|
|
33576
|
+
options: []
|
|
33577
|
+
};
|
|
33578
|
+
}
|
|
33579
|
+
/**
|
|
33580
|
+
* Internal plugin that wires a freshly-built form into the document at
|
|
33581
|
+
* save time. It runs after pages are finalized so it can:
|
|
33582
|
+
*
|
|
33583
|
+
* - append each field's (pre-registered) indirect reference to its
|
|
33584
|
+
* page's `/Annots` array (`onBuildPageDict`), and
|
|
33585
|
+
* - register the `/AcroForm` dictionary on the catalog with a `/DR`
|
|
33586
|
+
* default-resources entry and `/NeedAppearances true` so viewers
|
|
33587
|
+
* generate the field appearances (`onBuildCatalog`).
|
|
33588
|
+
*
|
|
33589
|
+
* This avoids editing any shared document code: everything needed to
|
|
33590
|
+
* make `getForm()` fields round-trip is performed through the public
|
|
33591
|
+
* plugin hooks.
|
|
33592
|
+
*/
|
|
33593
|
+
function createAcroFormWiringPlugin(acroFormDict, acroFormRef, fieldRefsByPage) {
|
|
33594
|
+
return {
|
|
33595
|
+
name: "modern-pdf-lib:schemaForm-acroform-wiring",
|
|
33596
|
+
version: "1.0.0",
|
|
33597
|
+
onBuildPageDict(pageDict, pageIndex) {
|
|
33598
|
+
const refs = fieldRefsByPage.get(pageIndex);
|
|
33599
|
+
if (refs === void 0 || refs.length === 0) return;
|
|
33600
|
+
const existing = pageDict.get("/Annots");
|
|
33601
|
+
if (existing !== void 0 && existing.kind === "array") for (const ref of refs) existing.push(ref);
|
|
33602
|
+
else pageDict.set("/Annots", PdfArray.of([...refs]));
|
|
33603
|
+
},
|
|
33604
|
+
onBuildCatalog(catalog) {
|
|
33605
|
+
if (!acroFormDict.has("/DR")) {
|
|
33606
|
+
const helvetica = new PdfDict();
|
|
33607
|
+
helvetica.set("/Type", PdfName.of("Font"));
|
|
33608
|
+
helvetica.set("/Subtype", PdfName.of("Type1"));
|
|
33609
|
+
helvetica.set("/BaseFont", PdfName.of("Helvetica"));
|
|
33610
|
+
const fontDict = new PdfDict();
|
|
33611
|
+
fontDict.set("/Helv", helvetica);
|
|
33612
|
+
const resources = new PdfDict();
|
|
33613
|
+
resources.set("/Font", fontDict);
|
|
33614
|
+
acroFormDict.set("/DR", resources);
|
|
33615
|
+
}
|
|
33616
|
+
if (!acroFormDict.has("/DA")) acroFormDict.set("/DA", PdfString.literal("/Helv 0 Tf 0 g"));
|
|
33617
|
+
acroFormDict.set("/NeedAppearances", PdfBool.TRUE);
|
|
33618
|
+
catalog.set("/AcroForm", acroFormRef);
|
|
33619
|
+
}
|
|
33620
|
+
};
|
|
33621
|
+
}
|
|
33622
|
+
/**
|
|
33623
|
+
* Build a fillable AcroForm PDF from a JSON-Schema-like description.
|
|
33624
|
+
*
|
|
33625
|
+
* For an object schema, the `properties` are iterated in insertion
|
|
33626
|
+
* order and one labelled form field is emitted per property. Fields
|
|
33627
|
+
* are stacked top-down; when the vertical cursor runs off the page a
|
|
33628
|
+
* new page is added and the cursor reset. Required fields (listed in
|
|
33629
|
+
* the schema's `required` array) are marked with a trailing asterisk in
|
|
33630
|
+
* their label.
|
|
33631
|
+
*
|
|
33632
|
+
* @param schema The JSON-Schema-like description.
|
|
33633
|
+
* @param options Optional layout options.
|
|
33634
|
+
* @returns The generated (unsaved) {@link PdfDocument} and the
|
|
33635
|
+
* list of generated field descriptors.
|
|
33636
|
+
*/
|
|
33637
|
+
function buildFormFromJsonSchema(schema, options) {
|
|
33638
|
+
const doc = createPdf();
|
|
33639
|
+
const form = doc.getForm();
|
|
33640
|
+
const pageSize = options?.pageSize ?? [PageSizes.A4[0], PageSizes.A4[1]];
|
|
33641
|
+
const [pageWidth, pageHeight] = pageSize;
|
|
33642
|
+
const labelWidth = options?.labelWidth !== void 0 ? options.labelWidth : DEFAULT_LABEL_WIDTH;
|
|
33643
|
+
const fields = [];
|
|
33644
|
+
const fieldRefsByPage = /* @__PURE__ */ new Map();
|
|
33645
|
+
const registry = doc.getRegistry();
|
|
33646
|
+
const acroFormDict = form.toDict(registry);
|
|
33647
|
+
const acroFormRef = registry.register(acroFormDict);
|
|
33648
|
+
const fieldRefs = [];
|
|
33649
|
+
let pageIndex = -1;
|
|
33650
|
+
let cursorY = 0;
|
|
33651
|
+
const fieldX = MARGIN + labelWidth;
|
|
33652
|
+
const fieldWidth = Math.max(40, pageWidth - fieldX - MARGIN);
|
|
33653
|
+
/** Start a fresh page and reset the vertical cursor. */
|
|
33654
|
+
const newPage = () => {
|
|
33655
|
+
doc.addPage(pageSize);
|
|
33656
|
+
pageIndex += 1;
|
|
33657
|
+
cursorY = pageHeight - TOP_MARGIN;
|
|
33658
|
+
};
|
|
33659
|
+
newPage();
|
|
33660
|
+
const title = options?.title ?? schema.title;
|
|
33661
|
+
if (title !== void 0 && title.length > 0) {
|
|
33662
|
+
doc.getPage(pageIndex).drawText(title, {
|
|
33663
|
+
x: MARGIN,
|
|
33664
|
+
y: cursorY,
|
|
33665
|
+
size: TITLE_SIZE,
|
|
33666
|
+
color: rgb(0, 0, 0)
|
|
33667
|
+
});
|
|
33668
|
+
cursorY -= 36;
|
|
33669
|
+
}
|
|
33670
|
+
const properties = schema.type === "object" ? schema.properties : void 0;
|
|
33671
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
33672
|
+
if (properties !== void 0) for (const [name, propSchema] of Object.entries(properties)) {
|
|
33673
|
+
const prop = propSchema ?? {};
|
|
33674
|
+
const { kind, options: dropdownOptions } = mapPropertyToKind(prop);
|
|
33675
|
+
if (cursorY - ROW_HEIGHT < BOTTOM_MARGIN) newPage();
|
|
33676
|
+
const page = doc.getPage(pageIndex);
|
|
33677
|
+
const labelText = prop.title ?? name;
|
|
33678
|
+
const required = requiredSet.has(name);
|
|
33679
|
+
const label = required ? `${labelText} *` : labelText;
|
|
33680
|
+
page.drawText(label, {
|
|
33681
|
+
x: MARGIN,
|
|
33682
|
+
y: cursorY - FIELD_HEIGHT + 6,
|
|
33683
|
+
size: LABEL_SIZE,
|
|
33684
|
+
color: required ? rgb(.6, 0, 0) : rgb(.1, .1, .1)
|
|
33685
|
+
});
|
|
33686
|
+
const y1 = cursorY - FIELD_HEIGHT;
|
|
33687
|
+
const y2 = cursorY;
|
|
33688
|
+
let createdDict;
|
|
33689
|
+
if (kind === "checkbox") {
|
|
33690
|
+
const cbSize = FIELD_HEIGHT;
|
|
33691
|
+
createdDict = form.createCheckbox(name, pageIndex, [
|
|
33692
|
+
fieldX,
|
|
33693
|
+
y1,
|
|
33694
|
+
fieldX + cbSize,
|
|
33695
|
+
y2
|
|
33696
|
+
]).getDict();
|
|
33697
|
+
} else if (kind === "dropdown") createdDict = form.createDropdown(name, pageIndex, [
|
|
33698
|
+
fieldX,
|
|
33699
|
+
y1,
|
|
33700
|
+
fieldX + fieldWidth,
|
|
33701
|
+
y2
|
|
33702
|
+
], dropdownOptions).getDict();
|
|
33703
|
+
else {
|
|
33704
|
+
createdDict = form.createTextField(name, pageIndex, [
|
|
33705
|
+
fieldX,
|
|
33706
|
+
y1,
|
|
33707
|
+
fieldX + fieldWidth,
|
|
33708
|
+
y2
|
|
33709
|
+
]).getDict();
|
|
33710
|
+
page.drawLine({
|
|
33711
|
+
start: {
|
|
33712
|
+
x: fieldX,
|
|
33713
|
+
y: y1
|
|
33714
|
+
},
|
|
33715
|
+
end: {
|
|
33716
|
+
x: fieldX + fieldWidth,
|
|
33717
|
+
y: y1
|
|
33718
|
+
},
|
|
33719
|
+
thickness: .5,
|
|
33720
|
+
color: rgb(.6, .6, .6)
|
|
33721
|
+
});
|
|
33722
|
+
}
|
|
33723
|
+
const ref = registry.register(createdDict);
|
|
33724
|
+
fieldRefs.push(ref);
|
|
33725
|
+
const pageRefs = fieldRefsByPage.get(pageIndex) ?? [];
|
|
33726
|
+
pageRefs.push(ref);
|
|
33727
|
+
fieldRefsByPage.set(pageIndex, pageRefs);
|
|
33728
|
+
fields.push({
|
|
33729
|
+
name,
|
|
33730
|
+
kind
|
|
33731
|
+
});
|
|
33732
|
+
cursorY -= ROW_HEIGHT;
|
|
33733
|
+
}
|
|
33734
|
+
acroFormDict.set("/Fields", PdfArray.of([...fieldRefs]));
|
|
33735
|
+
doc.use(createAcroFormWiringPlugin(acroFormDict, acroFormRef, fieldRefsByPage));
|
|
33736
|
+
return {
|
|
33737
|
+
doc,
|
|
33738
|
+
fields
|
|
33739
|
+
};
|
|
33740
|
+
}
|
|
33741
|
+
//#endregion
|
|
33742
|
+
//#region src/runtime/serverAdapters.ts
|
|
33743
|
+
/**
|
|
33744
|
+
* Characters that are safe to leave unencoded inside the RFC 5987
|
|
33745
|
+
* `attr-char` production used by `filename*`. Everything else is
|
|
33746
|
+
* percent-encoded from its UTF-8 bytes.
|
|
33747
|
+
*
|
|
33748
|
+
* `attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" /
|
|
33749
|
+
* "." / "^" / "_" / "`" / "|" / "~"` (RFC 5987 §3.2.1)
|
|
33750
|
+
*/
|
|
33751
|
+
const RFC5987_ATTR_CHAR = /[A-Za-z0-9!#$&+\-.^_`|~]/;
|
|
33752
|
+
/** Reusable UTF-8 encoder for the `filename*` byte serialisation. */
|
|
33753
|
+
const utf8Encoder = new TextEncoder();
|
|
33754
|
+
/**
|
|
33755
|
+
* Percent-encode a string as an RFC 5987 `value-chars` sequence: every
|
|
33756
|
+
* byte that is not an `attr-char` is written as `%HH` (upper-case hex)
|
|
33757
|
+
* from its UTF-8 representation.
|
|
33758
|
+
*
|
|
33759
|
+
* @param value The raw (possibly non-ASCII) string.
|
|
33760
|
+
* @returns The `value-chars` portion of an `ext-value`.
|
|
33761
|
+
*/
|
|
33762
|
+
function encodeRfc5987(value) {
|
|
33763
|
+
const bytes = utf8Encoder.encode(value);
|
|
33764
|
+
let out = "";
|
|
33765
|
+
for (const byte of bytes) {
|
|
33766
|
+
const char = String.fromCharCode(byte);
|
|
33767
|
+
if (byte < 128 && RFC5987_ATTR_CHAR.test(char)) out += char;
|
|
33768
|
+
else out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
33769
|
+
}
|
|
33770
|
+
return out;
|
|
33771
|
+
}
|
|
33772
|
+
/**
|
|
33773
|
+
* A small transliteration table mapping common accented Latin letters to
|
|
33774
|
+
* their closest ASCII equivalent. Used only for the *fallback* `filename`
|
|
33775
|
+
* parameter — the authoritative value is always carried by `filename*`.
|
|
33776
|
+
*
|
|
33777
|
+
* This is intentionally minimal: it covers the Latin-1 / Latin Extended-A
|
|
33778
|
+
* letters most likely to appear in document names so that legacy clients
|
|
33779
|
+
* (which ignore `filename*`) still receive a readable name.
|
|
33780
|
+
*/
|
|
33781
|
+
const TRANSLITERATIONS = new Map(Object.entries({
|
|
33782
|
+
à: "a",
|
|
33783
|
+
á: "a",
|
|
33784
|
+
â: "a",
|
|
33785
|
+
ã: "a",
|
|
33786
|
+
ä: "a",
|
|
33787
|
+
å: "a",
|
|
33788
|
+
ā: "a",
|
|
33789
|
+
ă: "a",
|
|
33790
|
+
ą: "a",
|
|
33791
|
+
ç: "c",
|
|
33792
|
+
ć: "c",
|
|
33793
|
+
č: "c",
|
|
33794
|
+
è: "e",
|
|
33795
|
+
é: "e",
|
|
33796
|
+
ê: "e",
|
|
33797
|
+
ë: "e",
|
|
33798
|
+
ē: "e",
|
|
33799
|
+
ė: "e",
|
|
33800
|
+
ę: "e",
|
|
33801
|
+
ě: "e",
|
|
33802
|
+
ì: "i",
|
|
33803
|
+
í: "i",
|
|
33804
|
+
î: "i",
|
|
33805
|
+
ï: "i",
|
|
33806
|
+
ī: "i",
|
|
33807
|
+
į: "i",
|
|
33808
|
+
ñ: "n",
|
|
33809
|
+
ń: "n",
|
|
33810
|
+
ň: "n",
|
|
33811
|
+
ò: "o",
|
|
33812
|
+
ó: "o",
|
|
33813
|
+
ô: "o",
|
|
33814
|
+
õ: "o",
|
|
33815
|
+
ö: "o",
|
|
33816
|
+
ø: "o",
|
|
33817
|
+
ō: "o",
|
|
33818
|
+
ù: "u",
|
|
33819
|
+
ú: "u",
|
|
33820
|
+
û: "u",
|
|
33821
|
+
ü: "u",
|
|
33822
|
+
ū: "u",
|
|
33823
|
+
ů: "u",
|
|
33824
|
+
ý: "y",
|
|
33825
|
+
ÿ: "y",
|
|
33826
|
+
ž: "z",
|
|
33827
|
+
ź: "z",
|
|
33828
|
+
ż: "z",
|
|
33829
|
+
ß: "ss",
|
|
33830
|
+
æ: "ae",
|
|
33831
|
+
œ: "oe",
|
|
33832
|
+
À: "A",
|
|
33833
|
+
Á: "A",
|
|
33834
|
+
Â: "A",
|
|
33835
|
+
Ã: "A",
|
|
33836
|
+
Ä: "A",
|
|
33837
|
+
Å: "A",
|
|
33838
|
+
Ç: "C",
|
|
33839
|
+
È: "E",
|
|
33840
|
+
É: "E",
|
|
33841
|
+
Ê: "E",
|
|
33842
|
+
Ë: "E",
|
|
33843
|
+
Ì: "I",
|
|
33844
|
+
Í: "I",
|
|
33845
|
+
Î: "I",
|
|
33846
|
+
Ï: "I",
|
|
33847
|
+
Ñ: "N",
|
|
33848
|
+
Ò: "O",
|
|
33849
|
+
Ó: "O",
|
|
33850
|
+
Ô: "O",
|
|
33851
|
+
Õ: "O",
|
|
33852
|
+
Ö: "O",
|
|
33853
|
+
Ø: "O",
|
|
33854
|
+
Ù: "U",
|
|
33855
|
+
Ú: "U",
|
|
33856
|
+
Û: "U",
|
|
33857
|
+
Ü: "U",
|
|
33858
|
+
Ý: "Y"
|
|
33859
|
+
}));
|
|
33860
|
+
/**
|
|
33861
|
+
* Produce an ASCII-only fallback for a filename: accented Latin letters
|
|
33862
|
+
* are transliterated, any remaining non-ASCII characters are dropped, and
|
|
33863
|
+
* control characters are stripped. The result is intended to be wrapped
|
|
33864
|
+
* in a `quoted-string` by {@link quoteFilename}.
|
|
33865
|
+
*
|
|
33866
|
+
* @param filename The original, possibly non-ASCII filename.
|
|
33867
|
+
* @returns An ASCII approximation (never empty: falls back to
|
|
33868
|
+
* `"file.pdf"` if nothing survives).
|
|
33869
|
+
*/
|
|
33870
|
+
function asciiFallback(filename) {
|
|
33871
|
+
let out = "";
|
|
33872
|
+
for (const char of filename) {
|
|
33873
|
+
const code = char.codePointAt(0) ?? 0;
|
|
33874
|
+
if (code >= 32 && code < 127) out += char;
|
|
33875
|
+
else {
|
|
33876
|
+
const replacement = TRANSLITERATIONS.get(char);
|
|
33877
|
+
if (replacement !== void 0) out += replacement;
|
|
33878
|
+
}
|
|
33879
|
+
}
|
|
33880
|
+
out = out.trim();
|
|
33881
|
+
return out.length > 0 ? out : "file.pdf";
|
|
33882
|
+
}
|
|
33883
|
+
/**
|
|
33884
|
+
* Escape a string for use inside an HTTP `quoted-string` (RFC 9110 /
|
|
33885
|
+
* RFC 2616): backslash and double-quote are backslash-escaped.
|
|
33886
|
+
*
|
|
33887
|
+
* @param value An ASCII string.
|
|
33888
|
+
* @returns The escaped contents (without the surrounding quotes).
|
|
33889
|
+
*/
|
|
33890
|
+
function quoteFilename(value) {
|
|
33891
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
33892
|
+
}
|
|
33893
|
+
/**
|
|
33894
|
+
* Returns `true` when the string contains only printable ASCII and no
|
|
33895
|
+
* characters that would be illegal in a `quoted-string` filename.
|
|
33896
|
+
*/
|
|
33897
|
+
function isSimpleAscii(filename) {
|
|
33898
|
+
for (const char of filename) {
|
|
33899
|
+
const code = char.codePointAt(0) ?? 0;
|
|
33900
|
+
if (code < 32 || code >= 127) return false;
|
|
33901
|
+
}
|
|
33902
|
+
return true;
|
|
33903
|
+
}
|
|
33904
|
+
/**
|
|
33905
|
+
* Build the value of the `Content-Disposition` header.
|
|
33906
|
+
*
|
|
33907
|
+
* @param download `true` → `attachment`; otherwise `inline`.
|
|
33908
|
+
* @param filename Optional suggested filename.
|
|
33909
|
+
* @returns A complete header value, e.g.
|
|
33910
|
+
* `attachment; filename="report.pdf"` or, for non-ASCII,
|
|
33911
|
+
* `attachment; filename="resume.pdf"; filename*=UTF-8''r%C3%A9sum%C3%A9.pdf`.
|
|
33912
|
+
*/
|
|
33913
|
+
function contentDisposition(download, filename) {
|
|
33914
|
+
const type = download ? "attachment" : "inline";
|
|
33915
|
+
if (filename === void 0 || filename.length === 0) return type;
|
|
33916
|
+
if (isSimpleAscii(filename) && !filename.includes("\"") && !filename.includes("\\")) return `${type}; filename="${filename}"`;
|
|
33917
|
+
const quoted = quoteFilename(asciiFallback(filename));
|
|
33918
|
+
if (isSimpleAscii(filename)) return `${type}; filename="${quoted}"`;
|
|
33919
|
+
return `${type}; filename="${quoted}"; filename*=UTF-8''${encodeRfc5987(filename)}`;
|
|
33920
|
+
}
|
|
33921
|
+
/**
|
|
33922
|
+
* Build the set of HTTP response headers for serving a PDF body.
|
|
33923
|
+
*
|
|
33924
|
+
* Always includes `Content-Type: application/pdf`, a numeric
|
|
33925
|
+
* `Content-Length`, and a `Content-Disposition` (`inline` by default,
|
|
33926
|
+
* `attachment` when `options.download` is `true`). `Cache-Control` and
|
|
33927
|
+
* `Last-Modified` are added only when supplied.
|
|
33928
|
+
*
|
|
33929
|
+
* Custom headers from `options.headers` are merged first so the core PDF
|
|
33930
|
+
* headers always win on conflict.
|
|
33931
|
+
*
|
|
33932
|
+
* @param byteLength Length of the PDF body in bytes (the `Content-Length`).
|
|
33933
|
+
* @param options Optional response shaping (see {@link PdfResponseOptions}).
|
|
33934
|
+
* @returns A plain object of header name → value.
|
|
33935
|
+
*/
|
|
33936
|
+
function pdfHeaders(byteLength, options) {
|
|
33937
|
+
const opts = options ?? {};
|
|
33938
|
+
const headers = { ...opts.headers ?? {} };
|
|
33939
|
+
headers["Content-Type"] = "application/pdf";
|
|
33940
|
+
headers["Content-Length"] = String(byteLength);
|
|
33941
|
+
headers["Content-Disposition"] = contentDisposition(opts.download === true, opts.filename);
|
|
33942
|
+
if (opts.cacheControl !== void 0) headers["Cache-Control"] = opts.cacheControl;
|
|
33943
|
+
if (opts.lastModified !== void 0) headers["Last-Modified"] = opts.lastModified.toUTCString();
|
|
33944
|
+
return headers;
|
|
33945
|
+
}
|
|
33946
|
+
/**
|
|
33947
|
+
* Feature-detect the Web-standard `Response` constructor.
|
|
33948
|
+
*
|
|
33949
|
+
* @throws Error when `Response` is not a constructor in the current runtime.
|
|
33950
|
+
*/
|
|
33951
|
+
function assertResponseAvailable() {
|
|
33952
|
+
if (typeof Response !== "function") throw new Error("pdfResponse: the Web-standard `Response` constructor is not available in this runtime. Use `sendPdfToNodeResponse(res, bytes)` for a classic Node `http.ServerResponse`, or upgrade to a runtime with the Fetch API (Node >=18, Deno, Bun, Cloudflare Workers, browsers).");
|
|
33953
|
+
}
|
|
33954
|
+
/**
|
|
33955
|
+
* Build a Web-standard {@link Response} that streams the given PDF bytes
|
|
33956
|
+
* to the client with the correct headers and status code.
|
|
33957
|
+
*
|
|
33958
|
+
* Works in any runtime with the Fetch API: Cloudflare Workers, Deno, Bun,
|
|
33959
|
+
* Node >=18, and browsers (e.g. inside a Service Worker).
|
|
33960
|
+
*
|
|
33961
|
+
* @param bytes The PDF body.
|
|
33962
|
+
* @param options Optional response shaping (see {@link PdfResponseOptions}).
|
|
33963
|
+
* @returns A `Response` with status `options.status ?? 200`.
|
|
33964
|
+
* @throws If the runtime lacks the `Response` constructor.
|
|
33965
|
+
*/
|
|
33966
|
+
function pdfResponse(bytes, options) {
|
|
33967
|
+
assertResponseAvailable();
|
|
33968
|
+
const opts = options ?? {};
|
|
33969
|
+
const headers = pdfHeaders(bytes.byteLength, opts);
|
|
33970
|
+
const body = bytes.slice();
|
|
33971
|
+
return new Response(body, {
|
|
33972
|
+
status: opts.status ?? 200,
|
|
33973
|
+
headers
|
|
33974
|
+
});
|
|
33975
|
+
}
|
|
33976
|
+
/**
|
|
33977
|
+
* Build a streaming Web-standard {@link Response} from a
|
|
33978
|
+
* `ReadableStream` of PDF bytes (e.g. from `doc.saveAsStream()`).
|
|
33979
|
+
*
|
|
33980
|
+
* Because the total size is generally unknown up-front, `Content-Length`
|
|
33981
|
+
* is **omitted** unless `options.byteLength` is supplied — allowing the
|
|
33982
|
+
* runtime to use chunked transfer encoding.
|
|
33983
|
+
*
|
|
33984
|
+
* @param stream A readable stream of the PDF body.
|
|
33985
|
+
* @param options Response shaping plus an optional known `byteLength`.
|
|
33986
|
+
* @returns A streaming `Response`.
|
|
33987
|
+
* @throws If the runtime lacks the `Response` constructor.
|
|
33988
|
+
*/
|
|
33989
|
+
function pdfStreamResponse(stream, options) {
|
|
33990
|
+
assertResponseAvailable();
|
|
33991
|
+
const opts = options ?? {};
|
|
33992
|
+
const headers = opts.byteLength !== void 0 ? pdfHeaders(opts.byteLength, opts) : (() => {
|
|
33993
|
+
const h = pdfHeaders(0, opts);
|
|
33994
|
+
delete h["Content-Length"];
|
|
33995
|
+
return h;
|
|
33996
|
+
})();
|
|
33997
|
+
return new Response(stream, {
|
|
33998
|
+
status: opts.status ?? 200,
|
|
33999
|
+
headers
|
|
34000
|
+
});
|
|
34001
|
+
}
|
|
34002
|
+
/**
|
|
34003
|
+
* Send a PDF body to a classic Node-style response object
|
|
34004
|
+
* (`http.ServerResponse` or Express `res`).
|
|
34005
|
+
*
|
|
34006
|
+
* This calls {@link NodeServerResponseLike.writeHead} with the status and
|
|
34007
|
+
* {@link pdfHeaders}, then {@link NodeServerResponseLike.end} with the body.
|
|
34008
|
+
*
|
|
34009
|
+
* @param res The response object (structural typing — no import).
|
|
34010
|
+
* @param bytes The PDF body.
|
|
34011
|
+
* @param options Optional response shaping (see {@link PdfResponseOptions}).
|
|
34012
|
+
*/
|
|
34013
|
+
function sendPdfToNodeResponse(res, bytes, options) {
|
|
34014
|
+
const opts = options ?? {};
|
|
34015
|
+
const headers = pdfHeaders(bytes.byteLength, opts);
|
|
34016
|
+
res.writeHead(opts.status ?? 200, headers);
|
|
34017
|
+
res.end(bytes);
|
|
34018
|
+
}
|
|
34019
|
+
//#endregion
|
|
32170
34020
|
//#region src/render/matrix.ts
|
|
32171
34021
|
/** The identity transform. */
|
|
32172
34022
|
function identity() {
|
|
@@ -35994,6 +37844,6 @@ async function initWasm(options) {
|
|
|
35994
37844
|
wasmInitialized = true;
|
|
35995
37845
|
}
|
|
35996
37846
|
//#endregion
|
|
35997
|
-
export {
|
|
37847
|
+
export { pdfHeaders as $, buildAfArray as $a, minimalPreset as $i, readWoffHeader as $n, PdfInkAnnotation as $o, buildDPartRoot as $r, resolveBidi as $t, buildSoftMaskNone as A, PdfWorker as Aa, InvalidPageSizeError as Ai, verifySignatureDetailed as An, diffSignedContent as Ao, nameHalftone as Ar, saveIncremental as As, unregisterImageDecoder as At, redactRegions as B, extractJpegMetadata as Ba, formatDate$1 as Bi, setFieldValue as Bn, appendIncrementalUpdate as Bo, buildLab as Br, deviceRgbToXyz as Bt, tagTableHeaderCell as C, code39ToOperators as Ca, ExceededMaxLengthError as Ci, offsetSignedToUnsigned as Cn, findChangedObjects as Co, validateBoxGeometry as Cr, createXmpStream as Cs, feFlood as Ct, buildImageSoftMask as D, encodeCode128 as Da, ForeignPageError as Di, decodeTile as Dn, hasLtvData as Do, buildType1Halftone as Dr, buildSeparationColorSpace as Ds, getImageDecoder as Dt, buildColorKeyMask as E, code128ToOperators as Ea, FontNotEmbeddedError as Ei, assembleTiles as En, embedLtvData as Eo, buildThresholdHalftone as Er, buildDeviceNColorSpace as Es, decodeRegisteredImage as Et, attachAssociatedFiles as F, webpToJpeg as Fa, RichTextFieldReadError as Fi, checkCertificateStatus as Fn, MdpPermission as Fo, reconstructParagraphs as Fr, rgbToHsl as Ft, extractImages as G, optimizeImage as Ga, ellipsisText as Gi, AFDate_FormatEx as Gn, buildTimestampRequest as Go, toSarif as Gr, buildLatticeFormGouraudShading as Gt, compareImages as H, analyzeJpegMarkers as Ha, toAlpha as Hi, setFieldVisibility as Hn, parseExistingTrailer as Ho, DEFAULT_SARIF_TOOL_NAME as Hr, xyzToLab as Ht, registerEmbeddedFile as I, webpToPng as Ia, StreamingParseError as Ii, extractOcspUrl as In, buildDocMdpReference as Io, DEFAULT_DOC_TIMESTAMP_CONTENTS_SIZE as Ir, rgbToHsv as It, renderPageToCanvas as J, enforcePdfAFull as Ja, truncateText as Ji, AFNumber_Format as Jn, searchTextItems as Jo, buildNamespace as Jr, parseColorFont as Jt, generateThumbnail as K, recompressImage as Ka, estimateTextWidth as Ki, formatDate as Kn, parseTimestampResponse as Ko, MATHML_NAMESPACE as Kr, buildTensorPatchShading as Kt, RenderCache as L, convertTiffCmykToRgb as La, UnexpectedFieldTypeError as Li, createSandbox as Ln, getCertificationLevel as Lo, buildDocTimeStampDict as Lr, rgbToLab as Lt, buildUnencryptedWrapper as M, embedTiffDirect as Ma, NoSuchFieldError as Mi, downloadCrl as Mn, buildFieldLockDict as Mo, markdownToPdf as Mr, probeNextGenImage as Mt, attachOutputIntents as N, encodePngFromPixels as Na, PluginError as Ni, extractCrlUrls as Nn, getFieldLocks as No, buildCollection as Nr, hslToRgb as Nt, buildStencilMask as O, encodeCode128Values as Oa, InvalidColorError as Oi, decodeTileRegion as On, addCounterSignature as Oo, buildType5Halftone as Or, ChangeTracker as Os, hasImageDecoder as Ot, buildPageOutputIntent as P, recompressWebP as Pa, RemovePageFromEmptyDocumentError as Pi, isCertificateRevoked as Pn, detectModifications as Po, reconstructLines as Pr, hsvToRgb as Pt, interpretPage as Q, stripProhibitedFeatures as Qa, borderedPreset as Qi, isWoff2 as Qn, PdfRedactAnnotation as Qo, buildRequirements as Qr, reorderVisual as Qt, computeTileGrid as R, embedTiffCmyk as Ra, applyHeaderFooter as Ri, getFieldValue as Rn, setCertificationLevel as Ro, buildCalGray as Rr, rgbToXyz as Rt, tagTableDataCell as S, itfToOperators as Sa, EncryptedPdfError as Si, normalizeComponentDepth as Sn, computeObjectHash as So, buildPdfX6OutputIntent as Sr, buildXmpMetadata as Ss, feComposite as St, buildBlackPointCompensationExtGState as T, encodeCode39 as Ta, FieldExistsAsNonTerminalError as Ti, upscale8To16 as Tn, buildDssDictionary as To, buildSampledTransferFunction as Tr, PDFOperator as Ts, feOffset as Tt, comparePages as U, downscaleImage as Ua, toRoman as Ui, validateFieldValue as Un, saveIncrementalWithSignaturePreservation as Uo, SARIF_SCHEMA_URI as Ur, buildCoonsPatchShading as Ut, applyOcr as V, injectJpegMetadata as Va, replaceTemplateVariables as Vi, addVisibilityAction as Vn, findExistingSignatures as Vo, labToRgb as Vr, parseIccTransform as Vt, extractFonts as W, estimateJpegQuality as Wa, applyOverflow as Wi, AFSpecial_Format as Wn, validateByteRangeIntegrity as Wo, toJsonReport as Wr, buildFreeFormGouraudShading as Wt, renderPageToImage as X, generatePdfAXmpBytes as Xa, applyPreset as Xi, decodeWoff as Xn, PdfCaretAnnotation as Xo, buildPieceInfo as Xr, parseVariableFont as Xt, rasterize as Y, generatePdfAXmp as Ya, wrapText$2 as Yi, formatNumber$1 as Yn, PdfFileAttachmentAnnotation as Yo, buildNamespacesArray as Yr, normalizeAxisCoordinate as Yt, interpretContentStream as Z, countOccurrences as Za, applyTablePreset as Zi, isWoff as Zn, PdfPopupAnnotation as Zo, buildRequirement as Zr, resolveInstanceCoordinates as Zt, tagLink as _, ean13ToOperators as _a, flattenField as _i, buildPdfXOutputIntent as _n, validatePdfA as _o, levenshtein as _r, generateSquigglyAppearance as _s, SharedRingBuffer as _t, assembleFacturX as a, readEan13 as aa, timestampPlugin as ai, extractEmbeddedRevocationData as an, getSupportedLevels as ao, h$1 as ar, PdfSquareAnnotation as as, h as at, tagParagraph as b, encodeEan8 as ba, BatchProcessingError as bi, downscale16To8 as bn, isLinearized as bo, buildBoxDict as br, PdfLinkAnnotation as bs, feColorMatrix as bt, buildWtpdfIdentificationXmp as c, renderStyledBarcode as ca, enforcePdfUa as ci, validateCertificatePolicy as cn, generateWinAnsiToUnicodeCmap as co, resolveFallback as cr, PdfStrikeOutAnnotation as cs, renderToPdf as ct, autoTagPage as d, dataMatrixToOperators as da, batchMerge as di, buildCertificateChain as dn, buildOutputIntent as do, generateXRechnungCii as dr, generateCircleAppearance as ds, isWasmSimdSupported as dt, professionalPreset as ea, extractTables as ei, inspectEncryption as en, createAssociatedFile as eo, signDeferred as er, PdfStampAnnotation as es, pdfResponse as et, buildPdfUa2Xmp as f, encodeDataMatrix as fa, processBatch as fi, validateCertificateChain as fn, SRGB_ICC_PROFILE as fo, buildPdfA4Xmp as fr, generateFreeTextAppearance as fs, MemoryBudget as ft, tagHeading as g, calculateEanCheckDigit as ga, removeBookmark as gi, layoutTextFlow as gn, enforcePdfA as go, didYouMean as gr, generateSquareAppearance as gs, SharedFlag as gt, tagFigure as h, upcAToOperators as ha, removeAllBookmarks as hi, layoutParagraph as hn, flattenTransparency as ho, sampleShadingColor as hr, generateLineAppearance as hs, SharedCounter as ht, validateEn16931 as i, readCode39 as ia, metadataPlugin as ii, buildCertPath as in, getProfile as io, gtsPdfVtVersion as ir, PdfPolygonAnnotation as is, Fragment as it, buildEncryptedPayload as j, canDirectEmbed as ja, MissingOnValueCheckError as ji, TrustStore as jn, addFieldLock as jo, evaluateFunction as jr, detectNextGenFormat as jt, buildSoftMaskGroupExtGState as k, valuesToModules as ka, InvalidFieldNamePartError as ki, parseTileInfo as kn, getCounterSignatures as ko, identityTransferFunction as kr, saveDocumentIncremental as ks, registerImageDecoder as kt, convertPdfAConformanceXmp as l, encodePdf417 as la, validatePdfUa as li, validateExtendedKeyUsage as ln, generateZapfDingbatsToUnicodeCmap as lo, splitByScript as lr, PdfUnderlineAnnotation as ls, SIMD_NOTE as lt, LIST_NUMBERING_KEY as m, encodeUpcA as ma, getBookmarks as mi, layoutColumns as mn, detectTransparency as mo, buildFunctionShading as mr, generateInkAppearance as ms, createMemoryBudget as mt, detectFacturXProfile as n, readBarcode as na, tableToJson as ni, sanitizePdf as nn, parseXmpMetadata as no, buildPdfVtDParts as nr, PdfLineAnnotation as ns, sendPdfToNodeResponse as nt, buildFacturXXmp as o, readEan8 as oa, StreamingPdfParser as oi, verifyOfflineRevocation as on, isValidLevel as oo, renderToPdf$1 as or, PdfHighlightAnnotation as os, jsx as ot, validatePdfUa2 as p, calculateUpcCheckDigit as pa, addBookmark as pi, findHyphenationPoints as pn, generateSrgbIccProfile as po, pdfA4Rules as pr, generateHighlightAppearance as ps, MemoryBudgetExceededError as pt, renderDisplayListToCanvas as q, applyRedaction as qa, shrinkFontSize as qi, parseAcrobatDate as qn, requestTimestamp as qo, PDF2_NAMESPACE as qr, getColorGlyphLayers as qt, parseCiiXml as r, readCode128 as ra, accessibilityPlugin as ri, scanPdfThreats as rn, validateXmpMetadata as ro, buildVtDpm as rr, PdfPolyLineAnnotation as rs, buildFormFromJsonSchema as rt, buildPdfRIdentificationXmp as s, calculateBarcodeDimensions as sa, PdfDocumentBuilder as si, EKU_OIDS as sn, generateSymbolToUnicodeCmap as so, createRangeFetcher as sr, PdfSquigglyAnnotation as ss, jsxs as st, initWasm as t, stripedPreset as ta, tableToCsv as ti, verifyRedactions as tn, extractXmpMetadata as to, createWorkerPool as tr, PdfCircleAnnotation as ts, pdfStreamResponse as tt, preflightPdfA as u, pdf417ToOperators as ua, batchFlatten as ui, validateKeyUsage as un, getToUnicodeCmap as uo, generateOrderX as ur, PdfFreeTextAnnotation as us, detectRuntimeCapabilities as ut, tagList as v, ean8ToOperators as va, flattenFields as vi, enforcePdfX as vn, delinearizePdf as vo, renderCodeFrame as vr, generateStrikeOutAppearance as vs, isSharedMemoryAvailable as vt, tagTableRow as w, computeCode39CheckDigit as wa, FieldAlreadyExistsError as wi, summarizeBitDepth as wn, optimizeIncrementalSave as wo, STANDARD_SPOT_FUNCTIONS as wr, parseXmpMetadata$1 as ws, feGaussianBlur as wt, tagTable as x, encodeItf as xa, CombedTextLayoutError as xi, getComponentDepths as xn, linearizePdf as xo, buildGtsPdfxVersion as xr, PdfTextAnnotation as xs, feColorMatrixSaturate as xt, tagListItem as y, encodeEan13 as ya, flattenForm as yi, validatePdfX as yn, getLinearizationInfo as yo, generateCiiXml as yr, generateUnderlineAppearance as ys, feBlend as yt, renderPageTile as z, isCmykTiff as za, applyHeaderFooterToPage as zi, resolveFieldReference as zn, validateSignatureChain as zo, buildCalRGB as zr, xyzToRgb as zt };
|
|
35998
37848
|
|
|
35999
|
-
//# sourceMappingURL=src-
|
|
37849
|
+
//# sourceMappingURL=src-wZnfMPwi.mjs.map
|