C2PA manifest definition
A render can carry Content Credentials. Sealpixel does not sign them. The product of the host is the claim generator: it holds the key and attaches the original. Sealpixel contributes the part that only it knows: the actions that the edit log applied to the pixels, in the vocabulary of the C2PA 2.2 specification, with every redacted region in coordinates of the output. @sealpixel/c2pa writes that as a manifest definition. That is the JSON that c2patool --manifest and the c2pa-rs Builder take as it is.
New to C2PA? C2PA, explained for developers shows the parts of a signed file, the four workflows and the pitfalls.
Write the definition
import { manifestDefinition } from '@sealpixel/c2pa';
import { defaultPlugins } from '@sealpixel/plugins';
const { json } = await manifestDefinition({
log, // getEditLog().log, or manifest.editLog.log from a render manifest
plugins: defaultPlugins,
title: 'result.jpg',
});
await writeFile('result.c2pa.json', json);The native CLI writes the same definition for a plan:
sealpixel render original.jpg plan.json result.jpg --c2pa result.c2pa.jsonmanifestDefinitionFor(manifest, options) takes a render manifest instead of a log. The log, the index and the version of the renderer come from the manifest. The plugins must be the ones that the render used.
Sign it
With c2patool, the host names the original as the parent and signs with its own certificate:
c2patool result.jpg --manifest result.c2pa.json --parent original.jpg --output signed.jpgWith c2pa-rs, the host builds from the definition, adds the original with add_ingredient_from_stream as parentOf, and signs. @sealpixel/c2pa-sign does the same from Node, Deno, Bun or Lambda, with a signer function that you supply. In each case the signing certificate is the host's. c2pa-rs refuses a self-signed certificate, so the smallest setup that works is a CA with a signing certificate under it.
The definition carries no claim_generator_info. That field holds the name and the version of the product that signs, which is the host's.
What is in it
One c2pa.actions assertion with allActionsIncluded: true, in the order in which the stages run:
| Stage | Action |
|---|---|
| The original | Nothing from Sealpixel. The host attaches it as the parentOf ingredient, and the Builder adds c2pa.opened for it, tied to the ingredient as the validator demands |
redact, one per region | c2pa.edited with the region in changes, a description that names the mode, and the mode and the amount as dev.sealpixel.redact parameters |
annotate, one per shape | c2pa.addedText for text. c2pa.drawing for a rectangle, an ellipse, a line, an arrow or a path |
crop | c2pa.cropped. Also c2pa.orientation when the crop rotates or flips, and c2pa.edited for a background fill or an alpha mask |
finetune | c2pa.adjustedColor |
filter | c2pa.filtered |
sticker, one per item | c2pa.edited with the name of the asset as dev.sealpixel.asset. See below for c2pa.placed |
frame | c2pa.edited |
watermark | c2pa.addedText for text. c2pa.edited with the name of the asset for an image |
resize | c2pa.resized |
| A plugin kernel outside the core | c2pa.edited with the name of the plugin. The regions of the later stages assume that the kernel kept the size of the buffer |
The definition never writes c2pa.redacted. In C2PA that label means that assertions were removed from a manifest. It never writes c2pa.watermarked. That label means an invisible soft-binding watermark. It does not write c2pa.placed either, although a sticker is a placed asset. The validator demands that c2pa.placed points at a componentOf ingredient with the bytes of the asset, and only the signer holds those bytes. The definition signs as it is. A host that attaches the assets as ingredients may rewrite those actions.
Every action carries a digitalSourceType. The default is humanEdits from IPTC, because the log holds decisions that a person made. A stage that the host configured, such as a resize from initialOps, is algorithmicallyEnhanced under the conformance rubric. Set it with digitalSourceTypeByStage. The keys are plugin names (crop, fill, finetune, filter, resize, the name of a kernel) or stage kinds (color covers finetune and filter). softwareAgent names the renderer. The default is the version of this core.
The labels are those of the C2PA 2.2 specification. That version added c2pa.adjustedColor and c2pa.addedText and deprecated c2pa.color_adjustments. A reader with an older vocabulary, c2pa-rs 0.91 among them, shows the two as actions without a name. It signs and validates them all the same. pnpm test:c2pa checks that.
Regions
A redact region is in the source's image space. A C2PA region refers to the signed asset, which is the output. The definition moves every region into the output before it writes it:
- Through the transform of the crop stage, when the log has a crop.
- Through the rectangle that a
coverresize keeps. Acontainorfillresize changes no percentage. - Clipped to the frame of the output.
The result is a polygon in percent of the output. A rectangle has four vertices, also under a rotated crop. Each corner that the frame cuts off adds one vertex. A region that the crop removed completely writes no region. The ring runs clockwise on the image. Its first vertex is the top-left corner: the smallest y, then the smallest x. That vertex is also the origin. A flipped crop does not change the order. The mapping lives in the core, so the browser, Node and the native CLI write the same numbers.
The definition never writes a rectangle shape. A rectangle shape carries width and height, and the public Content Credentials Verify site refuses the whole actions assertion when a shape has those two fields. The same site reads vertices. c2patool and the current c2pa-rs read both forms. Vertices are the form that every reader takes. Issue #242 decides whether a plain rectangle goes back to rectangle when that site reads it.
What is checked
The parity script writes the definition of every golden log twice: through the wasm core and through the native CLI. It compares the two, and it compares them with fixtures/c2pa-goldens.json. Then it signs every definition with c2patool and a certificate chain that it makes on the spot, validates the file and reads the actions back. The script runs in CI on every commit.