Pro editionstability: Experimental
C2PA preview — Deep Reference
At a glance
Section titled “At a glance”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.
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.
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.
Public API surface
Section titled “Public API surface”| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
C2paManifestEmbedder | — | Bytes-only embed/extract SPI; no I/O; no claim synthesis | — | — | Frozen, vendor-neutral seam interface. |
C2paManifestEmbedder::embed() | string $pdfBytes, ManifestStore $store | Embeds $store->toBytes() at the profile-declared location; an empty Store MAY round-trip as a no-op | string new PDF bytes | C2paException on any embed failure (oversized Store, invalid PDF, profile-location collision) | Implementations never mutate or retain the input bytes. |
C2paManifestEmbedder::extract() | string $pdfBytes | Cheap detection probe; the no-Store case allocates almost nothing | ?ManifestStore (null on miss) | C2paException subclass when a Store is present but breaches a hardening invariant | A non-null Store has already passed JumbfBoxParser hardening. |
ManifestStore::fromBoxes() | array $boxes (list<JumbfBox>) | Wraps a parser-validated ordered box list | self | Does not throw itself; hand-built JumbfBox construction enforces the same hardening | The constructor is private; box order is load-bearing for round-trip equality. |
ManifestStore::empty() | none | Store with zero root boxes | self | Does not throw | toBytes() of an empty Store is the empty string. |
ManifestStore::isEmpty() | none | Tests for zero root boxes | bool | Does not throw | — |
ManifestStore::toBytes() | none | Concatenates the root-box serialisations | string | Does not throw | This byte sequence is what an embedder writes. |
ManifestStore::size() | none | Byte length of toBytes() | int (>= 0) | Does not throw | — |
JumbfBoxParser::__construct() | three optional cap overrides | Production caps: 64 MiB per box, 128 MiB total, 4096 children per superbox | JumbfBoxParser | Does not throw | The depth cap is fixed at MAX_DEPTH (8) and is not constructor-tunable. |
JumbfBoxParser::parse() | string $bytes | Validates and materialises the root boxes; empty input yields [] | list<JumbfBox> | JumbfBombException, JumbfCycleDetectedException, JumbfDepthExceededException, MalformedJumbfException | Stateless; never returns a partial graph; concurrent calls on one instance are safe. |
C2paCapabilityStatus::__construct() | six named readonly fields | Builds an arbitrary descriptor instance | C2paCapabilityStatus | Does not throw | current() is the canonical constructor. |
C2paCapabilityStatus::current() | none | Reads the gate live; hard-codes the claim booleans | C2paCapabilityStatus | Does not throw | generallyAvailable and conformanceClaimed are always false. |
C2paCapabilityStatus::summary() | none | One-line status text | string | Does not throw | Phrased to carry no GA or conformance claim. |
Feature | string-backed enum, 1 case | Single case PREVIEW_C2PA_DRAFT; constant ENV_PREVIEW_C2PA_DRAFT | enum case | Nothing on case access | Scoped stability gate; distinct from license entitlement. |
Feature::isEnabled() | none | Reads getenv() live; strict comparison against the string 1 | bool | Does not throw | Absent variable or any other value, including 0, true, yes, is off. |
ExperimentalC2paEmbedder::__construct() | none | Fail-closed gate check at construction time | ExperimentalC2paEmbedder | LogicException when Feature::PREVIEW_C2PA_DRAFT is off | No silent fallback exists. |
ExperimentalC2paEmbedder::buildManifestStore() | string $sourceBytes, string $producer (non-empty) | Builds a draft-shaped Store binding $sourceBytes via SHA-256 | ManifestStore | \JsonException on payload-encode failure; C2paException subclasses from box construction | Omits 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): selfpublic static function empty(): selfpublic function isEmpty(): boolpublic function toBytes(): stringpublic function size(): intfinal 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): arrayfinal 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(): selfpublic function summary(): stringenum Feature: string
case PREVIEW_C2PA_DRAFT = 'preview_c2pa_draft';
public const string ENV_PREVIEW_C2PA_DRAFT = 'NEXTPDF_FEATURE_PREVIEW_C2PA_DRAFT';
public function isEnabled(): boolfinal 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): ManifestStoreBehavior contract
Section titled “Behavior contract”- Two-layer split. The stable seam (
ManifestStore,C2paManifestEmbedder,JumbfBoxParser) is always reachable. Draft synthesis exists only inNextPDF\Pro\Compliance\C2pa\Experimental\ExperimentalC2paEmbedderbehind the default-off gate. Extraction and byte-carry never require the gate; synthesis always does. - Seam invariants. The
C2paManifestEmbeddercontract 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()returnsnullto signal absence; it never throws for absence. - Store semantics.
ManifestStoreis an immutable ordered list of rootJumbfBoxinstances, 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.
JumbfBoxParserunconditionally 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), anLBoxsmaller 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_TBOXESparse recursively as child sequences; every other TBox is a leaf with an opaque payload.cboris deliberately treated as a leaf for parser safety; upstream layers re-parse its payload when needed. - Process gate.
Feature::PREVIEW_C2PA_DRAFTis off by default.isEnabled()returnstrueonly whenNEXTPDF_FEATURE_PREVIEW_C2PA_DRAFTequals the string1exactly. The read is live on every call; nothing is memoised. - Fail-closed construction.
new ExperimentalC2paEmbedder()throwsLogicExceptionwhile 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 ac2pasuperbox containing onec2mamanifest, which holds ac2asassertion store (onec2pa.hash.dataassertion) and ac2clclaim. The assertion records a SHA-256 hash assertion over$sourceBytes; because thec2csClaim 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, toggles0x03, 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
c2csbox — per C2PA 2.1 §11.1.4.4 a single CBOR content box labelledc2pa.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…, dated2026-04-26) ofc2pa-org/specifications. It can change without notice and carries no backwards-compatibility guarantee. - Status invariant.
C2paCapabilityStatus::current()hard-codesgenerallyAvailableandconformanceClaimedtofalse. No configuration or environment flag flips either boolean. OnlypreviewEnabledreflects the gate;maturityis the non-claiming tokenpreview-draft.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- Setting the gate variable to
0,true,yes,on, or an empty string leaves the gate off. Only the exact string1enables it. putenv()changes take effect on the nextisEnabled()call because the read is live. A gate toggled mid-process is observed immediately.extract()distinguishes two outcomes:nullwhen no Store is present (cheap, exception-free), and a thrownC2paExceptionsubclass 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 presentManifestStoreround-trips to itself; the seam does not collapse it tonull.- Embedding an empty Store MAY return the input unchanged. The seam contract permits this no-op but does not mandate it.
- Hand-built
JumbfBoxgraphs 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, orkind— so telemetry does not scrape message strings. All subclasses extendC2paException(itself aRuntimeException), 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 withJSON_THROW_ON_ERROR; a$producerstring that is not valid UTF-8 fails with\JsonExceptionbefore 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.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
| 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.
Development notes
Section titled “Development notes”-
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_SHAagainst the draft commit your pipeline expects. Runcomposer c2pa:draft-statusin 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
C2paExceptionas the umbrella type when consumingextract()orparse(). Map the four subclasses to distinct telemetry counters using their structured fields. -
Inject tighter caps through the
JumbfBoxParserconstructor 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.
See also
Section titled “See also”- C2PA preview-capability status — capability page
- Security — Deep Reference (Pro)
- Compliance — Deep Reference (Pro)
- Post-quantum signing preview — Deep Reference (Enterprise)
- Security / Signing (Core)
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.