Skip to content
getnextpdf.com

Pro edition

Merge — Deep Reference

This page is the contract-level reference for the NextPDF Pro Merge module, NextPDF\Pro\Merge. SmartMerger assembles several input documents into one and applies Pro enhancements: a consolidated bookmark tree from per-input labels, whole-document deduplication, per-input page-range selection, and internal-link detection. SemanticSplitter is the companion structure-aware split entry point. This page states the public API, the observable behaviour contract, the resource bounds, and the failure modes. Task-oriented setup and samples live on the Merge capability page.

This capability ships in NextPDF Pro (nextpdf/pro) and activates with a Pro-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.

No runtime capability flag gates this module. The Merge classes are usable whenever nextpdf/pro is installed and licensed.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
SmartMerger::__construct()?PdfMerger $coreMerger = null, ?PdfSplitter $splitter = nullAccepts and ignores the legacy core merger; a null splitter constructs the default Pro splitter$coreMerger retained for backward-compatible construction only
SmartMerger::merge()list<MergeInput> $inputs, SmartMergeConfig $config = new SmartMergeConfig()Reduces page ranges, deduplicates whole inputs, delegates base assembly, then injects bookmarks and counts links per the configSmartMergeResultInvalidArgumentException on an empty input list; OverflowException when the input count exceeds maxInputs or an input exceeds maxBytesPerInputSole merge entry point
MergeInput::__construct()string $pdfData, list<PageRange> $pageRanges = [], string $label = ''Value object; an empty $pageRanges selects all pagesReadonly
MergeInput::hasPageRanges()True when the input carries at least one page rangebool
SmartMergeConfig::__construct()bool $consolidateBookmarks = true, bool $deduplicatePages = false, bool $rewriteLinks = true, int $maxInputs = 100, int $maxBytesPerInput = 100_000_000Value object holding the enhancement toggles and the resource boundsReadonly; deduplication is opt-in
SmartMergeConfig::default()Bookmarks and link scan on, deduplication offselfStatic factory
SmartMergeConfig::basic()All enhancements off; base concatenation onlyselfStatic factory
SmartMergeResult::__construct()string $pdfData, int $totalPages, int $sourceCount, int $mergedSize, int $bookmarksAdded = 0, int $duplicatesRemoved = 0, int $linksRewritten = 0, list<string> $inputLabels = []Readonly carrier for the merged bytes and consolidation statisticsReadonly
SmartMergeResult::isValid()True when the output begins with the %PDF headerboolHeader check only
SmartMergeResult::hasOptimizations()True when any duplicate was removed or any link countedbool
SemanticSplitter::__construct()?PdfSplitter $splitter = nullA null argument constructs the default Pro splitterConstructor injection for testing
SemanticSplitter::splitByStructure()string $pdfData, float $headingFontThreshold = 14.0Detects heading-sized Tf operators as section starts and splits at those boundaries; no detected structure returns one whole-document sectionSplitResultInvalidArgumentException when the buffer is empty or lacks the %PDF header; OverflowException when the input exceeds 100 MBFalls back to Core page-range split
public function __construct(
?PdfMerger $coreMerger = null,
?PdfSplitter $splitter = null,
)
public function merge(
array $inputs,
SmartMergeConfig $config = new SmartMergeConfig(),
): SmartMergeResult
public function __construct(
public string $pdfData,
public array $pageRanges = [],
public string $label = '',
)
public function hasPageRanges(): bool
public function __construct(
public bool $consolidateBookmarks = true,
public bool $deduplicatePages = false,
public bool $rewriteLinks = true,
public int $maxInputs = 100,
public int $maxBytesPerInput = 100_000_000,
)
public static function default(): self
public static function basic(): self
public function isValid(): bool
public function hasOptimizations(): bool
public function __construct(?PdfSplitter $splitter = null)
public function splitByStructure(
string $pdfData,
float $headingFontThreshold = 14.0,
): SplitResult

SmartMerger::merge() runs a fixed pipeline, observed externally as follows.

  1. An empty input list raises InvalidArgumentException. The input count is then bounded by maxInputs; an overrun raises OverflowException.
  2. Each input is size-checked against maxBytesPerInput before use. When the input declares page ranges, it is first reduced to the selected pages through the Pro splitter, then contributes only those pages.
  3. When deduplicatePages is enabled, each input document’s full byte string is fingerprinted with the non-cryptographic xxh128 function. An input whose bytes exactly match an earlier input is dropped. Deduplication is whole-document and byte-exact.
  4. Base assembly delegates to the Pro PdfSplitter::mergeDocuments() engine, which renumbers every input into one contiguous object space and emits a real cross-reference table.
  5. Bookmark consolidation is applied when consolidateBookmarks is enabled and at least one input carries a non-empty label. A minimal /Outlines dictionary is inserted, linked from the document catalog, with one outline entry per input in merge order.
  6. When rewriteLinks is enabled, the merged output is scanned for /S /GoTo actions and their count is reported.

SmartMergeResult reports the merged bytes plus statistics. totalPages comes from the base merge. sourceCount is the original input count, taken before deduplication. mergedSize is the output byte length. bookmarksAdded counts only inputs that supplied a non-empty label. duplicatesRemoved counts dropped whole inputs. linksRewritten is the detected GoTo count. inputLabels lists the resolved labels in merge order. isValid() checks the %PDF header; hasOptimizations() is true when a duplicate was removed or a link counted.

Each outline entry carries the input label as a /Title, escaped as a PDF literal string per ISO 32000-2:2020 §7.3.4.2. The reverse solidus is doubled first, parentheses are escaped, named control bytes use their defined sequences, and any remaining non-printable byte becomes a three-digit octal escape. A hostile label therefore cannot desynchronise the literal-string delimiter or inject object structure. Inputs with an empty label receive a Document N placeholder title, one-indexed.

The legacy Core PdfMerger::merge() is a deliberate fail-closed stub in this release; it is never invoked by SmartMerger. The base merge instead runs through Pro PdfSplitter::mergeDocuments(), so the merged file carries a byte-accurate cross-reference table with one entry per indirect object per ISO 32000-2:2020 §7.5.4. Determinism follows the Pro splitter’s documented profile: identical inputs and configuration yield a stable byte stream.

SemanticSplitter::splitByStructure() scans page content streams for Tf set-font operators at or above headingFontThreshold (default 14.0) and treats each such page as a section start. Boundaries are converted to page ranges and delegated to Pro PdfSplitter::split(). When no boundary is detected, the whole document returns as a single section. The input must begin with %PDF and stay within the 100 MB bound.

  • An empty input list fails with InvalidArgumentException before any assembly.
  • An input count above maxInputs (default 100), or any input above maxBytesPerInput (default 100 MB), fails with OverflowException. Both bounds are deliberate fail-closed rejections, not transient errors.
  • Deduplication is whole-document and byte-exact. Two inputs that render identically but differ in any byte are both retained, and duplicatesRemoved counts dropped whole inputs despite the page-oriented deduplicatePages name.
  • sourceCount reflects the original input count, not the post-deduplication document count.
  • Bookmark consolidation only fires when at least one input has a non-empty label. With consolidateBookmarks true but every label empty, no /Outlines object is written.
  • Injected outline entries carry titles and the /Parent, /Prev, /Next tree links; they do not embed explicit /Dest destinations in this release.
  • Link rewriting counts /S /GoTo actions only; it does not re-point destinations across renumbered objects. Treat linksRewritten as a detection count.
  • SemanticSplitter detection is lexical. It keys on Tf font-size operators, so image-only or unusually encoded pages produce no boundaries and return a single whole-document section.

No cryptographic operation occurs in this module, so no FIPS-mode-specific behaviour exists. The xxh128 content fingerprint used for deduplication is a non-cryptographic change-detection hash and carries no integrity or evidentiary weight.

ClaimStandardClause
Consolidated bookmarks written as an /Outlines dictionary linked from the document catalogISO 32000-2:2020§7.7.2
Base merge emits a byte-accurate cross-reference table for every indirect objectISO 32000-2:2020§7.5.4
Outline entry titles escaped as PDF literal strings, with backslash and parenthesis handlingISO 32000-2:2020§7.3.4.2
Full cross-document link re-resolutionNot supported (GoTo detection only)
Explicit per-section outline destinationsNot emitted in this release

All clauses are paraphrased; NextPDF does not reproduce normative text. These are capability statements.

  • Availability within the Pro package: SmartMerger, MergeInput, SmartMergeConfig, SmartMergeResult, and SemanticSplitter since 2.2.0. All are current in nextpdf/pro 3.1.0.
  • The base merge delegates to Pro PdfSplitter::mergeDocuments(). The legacy Core PdfMerger::merge() is a fail-closed stub in this release and is never called.
  • Enable deduplicatePages only when inputs may be byte-identical whole documents; it does not collapse near-duplicate or re-encoded copies.
  • Use SmartMergeConfig::basic() for pure concatenation and ::default() for bookmarks plus the link scan.
  • Catch OverflowException when merging untrusted input; the count and size bounds are intentional rejections.
  • Prefer the Pro PdfSplitter directly for simple page-range splitting; reach for SemanticSplitter only when heading-driven sectioning is required.

This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.