Using working stores and archives
Working stores and archives provide a standard way to save and restore the state of a Builder:
- A working store is the editable manifest state (including claims, ingredients, assertions) that has not yet been bound to a final asset.
- An archive is a working store serialized to a file or stream (typically a
.c2pafile) using the standard JUMBFapplication/c2paformat.
- Rust
- C++
- Python
Saving a working store to an archive
Use to_archive() to save a Builder to a stream:
use c2pa::{Context, Builder, Result};
use std::io::Cursor;
fn main() -> Result<()> {
let context = Context::new()
.with_settings(include_str!("config.json"))?;
let mut builder = Builder::from_context(context)
.with_definition(r#"{"title": "My Image"}"#)?;
// Save the working store to an archive
let mut archive = Cursor::new(Vec::new());
builder.to_archive(&mut archive)?;
std::fs::write("work.c2pa", archive.get_ref())?;
Ok(())
}
Restoring a working store from an archive
Use from_archive to restore with default Context, or with_archive to restore into a Builder with a custom Context:
use c2pa::{Context, Builder, Result};
use std::io::Cursor;
fn main() -> Result<()> {
// Restore with default context
let builder = Builder::from_archive(
Cursor::new(std::fs::read("work.c2pa")?)
)?;
// Or restore with a custom shared context (preserves settings)
let context = Context::new()
.with_settings(include_str!("config.json"))?;
let builder = Builder::from_context(context)
.with_archive(Cursor::new(std::fs::read("work.c2pa")?))?;
Ok(())
}
Two-phase workflow
Prepare a manifest in one step, sign it later:
use c2pa::{Context, Builder, Result};
use serde_json::json;
use std::io::Cursor;
fn prepare() -> Result<()> {
let context = Context::new()
.with_settings(include_str!("config.json"))?;
let mut builder = Builder::from_context(context)
.with_definition(json!({"title": "Artwork draft"}))?;
builder.add_ingredient_from_stream(
json!({"title": "Sketch", "relationship": "parentOf"}).to_string(),
"image/png",
&mut std::fs::File::open("sketch.png")?,
)?;
let mut archive = Cursor::new(Vec::new());
builder.to_archive(&mut archive)?;
std::fs::write("artwork.c2pa", archive.get_ref())?;
Ok(())
}
fn sign() -> Result<()> {
let context = Context::new()
.with_settings(include_str!("config.json"))?;
let mut builder = Builder::from_context(context)
.with_archive(Cursor::new(std::fs::read("artwork.c2pa")?))?;
let mut source = std::fs::File::open("artwork.jpg")?;
let mut dest = std::fs::File::create("signed_artwork.jpg")?;
builder.save_to_stream("image/jpeg", &mut source, &mut dest)?;
Ok(())
}
Capturing an ingredient as an archive
Save validation results by capturing an ingredient into an archive, then reuse it later.
Capture and sign an archive (no embedded asset):
let signer = context.signer()?;
let ingredient_c2pa = builder.sign(
signer,
"application/c2pa",
&mut io::empty(),
&mut io::empty(),
)?;
This returns the raw C2PA manifest store as Vec<u8>.
Later, you can add that archived ingredient to a new manifest as follows:
let mut builder = Builder::from_shared_context(&context)
.with_definition(
json!({
"title": "New Title",
"relationship": "componentOf"
})
)?;
builder.add_ingredient_from_stream(
json!({
"title": "Archived Ingredient",
"relationship": "componentOf",
"label": "ingredient_1"
})
.to_string(),
"application/c2pa",
&mut Cursor::new(ingredient_c2pa),
)?;
builder.add_action(json!({
"action": "c2pa.placed",
"parameters": { "ingredientIds": ["ingredient_1"] }
}))?;
Calling add_ingredient_from_stream() with format "application/c2pa":
- Reads the archive.
- Extracts the first ingredient from the active manifest.
- Merges with provided JSON properties, but your overrides take precedence.
Saving a working store to an archive
Use to_archive() to save a Builder to a file or stream:
#include "c2pa.hpp"
#include <fstream>
c2pa::Context context;
auto builder = c2pa::Builder(context, manifest_json);
builder.add_resource("thumbnail", "thumbnail.jpg");
builder.add_ingredient(ingredient_json, "source.jpg");
// Save working store to archive file
builder.to_archive("manifest.c2pa");
// Or save to a stream
std::ofstream archive_stream("manifest.c2pa", std::ios::binary);
builder.to_archive(archive_stream);
archive_stream.close();
Restoring a working store from an archive
Use from_archive to create a new Builder from an archive, or with_archive to load an archive into an existing Builder while preserving its Context:
#include "c2pa.hpp"
#include <fstream>
// Restore from file (default context)
auto builder = c2pa::Builder::from_archive("manifest.c2pa");
// Or restore with a custom context (preserves settings)
c2pa::Context context(R"({
"builder": {
"thumbnail": {"enabled": false}
}
})");
auto builder = c2pa::Builder(context);
std::ifstream archive_stream("manifest.c2pa", std::ios::binary);
builder.with_archive(archive_stream);
archive_stream.close();
// Sign with the restored working store
builder.sign("asset.jpg", "signed.jpg", signer);
Two-phase workflow
Prepare a manifest in one step, and sign it later:
Phase 1: Prepare manifest
void prepare_manifest() {
const std::string manifest_json = R"({
"title": "Artwork draft",
"assertions": [ ... ]
})";
c2pa::Context context;
auto builder = c2pa::Builder(context, manifest_json);
builder.add_resource("thumbnail", "thumb.jpg");
builder.add_ingredient("{\"title\": \"Sketch\"}", "sketch.png");
// Save working store as archive (C2PA JUMBF format)
builder.to_archive("artwork_manifest.c2pa");
std::cout << "Working store saved to artwork_manifest.c2pa" << std::endl;
}
Phase 2: Sign the asset
void sign_asset() {
// Restore the working store
auto builder = c2pa::Builder::from_archive("artwork_manifest.c2pa");
// Create signer using HSM (not shown)
auto signer = create_hsm_signer();
// Sign
builder.sign("artwork.jpg", "signed_artwork.jpg", signer);
std::cout << "Asset signed with manifest store" << std::endl;
}
Linking an ingredient archive to an action
To link an ingredient archive to an action via ingredientIds, you must use a label set in the add_ingredient call on the signing builder. Labels baked into the archive ingredient are not carried through, and instance_id does not work as a linking key for ingredient archives regardless of where it is set.
c2pa::Context context;
// Step 1: Create the ingredient archive.
auto manifest_str = read_file("training.json");
auto archive_builder = c2pa::Builder(context, manifest_str);
archive_builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "componentOf"})",
"photo.jpg");
archive_builder.to_archive("ingredient.c2pa");
// Step 2: Build a manifest with an action that references the ingredient.
auto manifest_json = R"({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [{
"action": "c2pa.placed",
"parameters": {
"ingredientIds": ["my-ingredient"]
}
}]
}
}]
})";
auto builder = c2pa::Builder(context, manifest_json);
// Step 3: Add the ingredient archive with a label matching the ingredientIds value.
// The label MUST be set here, on the signing builder's add_ingredient call.
builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"})",
"ingredient.c2pa");
builder.sign("source.jpg", "signed.jpg", signer);
When linking multiple ingredient archives, give each a distinct label and reference it in the appropriate action's ingredientIds array.
If each ingredient has its own action (e.g., one c2pa.opened for the parent and one c2pa.placed for a composited element), set up two actions with separate ingredientIds:
auto manifest_json = R"({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [
{
"action": "c2pa.opened",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
"parameters": { "ingredientIds": ["parent-photo"] }
},
{
"action": "c2pa.placed",
"parameters": { "ingredientIds": ["overlay-graphic"] }
}
]
}
}]
})";
auto builder = c2pa::Builder(context, manifest_json);
builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "parentOf", "label": "parent-photo"})",
"photo_archive.c2pa");
builder.add_ingredient(
R"({"title": "overlay.png", "relationship": "componentOf", "label": "overlay-graphic"})",
"overlay_archive.c2pa");
builder.sign("source.jpg", "signed.jpg", signer);
A single c2pa.placed action can also reference several componentOf ingredients composited together. List all labels in the ingredientIds array:
auto manifest_json = R"({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [{
"action": "c2pa.placed",
"parameters": {
"ingredientIds": ["base-layer", "overlay-layer"]
}
}]
}
}]
})";
auto builder = c2pa::Builder(context, manifest_json);
builder.add_ingredient(
R"({"title": "base.jpg", "relationship": "componentOf", "label": "base-layer"})",
"base_ingredient.c2pa");
builder.add_ingredient(
R"({"title": "overlay.jpg", "relationship": "componentOf", "label": "overlay-layer"})",
"overlay_ingredient.c2pa");
builder.sign("source.jpg", "signed.jpg", signer);
After signing, the action's parameters.ingredients array contains one resolved URL per ingredient.
Saving a working store to an archive
Use to_archive() to save a Builder to a stream:
import io
import json
from c2pa import Context, Builder
ctx = Context.from_dict({
"builder": {
"claim_generator_info": {"name": "My App", "version": "0.1.0"}
}
})
builder = Builder(manifest_json, context=ctx)
with open("thumbnail.jpg", "rb") as thumb:
builder.add_resource("thumbnail", thumb)
ingredient_json = json.dumps({
"title": "Original asset",
"relationship": "parentOf"
})
with open("source.jpg", "rb") as ingredient:
builder.add_ingredient(ingredient_json, "image/jpeg", ingredient)
# Save working store to archive
archive = io.BytesIO()
builder.to_archive(archive)
# Write to a file
with open("manifest.c2pa", "wb") as f:
archive.seek(0)
f.write(archive.read())
Restoring a working store from an archive
Use with_archive() to load an archive into a Builder while preserving its Context:
from c2pa import Context, Builder
ctx = Context.from_dict({
"builder": {
"thumbnail": {"enabled": False},
"claim_generator_info": {"name": "My App", "version": "0.1.0"}
}
})
# Create builder with context, then load archive into it
with open("manifest.c2pa", "rb") as archive:
builder = Builder({}, context=ctx)
builder.with_archive(archive)
# The builder has the archived manifest definition
# but keeps the context settings
with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst:
builder.sign(signer, "image/jpeg", src, dst)
Two-phase workflow
Prepare a manifest in one step, sign it later:
Phase 1: Prepare
import io
import json
from c2pa import Context, Builder
ctx = Context.from_dict({
"builder": {
"claim_generator_info": {"name": "My App", "version": "0.1.0"}
}
})
manifest_json = json.dumps({
"title": "Artwork draft",
"assertions": []
})
builder = Builder(manifest_json, context=ctx)
with open("thumb.jpg", "rb") as thumb:
builder.add_resource("thumbnail", thumb)
with open("sketch.png", "rb") as sketch:
builder.add_ingredient(
json.dumps({"title": "Sketch"}), "image/png", sketch
)
with open("artwork_manifest.c2pa", "wb") as f:
builder.to_archive(f)
Phase 2: Sign
from c2pa import Context, Builder
ctx = Context.from_dict({
"builder": {"thumbnail": {"enabled": False}}
})
with open("artwork_manifest.c2pa", "rb") as archive:
builder = Builder({}, context=ctx)
builder.with_archive(archive)
with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst:
builder.sign(signer, "image/jpeg", src, dst)
Linking an ingredient archive to an action
To link an ingredient archive to an action via ingredientIds, you must use a label set in the add_ingredient() call on the signing builder. Labels baked into the archive ingredient are not carried through, and instance_id does not work as a linking key for ingredient archives regardless of where it is set.
Step 1: Create the ingredient archive
import io, json
archive_builder = Builder.from_json({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [],
})
with open("photo.jpg", "rb") as f:
archive_builder.add_ingredient(
{"title": "photo.jpg", "relationship": "componentOf"},
"image/jpeg",
f,
)
archive = io.BytesIO()
archive_builder.to_archive(archive)
archive.seek(0)
Step 2: Build a manifest with an action that references the ingredient
manifest_json = {
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [
{
"label": "c2pa.actions.v2",
"data": {
"actions": [
{
"action": "c2pa.placed",
"parameters": {
"ingredientIds": ["my-ingredient"]
},
}
]
},
}
],
}
ctx = Context.from_dict({"signer": signer})
builder = Builder(manifest_json, context=ctx)
Step 3: Add the ingredient archive
The label must match the ingredientIds value and MUST be set here, on the signing builder's add_ingredient() call.
builder.add_ingredient(
{"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"},
"application/c2pa",
archive,
)
with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst:
builder.sign("image/jpeg", src, dst)
When linking multiple ingredient archives, give each a distinct label and reference it in the appropriate action's ingredientIds array.
If each ingredient has its own action (e.g., one c2pa.opened for the parent and one c2pa.placed for a composited element), set up two actions with separate ingredientIds:
manifest_json = {
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [
{
"action": "c2pa.opened",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
"parameters": {"ingredientIds": ["parent-photo"]},
},
{
"action": "c2pa.placed",
"parameters": {"ingredientIds": ["overlay-graphic"]},
},
]
},
}],
}
builder = Builder(manifest_json, context=ctx)
builder.add_ingredient(
{"title": "photo.jpg", "relationship": "parentOf", "label": "parent-photo"},
"application/c2pa",
photo_archive,
)
builder.add_ingredient(
{"title": "overlay.png", "relationship": "componentOf", "label": "overlay-graphic"},
"application/c2pa",
overlay_archive,
)