Skip to content
getnextpdf.com

Pro editionstability: Experimental

C2PA preview — Deep Reference

This page is the contract-level reference for the C2PA (Content Credentials) preview surface in NextPDF Pro. It covers five public symbols in NextPDF\Pro\Compliance\C2pa: the C2paManifestEmbedder SPI, the ManifestStore value object, the JumbfBoxParser, the C2paCapabilityStatus descriptor, and the gated Experimental\ExperimentalC2paEmbedder. It also documents the Feature::PREVIEW_C2PA_DRAFT gate and its environment variable, NEXTPDF_FEATURE_PREVIEW_C2PA_DRAFT.

The surface is experimental and split into two layers. The stable seam — ManifestStore, C2paManifestEmbedder, JumbfBoxParser — is always reachable and carries Manifest Store bytes both ways. Draft manifest synthesis lives only in ExperimentalC2paEmbedder and is default-off. The C2PA-PDF profile is not finalized by the working group; the synthesized wire format is pinned to a draft commit. No conformance claim is made, there is no verification path, and enabling the preview flag cannot create either. The task-oriented view lives on the 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.

The license activates the Pro compliance surface as a whole. The C2PA surface inside it stays a preview regardless of license tier. Draft synthesis additionally requires the process gate documented here; a Pro license alone never enables it.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
C2paManifestEmbedderBytes-only embed/extract SPI; no I/O; no claim synthesisFrozen, vendor-neutral seam interface.
C2paManifestEmbedder::embed()string $pdfBytes, ManifestStore $storeEmbeds $store->toBytes() at the profile-declared location; an empty Store MAY round-trip as a no-opstring new PDF bytesC2paException on any embed failure (oversized Store, invalid PDF, profile-location collision)Implementations never mutate or retain the input bytes.
C2paManifestEmbedder::extract()string $pdfBytesCheap detection probe; the no-Store case allocates almost nothing?ManifestStore (null on miss)C2paException subclass when a Store is present but breaches a hardening invariantA non-null Store has already passed JumbfBoxParser hardening.
ManifestStore::fromBoxes()array $boxes (list<JumbfBox>)Wraps a parser-validated ordered box listselfDoes not throw itself; hand-built JumbfBox construction enforces the same hardeningThe constructor is private; box order is load-bearing for round-trip equality.
ManifestStore::empty()noneStore with zero root boxesselfDoes not throwtoBytes() of an empty Store is the empty string.
ManifestStore::isEmpty()noneTests for zero root boxesboolDoes not throw
ManifestStore::toBytes()noneConcatenates the root-box serialisationsstringDoes not throwThis byte sequence is what an embedder writes.
ManifestStore::size()noneByte length of toBytes()int (>= 0)Does not throw
JumbfBoxParser::__construct()three optional cap overridesProduction caps: 64 MiB per box, 128 MiB total, 4096 children per superboxJumbfBoxParserDoes not throwThe depth cap is fixed at MAX_DEPTH (8) and is not constructor-tunable.
JumbfBoxParser::parse()string $bytesValidates and materialises the root boxes; empty input yields []list<JumbfBox>JumbfBombException, JumbfCycleDetectedException, JumbfDepthExceededException, MalformedJumbfExceptionStateless; never returns a partial graph; concurrent calls on one instance are safe.
C2paCapabilityStatus::__construct()six named readonly fieldsBuilds an arbitrary descriptor instanceC2paCapabilityStatusDoes not throwcurrent() is the canonical constructor.
C2paCapabilityStatus::current()noneReads the gate live; hard-codes the claim booleansC2paCapabilityStatusDoes not throwgenerallyAvailable and conformanceClaimed are always false.
C2paCapabilityStatus::summary()noneOne-line status textstringDoes not throwPhrased to carry no GA or conformance claim.
Featurestring-backed enum, 1 caseSingle case PREVIEW_C2PA_DRAFT; constant ENV_PREVIEW_C2PA_DRAFTenum caseNothing on case accessScoped stability gate; distinct from license entitlement.
Feature::isEnabled()noneReads getenv() live; strict comparison against the string 1boolDoes not throwAbsent variable or any other value, including 0, true, yes, is off.
ExperimentalC2paEmbedder::__construct()noneFail-closed gate check at construction timeExperimentalC2paEmbedderLogicException when Feature::PREVIEW_C2PA_DRAFT is offNo silent fallback exists.
ExperimentalC2paEmbedder::buildManifestStore()string $sourceBytes, string $producer (non-empty)Builds a draft-shaped Store binding $sourceBytes via SHA-256ManifestStore\JsonException on payload-encode failure; C2paException subclasses from box constructionOmits the c2cs Claim Signature box; the output is unsigned by construction.
interface C2paManifestEmbedder
public function embed(string $pdfBytes, ManifestStore $store): string;
public function extract(string $pdfBytes): ?ManifestStore;
final readonly class ManifestStore
public static function fromBoxes(array $boxes): self
public static function empty(): self
public function isEmpty(): bool
public function toBytes(): string
public function size(): int
final class JumbfBoxParser
public const int MAX_DEPTH = 8;
public const int MAX_PER_BOX_BYTES = 64 * 1024 * 1024;
public const int MAX_TOTAL_BYTES = 128 * 1024 * 1024;
public const int MAX_CHILDREN_PER_SUPERBOX = 4096;
public const array SUPERBOX_TBOXES = ['jumb', 'c2pa', 'c2ma', 'c2as', 'c2cl', 'c2cs', 'c2vc'];
public function __construct(
private readonly int $maxPerBoxBytes = self::MAX_PER_BOX_BYTES,
private readonly int $maxTotalBytes = self::MAX_TOTAL_BYTES,
private readonly int $maxChildrenPerSuperbox = self::MAX_CHILDREN_PER_SUPERBOX,
)
public function parse(string $bytes): array
final readonly class C2paCapabilityStatus
public const string MATURITY_PREVIEW_DRAFT = 'preview-draft';
public function __construct(
public bool $previewEnabled,
public bool $generallyAvailable,
public bool $conformanceClaimed,
public string $maturity,
public string $specPin,
public string $envGate,
)
public static function current(): self
public function summary(): string
enum Feature: string
case PREVIEW_C2PA_DRAFT = 'preview_c2pa_draft';
public const string ENV_PREVIEW_C2PA_DRAFT = 'NEXTPDF_FEATURE_PREVIEW_C2PA_DRAFT';
public function isEnabled(): bool
final class ExperimentalC2paEmbedder
public const string SPEC_PIN_SHA = '4e2afed8f3ace20d41317e2e386c9340d2959d55';
public const string SPEC_PIN_DATE = '2026-04-26';
public function __construct()
public function buildManifestStore(string $sourceBytes, string $producer): ManifestStore
  • Two-layer split. The stable seam (ManifestStore, C2paManifestEmbedder, JumbfBoxParser) is always reachable. Draft synthesis exists only in NextPDF\Pro\Compliance\C2pa\Experimental\ExperimentalC2paEmbedder behind the default-off gate. Extraction and byte-carry never require the gate; synthesis always does.
  • Seam invariants. The C2paManifestEmbedder contract is bytes-only: no in-memory PDF objects cross the seam, implementations perform no network or filesystem I/O, and the seam never assembles claim assertions itself. extract() returns null to signal absence; it never throws for absence.
  • Store semantics. ManifestStore is an immutable ordered list of root JumbfBox instances, per the Manifest Store model of C2PA 2.1 §11.1.1: one JUMBF container aggregating one or more manifests, addressable by URI. It exposes no claim-level accessors. Box order is preserved and is load-bearing for round-trip equality.
  • Hardening caps. JumbfBoxParser unconditionally rejects inputs that exceed any cap: per-box size over 64 MiB, cumulative store over 128 MiB, nesting deeper than 8 levels, or more than 4096 children in one superbox. No policy flag disables these caps. Tighter caps are constructor-injectable for memory-constrained processes.
  • Structural rejection. The parser also rejects, fail-closed: LBox = 0 (BMFF to-EOF), LBox = 1 (XLBox 64-bit length), an LBox smaller than the 8-byte header, truncation past the remaining input, TBox bytes outside printable ASCII (0x20–0x7E), offset re-entry (cycles), and non-exact child tiling of a superbox payload. It never returns a partially constructed graph.
  • Superbox routing. TBox values in SUPERBOX_TBOXES parse recursively as child sequences; every other TBox is a leaf with an opaque payload. cbor is deliberately treated as a leaf for parser safety; upstream layers re-parse its payload when needed.
  • Process gate. Feature::PREVIEW_C2PA_DRAFT is off by default. isEnabled() returns true only when NEXTPDF_FEATURE_PREVIEW_C2PA_DRAFT equals the string 1 exactly. The read is live on every call; nothing is memoised.
  • Fail-closed construction. new ExperimentalC2paEmbedder() throws LogicException while the gate is off. The message names the flag, the environment variable, and the pinned draft SHA and date. A caller cannot reach draft synthesis accidentally.
  • Synthesis shape. buildManifestStore() emits a c2pa superbox containing one c2ma manifest, which holds a c2as assertion store (one c2pa.hash.data assertion) and a c2cl claim. The assertion records a SHA-256 hash assertion over $sourceBytes; because the c2cs Claim Signature box is omitted and the output is unsigned, this is NOT a C2PA hard binding or a provenance verdict — it only follows the structural shape §9.1 describes. Description-box payloads carry a type UUID, toggles 0x03, and a null-terminated UTF-8 label, per C2PA 2.1 §11.1.4.1.1–11.1.4.1.2.
  • No Claim Signature. The c2cs box — per C2PA 2.1 §11.1.4.4 a single CBOR content box labelled c2pa.signature — is intentionally omitted from the synthesized Store. The output is unsigned by construction. This is the profile region judged most likely to drift before the working-group freeze.
  • Draft pin, no BC guarantee. The synthesized wire format is pinned to SPEC_PIN_SHA (4e2afed8…, dated 2026-04-26) of c2pa-org/specifications. It can change without notice and carries no backwards-compatibility guarantee.
  • Status invariant. C2paCapabilityStatus::current() hard-codes generallyAvailable and conformanceClaimed to false. No configuration or environment flag flips either boolean. Only previewEnabled reflects the gate; maturity is the non-claiming token preview-draft.
  • Setting the gate variable to 0, true, yes, on, or an empty string leaves the gate off. Only the exact string 1 enables it.
  • putenv() changes take effect on the next isEnabled() call because the read is live. A gate toggled mid-process is observed immediately.
  • extract() distinguishes two outcomes: null when no Store is present (cheap, exception-free), and a thrown C2paException subclass when a Store is present but hostile or malformed. Absence is never an error; presence plus malformation always is.
  • JumbfBoxParser::parse('') returns the empty list. An empty but present ManifestStore round-trips to itself; the seam does not collapse it to null.
  • Embedding an empty Store MAY return the input unchanged. The seam contract permits this no-op but does not mandate it.
  • Hand-built JumbfBox graphs run the same hardening at construction time: TBox length and ASCII checks, the depth cap, the child-depth invariant, the payload-or-children exclusivity rule, and the per-box size cap. A hand-built bomb fails at construction, not at embed time.
  • Every parser exception carries structured fields — capKind/observed/cap, offset, or kind — so telemetry does not scrape message strings. All subclasses extend C2paException (itself a RuntimeException), which is the umbrella catch type.
  • The parser docblock forbids silently swallowing these exceptions; consumers surface them or remap them with intent.
  • buildManifestStore() encodes JSON payloads with JSON_THROW_ON_ERROR; a $producer string that is not valid UTF-8 fails with \JsonException before any box is built.
  • A well-formed extract() result is a structural statement only. There is no claim validation, no signature verification, and no trust evaluation anywhere on this surface. Recognition is not a provenance verdict.
  • No signing key, certificate, or COSE structure is processed by this surface. The only cryptographic operation is a SHA-256 content hash inside the gated synthesis path.
ClaimStandardClause
Manifests serialize into one JUMBF store holding multiple manifests, addressable by URI.C2PA 2.1§11.1.1 (p63.b)
Description-box labels are null-terminated UTF-8 with excluded ranges; toggles are defined for all Description boxes.C2PA 2.1§11.1.4.1.1–11.1.4.1.2 (p63.a)
The Claim Signature box is labelled c2pa.signature, typed c2cs, and holds a single CBOR content box.C2PA 2.1§11.1.4.4 (p63.c)
A hard binding cryptographically ties a manifest to its asset and exposes modification — the preview’s unsigned hash assertion does NOT meet this bar.C2PA 2.1§9.1 (p57)

All clauses are paraphrased. NextPDF does not reproduce normative text. The statements above are structural-alignment statements about box layout, labels, and bindings. The C2PA-PDF profile is not finalized; the synthesized wire format tracks a pinned draft commit. C2paCapabilityStatus encodes this posture in code: generallyAvailable and conformanceClaimed are false in every configuration. Output from this surface is not a verifiable Content Credential, and no verification path exists in NextPDF.

  • The JUMBF box grammar the parser implements (4-byte big-endian LBox, 4-byte ASCII TBox, payload; superboxes nest child boxes) follows ISO 19566-5; that standard is outside the cited corpus, so the parser behavior is grounded from the product source, not a spec citation.

  • Keep the gate off in production. Draft synthesis adds no durable capability; the emitted bytes are transient and should be re-embedded once a stable adapter ships.

  • Assert ExperimentalC2paEmbedder::SPEC_PIN_SHA against the draft commit your pipeline expects. Run composer c2pa:draft-status in CI (exit 0 fresh, 1 soft-warn, 2 hard-fail) to detect pin staleness.

  • Treat C2paCapabilityStatus::current() as the single source of truth when surfacing C2PA status in tooling or UI. Do not restate its booleans by hand; summary() is safe for logs and status endpoints.

  • Catch C2paException as the umbrella type when consuming extract() or parse(). Map the four subclasses to distinct telemetry counters using their structured fields.

  • Inject tighter caps through the JumbfBoxParser constructor for memory-constrained verifier processes; the defaults are generous production caps.

  • C2paCapabilityStatus::__construct() is public, so a hand-built instance can carry arbitrary booleans. Such an instance is only a value object; it alters no behavior.

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.