import type { InputFormat, AcceptedInputFormat } from './convert/detect'; import type { TextOptions } from './convert/text'; export declare const DEFAULT_FETCH_TIMEOUT_MS = 5000; export declare const DEFAULT_MAX_BYTES_PER_URL: number; export declare const DEFAULT_ALLOWED_PROTOCOLS: readonly ["http:", "https:"]; export declare const DEFAULT_URL_CONCURRENCY = 8; export declare class PdfMergeError extends Error { readonly cause?: unknown | undefined; constructor(message: string, cause?: unknown | undefined); } export declare class InvalidPdfFormatError extends PdfMergeError { constructor(message?: string, cause?: unknown); } export declare class PdfFetchError extends PdfMergeError { readonly url: string; readonly index?: number | undefined; constructor(url: string, message: string, cause?: unknown, index?: number | undefined); } /** * Check an image is safe to hand to pdf-lib's embedders, from its header and * structure alone, before anything is decoded. * * For PNGs, pdf-lib's decoder is fragile in three ways, each guarded here: * - It allocates buffers proportional to the IHDR-declared width × height * *before* reading pixel data, so a tiny file declaring enormous dimensions * can force a multi-gigabyte allocation (a decompression bomb). The * declared pixel count is capped at `maxPixels`. * - It accepts an IHDR anywhere and decodes every animation frame, which * would let a decoy header or an oversized frame slip past that cap. * - It loops forever on a cut-off file, on an incomplete compressed stream, * or on a text chunk missing its separators, hanging the process. * The chunk structure is validated first (see `inspectPng`), then the * compressed data is checked (see `checkPngImageData`); that check runs * after the pixel cap, which bounds its cost. * * JPEGs are embedded without decoding (pdf-lib copies the DCT stream as-is), * so they carry no such allocation, but their frame-header dimensions still * size the output page, and an absurd header (e.g. 65535 × 65535) would * produce a page far beyond the PDF specification's 14400-point maximum. * Applying the same cap keeps the two formats consistent. * * @throws {InvalidPdfFormatError} when a PNG is malformed or truncated, or is * animated with more than one frame. * @throws {PdfMergeError} when the declared pixel count exceeds `maxPixels`. */ export declare function assertImageSafeToEmbed(bytes: Uint8Array, format: 'png' | 'jpg', maxPixels: number, label: string): void; /** The PDF format's maximum page dimension: 14400 points (200 inches). */ export declare const MAX_PDF_PAGE_DIMENSION = 14400; /** * How image inputs (`.png` / `.jpg`) are placed on their pages. See * `MergeOptions.image`. */ export interface ImageOptions { /** * - `'page'` (default): each image gets a standard page (see `pageSize`, * turned landscape for a landscape image) and is scaled **down** to fit * inside the margins, centered, with its aspect ratio preserved. An image * smaller than that area is drawn at its natural size, never enlarged. * - `'natural'`: the page is sized to the image itself at one pixel per * point (1/72 inch), the behaviour before 1.6. Pages are capped at the PDF * format's 14400-point (200-inch) maximum. * * Either way the full-resolution image is embedded; only its displayed size * changes, so zooming in still shows every pixel. */ fit?: 'page' | 'natural'; /** * Page size for `fit: 'page'`: `'letter'` (default, 612 × 792 pt, matching * converted text pages), `'a4'` (595.28 × 841.89 pt), or `[width, height]` * in points. The orientation follows the image, so a landscape image gets a * landscape page. */ pageSize?: 'letter' | 'a4' | [number, number]; /** * Blank margin on every side, in points, for `fit: 'page'`. Defaults to 54 * (0.75 inch), matching converted text pages and leaving room for page * numbers. Use `0` to fill the page edge to edge. */ margin?: number; } /** `ImageOptions` with defaults applied and values validated. */ export interface ResolvedImageLayout { fit: 'page' | 'natural'; pageSize: [number, number]; margin: number; } /** * Selector for which pages to keep from a source PDF. * Pages are **1-indexed** to match what users see in PDF viewers. * * - `number[]` — explicit page numbers, e.g. `[1, 3, 5]`. * - `string` — comma-separated ranges, e.g. `"1-3,5,8-10"`. * * Duplicate page numbers are kept (the page appears multiple times in the * output). Out-of-range, zero, negative, or unparseable selectors throw * `PdfMergeError`. */ export type PageSelector = number[] | string; export interface MergeSource { bytes: Uint8Array; pages?: PageSelector; /** * Label for this document in a generated table of contents (see * `MergeOptions.tableOfContents`). When omitted, the entry falls back to * `"Document N"` (the Node file merge defaults it to the file's basename). */ title?: string; /** * Source format. When omitted, the bytes are treated as a PDF. Non-PDF * inputs (`'txt'`, `'png'`, `'jpg'`) are converted to a PDF before merging. * Page selection (`pages`) is ignored for converted inputs. */ format?: InputFormat; } export type { InputFormat, AcceptedInputFormat } from './convert/detect'; export type { TextOptions } from './convert/text'; /** * Document-level metadata applied to the merged output. Every field is * optional — fields you omit are left as whatever pdf-lib produces by default. * * Useful when the merged PDF is going to be sent to a customer or stored in * a system that surfaces document properties (Finder, Explorer, DMS, etc.). * * Note: the `Producer` field is hard-coded by pdf-lib on save and cannot be * customized at this layer; everything else is settable. */ export interface PdfMetadata { title?: string; author?: string; subject?: string; keywords?: string[]; creator?: string; creationDate?: Date; modificationDate?: Date; } /** Common options accepted by every merge function. */ export interface MergeOptions { /** Metadata to set on the merged document. */ metadata?: PdfMetadata; /** * Layout options for `.txt` inputs converted to PDF (font size, margin, * line height, and a font override for scripts the bundled font doesn't * cover). Ignored when no text inputs are present. See `TextOptions`. */ text?: TextOptions; /** * Page layout for image inputs (`.png` / `.jpg`). By default each image is * scaled down to fit a US Letter page with three-quarter-inch margins, oriented to * match the image; pass `{ fit: 'natural' }` for a page sized to the image * itself. Ignored when no image inputs are present. See `ImageOptions`. */ image?: ImageOptions; /** * Watermark to stamp on every page of the merged output — either a text * label (`TextWatermark`) or an image / logo (`ImageWatermark`). Omit (or * leave undefined) to skip — when no watermark is supplied, no extra * drawing is performed and the original fast path is preserved. */ watermark?: Watermark; /** * Page numbers stamped in the margin of every page of the merged output. * Omit to skip. See `PageNumbers` for the available fields. */ pageNumbers?: PageNumbers; /** * Prepend a printed table-of-contents page listing each source document * and the page it starts on in the merged output. Each entry is a * clickable internal link that jumps to that document. Omit to skip. * * Pass an (even empty) object to enable it; see `TableOfContents` for the * available fields. Entry labels come from each source's `title` (for the * Node file merge, the file's basename by default), falling back to * `"Document N"`. * * The listed page numbers are the **physical** positions in the final PDF, * so the TOC page itself counts as page 1 and the numbers match what a PDF * viewer shows. When combined with `pageNumbers`, the stamped numbers cover * the TOC page(s) too, so both stay in agreement. */ tableOfContents?: TableOfContents; /** * Allow merging password-protected / encrypted source PDFs. When `true`, * pdf-lib's encryption check is skipped at load time. This **does not * decrypt** the document — encrypted page content streams cannot be * read without the password — so the merge will fail later for truly * password-protected files. Use this flag for PDFs that *declare* * encryption metadata but are functionally readable (a surprisingly * common case from older PDF generators). Defaults to `false`. */ ignoreEncryption?: boolean; /** * Maximum pixel count (width × height) accepted for an image — whether a * converted `'png'` / `'jpg'` input or an image watermark — checked from the * file header before anything is decoded. For PNGs this guards against * **decompression bombs**: a tiny file declaring enormous dimensions would * otherwise make pdf-lib allocate width × height × 4 bytes *before* * decoding, crashing the process. For JPEGs (never decoded) it keeps an * absurd header from producing an out-of-spec page size. Defaults to * `100_000_000` (100 MP); raise it for legitimately huge scans, or lower it * for untrusted uploads — a PNG at the cap can still cost hundreds of MB to * decode. */ maxImagePixels?: number; /** * Optional `AbortSignal` to cancel the merge. Aborts both the URL * fetch step (in `mergePdfUrls`) and the merge loop itself. If the * signal is already aborted when the call begins, the function throws * immediately. Aborted operations throw the signal's `reason` if set, * otherwise an `AbortError`-shaped `DOMException`. */ signal?: AbortSignal; } /** * Where to place a watermark. Named positions are computed per page (so they * work on any page size); pass `{ x, y }` for absolute PDF-point coordinates * measured from the bottom-left corner. */ export type WatermarkPosition = 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | { x: number; y: number; }; /** * Watermark stamped on every page of the merged output — either a text label * (`TextWatermark`) or an image / logo (`ImageWatermark`). Provide exactly one * of `text` or `image`. */ export type Watermark = TextWatermark | ImageWatermark; /** * Text watermark drawn on every page of the merged output. * * Only `text` is required; the other fields control appearance and * positioning. The watermark is drawn directly on each page using * Helvetica — no extra fonts are embedded, so the size overhead is minimal. */ export interface TextWatermark { /** The text to stamp. Required; must be a non-empty string. */ text: string; /** Never set on a text watermark — modelling the "exactly one of text or * image" contract at the type level so `{ text, image }` is a compile error. */ image?: never; /** Opacity from 0 (invisible) to 1 (opaque). Defaults to 0.2. */ opacity?: number; /** Font size in points. Defaults to 48. */ fontSize?: number; /** RGB color, each channel 0-1. Defaults to mid-gray. */ color?: { r: number; g: number; b: number; }; /** Rotation in degrees, counter-clockwise. Defaults to 0. */ rotate?: number; /** * Where to place the watermark. Defaults to `'center'`. See * `WatermarkPosition`. */ position?: WatermarkPosition; } /** * Image / logo watermark drawn on every page of the merged output. * * Supply a PNG or JPEG as raw bytes (`Uint8Array` / `ArrayBuffer`) or a * Base64 string (a bare Base64 payload or a `data:image/...;base64,` URI). * The format is auto-detected from the image's magic bytes; set `format` * to override detection. The image is embedded once and re-used across * pages, so the size overhead stays flat regardless of page count. */ export interface ImageWatermark { /** * The image to stamp. Raw PNG/JPEG bytes as a `Uint8Array` or * `ArrayBuffer`, or a Base64 string (bare payload or `data:` URI). * Required. */ image: Uint8Array | ArrayBuffer | string; /** Never set on an image watermark — modelling the "exactly one of text or * image" contract at the type level so `{ text, image }` is a compile error. */ text?: never; /** * Image format. Auto-detected from the magic bytes when omitted; set it * to force `'png'` or `'jpg'` (`'jpeg'` is accepted as an alias). */ format?: 'png' | 'jpg' | 'jpeg'; /** * Drawn width in PDF points. When only one of `width` / `height` is set, * the other is derived from the image's aspect ratio. When both are * omitted (and no `scale`), the image's natural pixel size is used. */ width?: number; /** Drawn height in PDF points. See `width` for aspect-ratio behavior. */ height?: number; /** * Uniform scale factor applied to the image's natural pixel size (e.g. * `0.5` for half size). Ignored when `width` or `height` is provided. */ scale?: number; /** Opacity from 0 (invisible) to 1 (opaque). Defaults to 0.2. */ opacity?: number; /** Rotation in degrees, counter-clockwise. Defaults to 0. */ rotate?: number; /** * Where to place the watermark. Defaults to `'center'`. See * `WatermarkPosition`. */ position?: WatermarkPosition; } /** * Page numbers stamped on each page of the merged output. * * The format string supports two tokens: * - `{current}` — the page's index in the merged document (1-based by * default; controlled by `startAt`). * - `{total}` — the total page count of the merged document. * * Example: `format: 'Page {current} of {total}'` produces * `"Page 1 of 10"`, `"Page 2 of 10"`, etc. */ export interface PageNumbers { /** Format template. Defaults to `'{current} / {total}'`. */ format?: string; /** Page-number value for the first page. Defaults to `1`. */ startAt?: number; /** Font size in points. Defaults to `10`. */ fontSize?: number; /** RGB color, each channel 0–1. Defaults to mid-gray. */ color?: { r: number; g: number; b: number; }; /** * Where to place the page number. Named positions are computed per page * with a small margin from the edge. `{ x, y }` is taken as an absolute * baseline-left point in PDF points. * * Defaults to `'bottom-center'`. */ position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' | { x: number; y: number; }; } /** * A printed table-of-contents page prepended to the merged output. * * Every field is optional. The TOC lists one entry per source document — * its label on the left, its starting page on the right joined by a dotted * leader — and each entry is a clickable internal link to that page. When * there are more entries than fit on one page, the TOC spills onto additional * pages automatically. */ export interface TableOfContents { /** Heading printed at the top of the first TOC page. Defaults to * `'Table of Contents'`. Pass an empty string to omit the heading. */ heading?: string; /** Font size for the entry rows, in points. Defaults to `12`. */ fontSize?: number; /** Font size for the heading, in points. Defaults to `20`. */ headingFontSize?: number; /** RGB text color, each channel 0–1. Defaults to black. */ color?: { r: number; g: number; b: number; }; /** * Size of the generated TOC page(s), in PDF points `[width, height]`. * Defaults to matching the first content page (or US Letter, 612×792, when * the document has no pages yet). */ pageSize?: [number, number]; } /** * Throws if the signal is already aborted. Used at the entry of each * merge function and between sources in the merge loop. Mirrors the * native abort convention: throw `signal.reason` when provided, * otherwise an `AbortError`-shaped `DOMException`. */ /** Resolve or reject with `promise`, or reject as soon as `signal` aborts * (with the same value `throwIfAborted` would throw). */ export declare function untilAborted(promise: Promise, signal: AbortSignal | undefined): Promise; export declare function throwIfAborted(signal: AbortSignal | undefined): void; /** * The merged PDF plus information about it, returned by the `*WithInfo` * merge functions. `pdf` is exactly what the corresponding plain merge * function returns (a Base64 string, or a `Uint8Array`). */ export interface MergeResult { /** The merged PDF — same type the plain merge function returns. */ pdf: T; /** Total number of pages in the merged document. */ pageCount: number; /** * Size of the merged PDF **in bytes** (the actual PDF file size). For the * Base64 / URL variants this is the size of the decoded PDF bytes, not the * length of the Base64 string. */ byteLength: number; } /** Like `mergePdfSources`, but also reports the merged page count so callers * don't have to re-parse the output. */ export declare function mergePdfSourcesWithInfo(sources: MergeSource[], options?: MergeOptions): Promise<{ bytes: Uint8Array; pageCount: number; }>; export declare function mergePdfSources(sources: MergeSource[], options?: MergeOptions): Promise; /** Entry accepted by `mergeBase64PDFs`: a plain Base64 string, or an object * specifying which `pages` to keep and/or a `title` for the table of contents. */ export type MergeBase64Input = string | { data: string; pages?: PageSelector; title?: string; }; /** * Like {@link mergeBase64PDFs}, but also returns the merged document's * `pageCount` and `byteLength` (see {@link MergeResult}). `byteLength` is the * size of the decoded PDF bytes, not the Base64 string length. */ export declare function mergeBase64PDFsWithInfo(inputs: MergeBase64Input[], options?: MergeOptions): Promise>; /** * Merges an ordered array of Base64 encoded PDFs into a single Base64 PDF string. * The output preserves the input order: index 0 appears first, then 1, then 2, ... * * @throws {PdfMergeError} when the input array is empty. * @throws {InvalidPdfFormatError} when any input is not a decodable PDF. */ export declare function mergeBase64PDFs(inputs: MergeBase64Input[], options?: MergeOptions): Promise; export declare function toBytes(input: Uint8Array, label: string): Uint8Array; /** Entry accepted by `mergePdfBuffers`: raw bytes, or an object specifying * which `pages` to keep and/or a `title` for the table of contents. */ export type MergeBufferInput = Buffer | Uint8Array | { data: Buffer | Uint8Array; pages?: PageSelector; title?: string; /** * Input format. Auto-detected from the bytes for PDF/PNG/JPEG; required * (`'txt'`) for plain text, which has no reliable signature. `'jpeg'` is * accepted as an alias for `'jpg'`. */ format?: AcceptedInputFormat; }; /** Resolve an input's format from an explicit hint (validated) or, failing * that, its magic bytes. Shared by the buffer and file input paths. */ export declare function resolveInputFormat(explicit: unknown, bytes: Uint8Array, index: number): InputFormat | undefined; /** * Like {@link mergePdfBuffers}, but also returns the merged document's * `pageCount` and `byteLength` (see {@link MergeResult}). */ export declare function mergePdfBuffersWithInfo(inputs: MergeBufferInput[], options?: MergeOptions): Promise>; /** * Merges an ordered array of raw PDF byte buffers and returns the merged * document as a `Uint8Array`. Use this when you already have PDF bytes * (from `fs.readFile`, an S3 download, an HTTP body, etc.) and want to skip * the Base64 round-trip. * * @throws {PdfMergeError} when the input array is empty. * @throws {InvalidPdfFormatError} when any input is not a parseable PDF. */ export declare function mergePdfBuffers(inputs: MergeBufferInput[], options?: MergeOptions): Promise; export declare function sanitizeUrlForMessages(rawUrl: string): string; export interface HostGuard { blockPrivateHosts: boolean; /** Lower-cased hostname allowlist, or undefined for "no allowlist". */ allowedHosts?: Set; } export declare function buildHostGuard(options: { blockPrivateHosts?: boolean; allowedHosts?: readonly string[]; }): HostGuard | undefined; /** Resolves a hostname to its IP address strings, for the private-host * guard. Provided by the Node entry (via `dns`); omitted in the browser, * where hostnames can't be resolved and DNS-based checks are skipped. */ export type HostResolver = (host: string) => Promise; /** Validates a parsed URL's host against the guard. Throws `PdfMergeError` * for a policy rejection (allowlist miss / private literal) and * `PdfFetchError` when DNS resolution itself fails. Sync checks (allowlist, * localhost, literal IP) always run; hostname DNS resolution runs only when * a `resolveHost` is supplied. */ export declare function assertHostAllowed(parsed: URL, index: number, guard: HostGuard, resolveHost?: HostResolver, signal?: AbortSignal): Promise; /** * Discard a response body we won't read, so its connection is closed instead * of streaming on until the response is garbage-collected. */ export declare function discardBody(response: Response): Promise; /** True when a parsed URL carries a username or password. `fetch` refuses * such URLs, and its error message would repeat the credentials. */ export declare function hasUrlCredentials(parsed: URL): boolean; /** Headers dropped when a redirect crosses to a different origin, so * credentials aren't leaked to an unexpected host. */ export declare function stripSensitiveHeaders(headers: Record): Record; export declare function validateAndParseUrl(url: unknown, index: number, allowedProtocols: readonly string[]): URL; /** A pluggable fetch strategy. The universal default follows redirects with * the platform's own handling; the Node entry injects a strategy that follows * redirects manually so it can host-guard each hop. */ export interface PerformFetchArgs { rawUrl: string; signal: AbortSignal; headers?: Record; index: number; allowedProtocols: readonly string[]; safeUrl: string; } export type PerformFetch = (args: PerformFetchArgs) => Promise; export interface MergePdfUrlsOptions extends MergeOptions { /** * Per-request timeout in milliseconds, covering the host check, the * request and the body download. Defaults to 5000. `Infinity` (or any * value above 2^31 − 1, about 24.8 days) means no timeout. */ timeoutMs?: number; /** * Maximum bytes accepted from any single URL response. Defaults to ~100 MiB. * Prevents memory exhaustion from hostile or runaway endpoints. `Infinity` * removes the cap. */ maxBytesPerUrl?: number; /** * Protocols accepted when fetching. Defaults to ['http:', 'https:']. * Pass a narrower list (e.g. `['https:']`) to harden further. */ allowedProtocols?: readonly string[]; /** * Maximum number of URL fetches running in parallel. Defaults to 8. */ concurrency?: number; /** * Default request headers applied to every URL fetch (e.g. `Authorization`, * `User-Agent`). Per-URL headers — supplied via the object form of * `urls` — are merged on top of these and take precedence. */ headers?: Record; /** * SSRF hardening: when `true`, reject any URL (and, in Node, any redirect * target) whose host is `localhost` or resolves to a loopback / private / * link-local / reserved IP address. Defaults to `false`. * * In the browser, DNS can't be resolved and redirects can't be intercepted, * so this only checks IP-literal / `localhost` initial URLs — CORS is the * effective boundary there. */ blockPrivateHosts?: boolean; /** * SSRF hardening: an explicit allowlist of hostnames (case-insensitive, * exact match). When provided and non-empty, any URL (or, in Node, redirect * target) whose host is not on the list is rejected. An empty or omitted * list means "no host allowlist". */ allowedHosts?: readonly string[]; } /** A URL fetched by `mergePdfUrls`, optionally with per-URL headers, a * `pages` selector, and/or a `title` for the table of contents. */ export type MergeUrlInput = string | { url: string; headers?: Record; pages?: PageSelector; title?: string; }; /** Platform-specific seams for the URL pipeline. The Node entry provides a * DNS resolver and a manual-redirect fetch strategy; the browser passes * neither and gets a redirect-following fetch with initial-URL-only guarding. */ export interface UrlPlatform { resolveHost?: HostResolver; makeGuardedFetch?: (guard: HostGuard, resolveHost?: HostResolver) => PerformFetch; } /** * Core URL merge, parameterized by platform capabilities. Entry points wrap * this: Node injects DNS + manual-redirect guarding; the browser injects * nothing (redirect-following fetch, initial-URL host checks only). */ export declare function mergePdfUrlsCoreWithInfo(urls: MergeUrlInput[], options?: MergePdfUrlsOptions, platform?: UrlPlatform): Promise>; /** Thin wrapper returning just the Base64 string (see `mergePdfUrlsCoreWithInfo`). */ export declare function mergePdfUrlsCore(urls: MergeUrlInput[], options?: MergePdfUrlsOptions, platform?: UrlPlatform): Promise; /** * Fetches PDFs from the given ordered URLs and merges them into a single * Base64 PDF string, using the platform's native `fetch`. This is the * browser-ready implementation (redirects are followed by the browser and * subject to CORS; the DNS/redirect SSRF guard is Node-only). The Node entry * exports an enhanced `mergePdfUrls` with the full host guard. */ export declare function mergePdfUrls(urls: MergeUrlInput[], options?: MergePdfUrlsOptions): Promise; /** * Like {@link mergePdfUrls}, but also returns the merged document's * `pageCount` and `byteLength` (see {@link MergeResult}). `byteLength` is the * size of the decoded PDF bytes, not the Base64 string length. */ export declare function mergePdfUrlsWithInfo(urls: MergeUrlInput[], options?: MergePdfUrlsOptions): Promise>;