Pro edition
Merge — Deep Reference
At a glance
Section titled “At a glance”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.
Availability & licensing
Section titled “Availability & licensing”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.
Public API surface
Section titled “Public API surface”| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
SmartMerger::__construct() | ?PdfMerger $coreMerger = null, ?PdfSplitter $splitter = null | Accepts 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 config | SmartMergeResult | InvalidArgumentException on an empty input list; OverflowException when the input count exceeds maxInputs or an input exceeds maxBytesPerInput | Sole merge entry point |
MergeInput::__construct() | string $pdfData, list<PageRange> $pageRanges = [], string $label = '' | Value object; an empty $pageRanges selects all pages | — | — | Readonly |
MergeInput::hasPageRanges() | — | True when the input carries at least one page range | bool | — | — |
SmartMergeConfig::__construct() | bool $consolidateBookmarks = true, bool $deduplicatePages = false, bool $rewriteLinks = true, int $maxInputs = 100, int $maxBytesPerInput = 100_000_000 | Value object holding the enhancement toggles and the resource bounds | — | — | Readonly; deduplication is opt-in |
SmartMergeConfig::default() | — | Bookmarks and link scan on, deduplication off | self | — | Static factory |
SmartMergeConfig::basic() | — | All enhancements off; base concatenation only | self | — | Static 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 statistics | — | — | Readonly |
SmartMergeResult::isValid() | — | True when the output begins with the %PDF header | bool | — | Header check only |
SmartMergeResult::hasOptimizations() | — | True when any duplicate was removed or any link counted | bool | — | — |
SemanticSplitter::__construct() | ?PdfSplitter $splitter = null | A null argument constructs the default Pro splitter | — | — | Constructor injection for testing |
SemanticSplitter::splitByStructure() | string $pdfData, float $headingFontThreshold = 14.0 | Detects heading-sized Tf operators as section starts and splits at those boundaries; no detected structure returns one whole-document section | SplitResult | InvalidArgumentException when the buffer is empty or lacks the %PDF header; OverflowException when the input exceeds 100 MB | Falls back to Core page-range split |
Entry-point signatures
Section titled “Entry-point signatures”public function __construct( ?PdfMerger $coreMerger = null, ?PdfSplitter $splitter = null,)
public function merge( array $inputs, SmartMergeConfig $config = new SmartMergeConfig(),): SmartMergeResultpublic function __construct( public string $pdfData, public array $pageRanges = [], public string $label = '',)
public function hasPageRanges(): boolpublic 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(): selfpublic function isValid(): bool
public function hasOptimizations(): boolpublic function __construct(?PdfSplitter $splitter = null)
public function splitByStructure( string $pdfData, float $headingFontThreshold = 14.0,): SplitResultBehavior contract
Section titled “Behavior contract”Merge pipeline
Section titled “Merge pipeline”SmartMerger::merge() runs a fixed pipeline, observed externally as follows.
- An empty input list raises
InvalidArgumentException. The input count is then bounded bymaxInputs; an overrun raisesOverflowException. - Each input is size-checked against
maxBytesPerInputbefore use. When the input declares page ranges, it is first reduced to the selected pages through the Pro splitter, then contributes only those pages. - When
deduplicatePagesis enabled, each input document’s full byte string is fingerprinted with the non-cryptographicxxh128function. An input whose bytes exactly match an earlier input is dropped. Deduplication is whole-document and byte-exact. - 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. - Bookmark consolidation is applied when
consolidateBookmarksis enabled and at least one input carries a non-empty label. A minimal/Outlinesdictionary is inserted, linked from the document catalog, with one outline entry per input in merge order. - When
rewriteLinksis enabled, the merged output is scanned for/S /GoToactions and their count is reported.
Result statistics
Section titled “Result statistics”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.
Bookmark titles
Section titled “Bookmark titles”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.
Base assembly
Section titled “Base assembly”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.
Structure-aware splitting
Section titled “Structure-aware splitting”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.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- An empty input list fails with
InvalidArgumentExceptionbefore any assembly. - An input count above
maxInputs(default 100), or any input abovemaxBytesPerInput(default 100 MB), fails withOverflowException. 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
duplicatesRemovedcounts dropped whole inputs despite the page-orienteddeduplicatePagesname. sourceCountreflects 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
consolidateBookmarkstrue but every label empty, no/Outlinesobject is written. - Injected outline entries carry titles and the
/Parent,/Prev,/Nexttree links; they do not embed explicit/Destdestinations in this release. - Link rewriting counts
/S /GoToactions only; it does not re-point destinations across renumbered objects. TreatlinksRewrittenas a detection count. SemanticSplitterdetection is lexical. It keys onTffont-size operators, so image-only or unusually encoded pages produce no boundaries and return a single whole-document section.
FIPS-mode behavior
Section titled “FIPS-mode behavior”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.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
Consolidated bookmarks written as an /Outlines dictionary linked from the document catalog | ISO 32000-2:2020 | §7.7.2 |
| Base merge emits a byte-accurate cross-reference table for every indirect object | ISO 32000-2:2020 | §7.5.4 |
| Outline entry titles escaped as PDF literal strings, with backslash and parenthesis handling | ISO 32000-2:2020 | §7.3.4.2 |
| Full cross-document link re-resolution | — | Not supported (GoTo detection only) |
| Explicit per-section outline destinations | — | Not emitted in this release |
All clauses are paraphrased; NextPDF does not reproduce normative text. These are capability statements.
Development notes
Section titled “Development notes”- Availability within the Pro package:
SmartMerger,MergeInput,SmartMergeConfig,SmartMergeResult, andSemanticSplittersince 2.2.0. All are current innextpdf/pro3.1.0. - The base merge delegates to Pro
PdfSplitter::mergeDocuments(). The legacy CorePdfMerger::merge()is a fail-closed stub in this release and is never called. - Enable
deduplicatePagesonly 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
OverflowExceptionwhen merging untrusted input; the count and size bounds are intentional rejections. - Prefer the Pro
PdfSplitterdirectly for simple page-range splitting; reach forSemanticSplitteronly when heading-driven sectioning is required.
Publication boundary
Section titled “Publication boundary”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.
See also
Section titled “See also”- Merge (capability) — install, quick start, and production samples.
- Toc — Deep Reference
- Diff — Deep Reference
- Document — Deep Reference — Pro splitter and base merge engine.