From 2f31f42ce887c44aeb457e6b0462ef099d08780d Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 11:28:57 -0800 Subject: [PATCH 01/18] feat: Drop the WIP docs --- docs/tmn-wip-docs/context.md | 588 +++++++++++++++++++ docs/tmn-wip-docs/intents.md.md | 486 ++++++++++++++++ docs/tmn-wip-docs/selective-manifests.md | 708 +++++++++++++++++++++++ docs/tmn-wip-docs/settings.md | 449 ++++++++++++++ docs/tmn-wip-docs/usage.md | 196 +++++++ docs/tmn-wip-docs/working-stores.md | 648 +++++++++++++++++++++ 6 files changed, 3075 insertions(+) create mode 100644 docs/tmn-wip-docs/context.md create mode 100644 docs/tmn-wip-docs/intents.md.md create mode 100644 docs/tmn-wip-docs/selective-manifests.md create mode 100644 docs/tmn-wip-docs/settings.md create mode 100644 docs/tmn-wip-docs/usage.md create mode 100644 docs/tmn-wip-docs/working-stores.md diff --git a/docs/tmn-wip-docs/context.md b/docs/tmn-wip-docs/context.md new file mode 100644 index 00000000..ccc84145 --- /dev/null +++ b/docs/tmn-wip-docs/context.md @@ -0,0 +1,588 @@ +# Using Context to configure the SDK + +Use the `Context` class to configure how `Reader`, `Builder`, and other aspects of the SDK operate. + +## What is Context? + +Context encapsulates SDK configuration: + +- **Settings**: Verification options, [Builder behavior](#configuring-builder), [Reader trust configuration](#configuring-reader), thumbnail configuration, and more. See [Using settings](settings.md) for complete details. +- [**Signer configuration**](#configuring-a-signer): Optional signer credentials that can be stored in the Context for reuse. +- **State isolation**: Each `Context` is independent, allowing different configurations to coexist in the same application. + +### Why use Context? + +`Context` is better than the deprecated global `load_settings()` function because it: + +- **Makes dependencies explicit**: Configuration is passed directly to `Reader` and `Builder`, not hidden in global state. +- **Enables multiple configurations**: Run different configurations simultaneously. For example, one for development with test certificates, another for production with strict validation. +- **Eliminates global state**: Each `Reader` and `Builder` gets its configuration from the `Context` you pass, avoiding subtle bugs from shared state. +- **Simplifies testing**: Create isolated configurations for tests without worrying about cleanup or interference between them. +- **Improves code clarity**: Reading `Builder(manifest_json, context=ctx)` immediately shows that configuration is being used. + +### Class diagram + +This diagram shows the public classes in the SDK and their relationships. + +```mermaid +classDiagram + direction LR + + class Settings { + +from_json(json_str) Settings$ + +from_dict(config) Settings$ + +set(path, value) Settings + +update(data, format) Settings + +close() + +is_valid bool + } + + class ContextProvider { + <> + +is_valid bool + } + + class Context { + +from_json(json_str, signer) Context$ + +from_dict(config, signer) Context$ + +has_signer bool + +is_valid bool + +close() + } + + class Reader { + +get_supported_mime_types() list~str~$ + +try_create(format_or_path, stream, manifest_data, context) Reader | None$ + +json() str + +detailed_json() str + +get_active_manifest() dict | None + +get_manifest(label) dict + +get_validation_state() str | None + +get_validation_results() dict | None + +resource_to_stream(uri, stream) int + +is_embedded() bool + +get_remote_url() str | None + +close() + } + + class Builder { + +from_json(manifest_json, context) Builder$ + +from_archive(stream, context) Builder$ + +get_supported_mime_types() list~str~$ + +set_no_embed() + +set_remote_url(url) + +set_intent(intent, digital_source_type) + +add_resource(uri, stream) + +add_ingredient(json, format, source) + +add_action(action_json) + +to_archive(stream) + +sign(signer, format, source, dest) bytes + +sign_file(source_path, dest_path, signer) bytes + +close() + } + + class Signer { + +from_info(signer_info) Signer$ + +from_callback(callback, alg, certs, tsa_url) Signer$ + +reserve_size() int + +close() + } + + class C2paSignerInfo { + <> + +alg + +sign_cert + +private_key + +ta_url + } + + class C2paSigningAlg { + <> + ES256 + ES384 + ES512 + PS256 + PS384 + PS512 + ED25519 + } + + class C2paBuilderIntent { + <> + CREATE + EDIT + UPDATE + } + + class C2paDigitalSourceType { + <> + DIGITAL_CAPTURE + DIGITAL_CREATION + TRAINED_ALGORITHMIC_MEDIA + ... + } + + class C2paError { + <> + +message str + } + + class C2paError_Subtypes { + <> + ManifestNotFound + NotSupported + Json + Io + Verify + Signature + ... + } + + ContextProvider <|.. Context : satisfies + Settings --> Context : optional input + Signer --> Context : optional, consumed + C2paSignerInfo --> Signer : creates via from_info + C2paSigningAlg --> C2paSignerInfo : alg field + C2paSigningAlg --> Signer : from_callback alg + Context --> Reader : optional context= + Context --> Builder : optional context= + Signer --> Builder : sign(signer) + C2paBuilderIntent --> Builder : set_intent + C2paDigitalSourceType --> Builder : set_intent + C2paError --> C2paError_Subtypes : subclasses +``` + +> [!NOTE] +> The deprecated `load_settings()` function still works for backward compatibility but you are encouraged to migrate your code to use `Context`. See [Migrating from load_settings](#migrating-from-load_settings). + +## Creating a Context + +There are several ways to create a `Context`, depending on your needs: + +- [Using SDK default settings](#using-sdk-default-settings) +- [From a JSON string](#from-a-json-string) +- [From a dictionary](#from-a-dictionary) +- [From a Settings object](#from-a-settings-object) + +### Using SDK default settings + +The simplest approach is using [SDK default settings](settings.md#default-configuration). + +**When to use:** For quick prototyping, or when you're happy with default behavior (verification enabled, thumbnails enabled at 1024px, and so on). + +```py +from c2pa import Context + +ctx = Context() # Uses SDK defaults +``` + +### From a JSON string + +You can create a `Context` directly from a JSON configuration string. + +**When to use:** For simple configuration that doesn't need to be shared across the codebase, or when hard-coding settings for a specific purpose (for example, a utility script). + +```py +ctx = Context.from_json('''{ + "verify": {"verify_after_sign": true}, + "builder": { + "thumbnail": {"enabled": false}, + "claim_generator_info": {"name": "An app", "version": "0.1.0"} + } +}''') +``` + +### From a dictionary + +You can create a `Context` from a Python dictionary. + +**When to use:** When you want to build configuration programmatically using native Python data structures. + +```py +ctx = Context.from_dict({ + "verify": {"verify_after_sign": True}, + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "An app", "version": "0.1.0"} + } +}) +``` + +### From a Settings object + +You can build a `Settings` object programmatically, then create a `Context` from that. + +**When to use:** For configuration that needs runtime logic (such as conditional settings based on environment), or when you want to build settings incrementally. + +```py +from c2pa import Settings, Context + +settings = Settings() +settings.set("builder.thumbnail.enabled", "false") +settings.set("verify.verify_after_sign", "true") +settings.update({ + "builder": { + "claim_generator_info": {"name": "An app", "version": "0.1.0"} + } +}) + +ctx = Context(settings=settings) +``` + +## Common configuration patterns + +### Development environment with test certificates + +During development, you often need to trust self-signed or custom CA certificates: + +```py +# Load your test root CA +with open("test-ca.pem", "r") as f: + test_ca = f.read() + +ctx = Context.from_dict({ + "trust": { + "user_anchors": test_ca + }, + "verify": { + "verify_after_reading": True, + "verify_after_sign": True, + "remote_manifest_fetch": False, + "ocsp_fetch": False + }, + "builder": { + "claim_generator_info": {"name": "Dev Build", "version": "dev"}, + "thumbnail": {"enabled": False} + } +}) +``` + +### Configuration from environment variables + +Adapt configuration based on the runtime environment: + +```py +import os + +env = os.environ.get("ENVIRONMENT", "dev") + +settings = Settings() +if env == "production": + settings.update({"verify": {"strict_v1_validation": True}}) +else: + settings.update({"verify": {"remote_manifest_fetch": False}}) + +ctx = Context(settings=settings) +``` + +### Layered configuration + +Load base configuration and apply runtime overrides: + +```py +import json + +# Load base configuration from a file +with open("config/base.json", "r") as f: + base_config = json.load(f) + +settings = Settings.from_dict(base_config) + +# Apply environment-specific overrides +settings.update({"builder": {"claim_generator_info": {"version": app_version}}}) + +ctx = Context(settings=settings) +``` + +For the full list of settings and defaults, see [Using settings](settings.md). + +## Configuring Reader + +Use `Context` to control how `Reader` validates manifests and handles remote resources, including: + +- **Verification behavior**: Whether to verify after reading, check trust, and so on. +- [**Trust configuration**](#trust-configuration): Which certificates to trust when validating signatures. +- [**Network access**](#offline-operation): Whether to fetch remote manifests or OCSP responses. + +> [!IMPORTANT] +> `Context` is used only at construction. `Reader` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Reader`. + +```py +ctx = Context.from_dict({"verify": {"remote_manifest_fetch": False}}) +reader = Reader("image.jpg", context=ctx) +``` + +### Reading from a file + +```py +ctx = Context.from_dict({ + "verify": { + "remote_manifest_fetch": False, + "ocsp_fetch": False + } +}) + +reader = Reader("image.jpg", context=ctx) +print(reader.json()) +``` + +### Reading from a stream + +```py +with open("image.jpg", "rb") as stream: + reader = Reader("image/jpeg", stream, context=ctx) + print(reader.json()) +``` + +### Trust configuration + +Example of trust configuration in a settings dictionary: + +```py +ctx = Context.from_dict({ + "trust": { + "user_anchors": "-----BEGIN CERTIFICATE-----\nMIICEzCCA...\n-----END CERTIFICATE-----", + "trust_config": "1.3.6.1.4.1.311.76.59.1.9\n1.3.6.1.4.1.62558.2.1" + } +}) + +reader = Reader("signed_asset.jpg", context=ctx) +``` + +### Full validation + +To configure full validation, with all verification features enabled: + +```py +ctx = Context.from_dict({ + "verify": { + "verify_after_reading": True, + "verify_trust": True, + "verify_timestamp_trust": True, + "remote_manifest_fetch": True + } +}) + +reader = Reader("asset.jpg", context=ctx) +``` + +For more information, see [Settings - Verify](settings.md#verify). + +### Offline operation + +To configure `Reader` to work with no network access: + +```py +ctx = Context.from_dict({ + "verify": { + "remote_manifest_fetch": False, + "ocsp_fetch": False + } +}) + +reader = Reader("local_asset.jpg", context=ctx) +``` + +For more information, see [Settings - Offline or air-gapped environments](settings.md#offline-or-air-gapped-environments). + +## Configuring Builder + +`Builder` uses `Context` to control how to create and sign C2PA manifests. The `Context` affects: + +- **Claim generator information**: Application name, version, and metadata embedded in the manifest. +- **Thumbnail generation**: Whether to create thumbnails, size, quality, and format. +- **Action tracking**: Auto-generation of actions like `c2pa.created`, `c2pa.opened`, `c2pa.placed`. +- **Intent**: The purpose of the claim (create, edit, or update). +- **Verification after signing**: Whether to validate the manifest immediately after signing. +- **Signer configuration** (optional): Credentials can be stored in the context for reuse. + +> [!IMPORTANT] +> The `Context` is used only when constructing the `Builder`. The `Builder` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Builder`. + +### Basic use + +```py +ctx = Context.from_dict({ + "builder": { + "claim_generator_info": { + "name": "An app", + "version": "0.1.0" + }, + "intent": {"Create": "digitalCapture"} + } +}) + +builder = Builder(manifest_json, context=ctx) + +# Pass signer explicitly at signing time +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +### Controlling thumbnail generation + +```py +# Disable thumbnails for faster processing +no_thumbnails_ctx = Context.from_dict({ + "builder": { + "claim_generator_info": {"name": "Batch Processor"}, + "thumbnail": {"enabled": False} + } +}) + +# Or customize thumbnail size and quality for mobile +mobile_ctx = Context.from_dict({ + "builder": { + "claim_generator_info": {"name": "Mobile App"}, + "thumbnail": { + "enabled": True, + "long_edge": 512, + "quality": "low", + "prefer_smallest_format": True + } + } +}) +``` + +## Configuring a signer + +You can configure a signer in two ways: + +- [From Settings (signer-on-context)](#from-settings) +- [Explicit signer passed to sign()](#explicit-signer) + +### From Settings + +Create a `Signer` and pass it to the `Context`. The signer is **consumed** — the `Signer` object becomes invalid after this call and must not be reused. The `Context` takes ownership of the underlying native signer. + +```py +from c2pa import Context, Settings, Builder, Signer, C2paSignerInfo, C2paSigningAlg + +# Create a signer +signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, + sign_cert=cert_data, + private_key=key_data, + ta_url=b"http://timestamp.digicert.com" +) +signer = Signer.from_info(signer_info) + +# Create context with signer (signer is consumed) +ctx = Context(settings=settings, signer=signer) +# signer is now invalid and must not be used again + +# Build and sign — no signer argument needed +builder = Builder(manifest_json, context=ctx) +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign(format="image/jpeg", source=src, dest=dst) +``` + +> [!NOTE] +> Signer-on-context requires a compatible version of the native c2pa-c library. If the library does not support this feature, a `C2paError` is raised when passing a `Signer` to `Context`. + +### Explicit signer + +For full programmatic control, create a `Signer` and pass it directly to `Builder.sign()`: + +```py +signer = Signer.from_info(signer_info) +builder = Builder(manifest_json, context=ctx) + +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +If both an explicit signer and a context signer are available, the explicit signer always takes precedence: + +```py +# Explicit signer wins over context signer +builder.sign(explicit_signer, "image/jpeg", source, dest) +``` + +## Context lifetime and usage + +### Context as a context manager + +`Context` supports the `with` statement for automatic resource cleanup: + +```py +with Context() as ctx: + reader = Reader("image.jpg", context=ctx) + print(reader.json()) +# Resources are automatically released +``` + +### Reusable contexts + +You can reuse the same `Context` to create multiple readers and builders: + +```py +ctx = Context(settings=settings) + +# All three use the same configuration +builder1 = Builder(manifest1, context=ctx) +builder2 = Builder(manifest2, context=ctx) +reader = Reader("image.jpg", context=ctx) + +# Context can be closed after construction; readers/builders still work +``` + +### Multiple contexts for different purposes + +Use different `Context` objects when you need different settings; for example, for development vs. production, or different trust configurations: + +```py +dev_ctx = Context(settings=dev_settings) +prod_ctx = Context(settings=prod_settings) + +# Different builders with different configurations +dev_builder = Builder(manifest, context=dev_ctx) +prod_builder = Builder(manifest, context=prod_ctx) +``` + +### ContextProvider protocol + +The `ContextProvider` protocol allows third-party implementations of custom context providers. Any class that implements `is_valid` and `_c_context` properties satisfies the protocol and can be passed to `Reader` or `Builder` as `context`. + +```py +from c2pa import ContextProvider, Context + +# The built-in Context satisfies ContextProvider +ctx = Context() +assert isinstance(ctx, ContextProvider) # True +``` + +## Migrating from load_settings + +The `load_settings()` function is deprecated. Replace it with `Settings` and `Context`: + +| Aspect | load_settings (legacy) | Context | +|--------|------------------------|---------| +| Scope | Global state | Per Reader/Builder, passed explicitly | +| Multiple configs | Not supported | One context per configuration | +| Testing | Shared global state | Isolated contexts per test | + +**Deprecated:** + +```py +from c2pa import load_settings, Reader + +load_settings({"builder": {"thumbnail": {"enabled": False}}}) +reader = Reader("image.jpg") # uses global settings +``` + +**Using current APIs:** + +```py +from c2pa import Settings, Context, Reader + +settings = Settings.from_dict({"builder": {"thumbnail": {"enabled": False}}}) +ctx = Context(settings=settings) +reader = Reader("image.jpg", context=ctx) +``` + +## See also + +- [Using settings](settings.md) — schema, property reference, and examples. +- [Usage](usage.md) — reading and signing with Reader and Builder. +- [CAI settings schema](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/): full schema reference. diff --git a/docs/tmn-wip-docs/intents.md.md b/docs/tmn-wip-docs/intents.md.md new file mode 100644 index 00000000..5b331d9f --- /dev/null +++ b/docs/tmn-wip-docs/intents.md.md @@ -0,0 +1,486 @@ +# Using Builder intents + +Intents enable validation, add required default actions, and help prevent invalid operations when using a `Builder`. Intents are about the operation (create, edit, update) executed on the source asset. + +## Why use intents? + +Without intents, the caller must manually construct the correct manifest structure to be compliant with the specification: adding the required actions (`c2pa.created` or `c2pa.opened` as first action as per specification), setting digital source types, managing ingredients, and linking actions to ingredients. Getting any of this wrong produces a manifest that does not comply with the C2PA specification. + +With intents, the caller declares *what is being done* and the Builder handles the rest: + +```py +# Without intents: a caller must manually wire things up, and make sure ingredients are properly linked to actions. +# This is especially important in the case of parentOf ingredient relationships, with the c2pa.opened action +with Builder({ + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + } + ] + }, + } + ], +}) as builder: + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) + +# With intents: the Builder generates the actions automatically +with Builder({}) as builder: + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, + ) + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Both ways of writing the code produce the same signed manifest. With intents the Builder validates the setup and fills in the spec-required structure. + +## Setting the intent + +There are three ways to set the intent on a `Builder` object instance. The intent determines which actions the Builder auto-generates at sign time. + +### Using Context + +Pass the intent through a `Context` object when creating the `Builder`. This is the an approach that keeps intent configuration alongside other builder settings such as `claim_generator_info` and `thumbnail`. Note that the context is created from settings, so you can modulate the settings for each context. + +```py +from c2pa import Context, Builder + +ctx = Context.from_dict({ + "builder": { + "intent": {"Create": "digitalCapture"}, + "claim_generator_info": {"name": "My App", "version": "1.0.0"}, + } +}) + +with Builder({}, context=ctx) as builder: + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +The same `Context` can be reused across multiple `Builder` instances, ensuring consistent configuration: + +```py +ctx = Context.from_dict({ + "builder": { + "intent": "edit", + "claim_generator_info": {"name": "Batch Editor"}, + } +}) + +for path in image_paths: + with Builder({}, context=ctx) as builder: + builder.sign_file(path, output_path(path), signer) +``` + +### Using `set_intent` on the Builder + +Call `set_intent` directly on a `Builder` instance. This is useful for one-off operations or when the intent needs to be determined at runtime: + +```py +with Builder({}) as builder: + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, + ) + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +If both a `Context` intent and a `set_intent` call are present, the `set_intent` call takes precedence. + +### Using `load_settings` (deprecated) + +The global `load_settings` function can configure the intent for all subsequent `Builder` instances. This approach is deprecated in favor of context-based APIs: + +```py +from c2pa import load_settings, Builder + +# Deprecated: sets intent globally +load_settings({"builder": {"intent": "edit"}}) + +with Builder({}) as builder: + with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Intent setting precedence + +When an intent is configured in multiple places, the most specific setting wins: + +```mermaid +flowchart TD + Check{Was set_intent called + on the Builder?} + Check --> |Yes| UseSetIntent["Use set_intent value"] + Check --> |No| CheckCtx{Was a Context with + builder.intent provided?} + CheckCtx --> |Yes| UseCtx["Use Context intent"] + CheckCtx --> |No| CheckGlobal{Was load_settings called + with builder.intent?} + CheckGlobal --> |Yes| UseGlobal["Use global intent + (deprecated)"] + CheckGlobal --> |No| NoIntent["No intent set. + Caller must define actions + manually in manifest JSON."] +``` + +## How intents relate to the source stream + +The intent **operates on the source stream** passed to `sign()`. It does not target a specific ingredient added via `add_ingredient`: it targets the source asset itself (and ONLY the source). + +The following diagram shows what happens at sign time for each intent: + +```mermaid +flowchart LR + subgraph CREATE + S1[source stream] --> B1[Builder] + B1 --> O1[signed output] + B1 -. adds .-> A1["c2pa.created action + + digital source type"] + end +``` + +```mermaid +flowchart LR + subgraph EDIT + S2[source stream] --> B2[Builder] + B2 --> O2[signed output] + S2 -. auto-created as .-> P2[parentOf ingredient] + P2 --> B2 + B2 -. adds .-> A2["c2pa.opened action + linked to parent"] + end +``` + +```mermaid +flowchart LR + subgraph UPDATE + S3[source stream] --> B3[Builder] + B3 --> O3[signed output] + S3 -. auto-created as .-> P3[parentOf ingredient] + P3 --> B3 + B3 -. adds .-> A3["c2pa.opened action + linked to parent"] + B3 -. restricts .-> R3[content must not change] + end +``` + +For **EDIT** and **UPDATE**, the Builder looks at the source stream, and if no `parentOf` ingredient has been added manually, it automatically creates one from that stream (and adds the needed action). The source stream *becomes* the parent ingredient. If a `parentOf` ingredient has already been added manually (via `add_ingredient`), the Builder uses that one instead and does not auto-create one from the source. + +### How intent relates to `add_ingredient` + +The intent and manually-added ingredients serve different roles. **The intent controls what the Builder does with the source stream** at sign time. The `add_ingredient` method **adds other ingredients explicitly**. + +```mermaid +flowchart TD + Intent["Intent + (via Context, set_intent, + or load_settings)"] --> Q{Intent type?} + Q --> |CREATE| CreateFlow["No parent allowed + Source stream is new content"] + Q --> |EDIT or UPDATE| EditFlow{Was a parentOf ingredient + added via add_ingredient?} + EditFlow --> |No| Auto["Builder auto-creates + parentOf from source stream"] + EditFlow --> |Yes| Manual["Builder uses the + manually-added parent"] + Auto --> Opened["Builder adds c2pa.opened + action linked to parent"] + Manual --> Opened + CreateFlow --> Created["Builder adds c2pa.created + action + digital source type"] + + AddIngredient["add_ingredient()"] --> IngType{relationship?} + IngType --> |parentOf| ParentIng["Overrides auto-parent + for EDIT/UPDATE"] + IngType --> |componentOf| CompIng["Additional ingredient + not affected by intent"] + ParentIng --> EditFlow +``` + +## Import + +```py +from c2pa import ( + Builder, + Reader, + Signer, + Context, + Settings, + C2paSignerInfo, + C2paSigningAlg, + C2paBuilderIntent, + C2paDigitalSourceType, +) +``` + +## Intent types + +| Intent | Operation | Parent ingredient | Auto-generated action | +| --- | --- | --- | --- | +| `CREATE` | Brand-new content | Must NOT have one | `c2pa.created` | +| `EDIT` | Modifying existing content | Auto-created from the source stream if not provided | `c2pa.opened` (linked to parent) | +| `UPDATE` | Metadata-only changes | Auto-created from the source stream if not provided | `c2pa.opened` (linked to parent) | + +## Choosing the right intent + +```mermaid +flowchart TD + Start([Start]) --> HasParent{Does the asset have + prior history?} + HasParent --> |No| IsNew[Brand-new content] + IsNew --> CREATE["Use CREATE + + C2paDigitalSourceType"] + HasParent --> |Yes| ContentChanged{Will the content + itself change?} + ContentChanged --> |Yes| EDIT[Use EDIT] + ContentChanged --> |No, metadata only| UPDATE[Use UPDATE] + ContentChanged --> |Need full manual control| MANUAL["Skip intents. + Define actions and ingredients + directly in manifest JSON."] +``` + +## CREATE intent + +Use `CREATE` when the asset is a brand-new creation with no prior history. In this case, a `C2paDigitalSourceType` is required (by the specification) to describe how the asset was produced. The Builder will: + +- Add a `c2pa.created` action with the specified digital source type. +- Reject the operation if a `parentOf` ingredient exists (new creations cannot have parents). + +### Example: New digital creation + +Using `Context`: + +```py +ctx = Context.from_dict({ + "builder": {"intent": {"Create": "digitalCreation"}} +}) + +with Builder({}, context=ctx) as builder: + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Using `set_intent`: + +```py +with Builder({}) as builder: + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.DIGITAL_CREATION, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Example: Marking AI-generated content + +```py +ctx = Context.from_dict({ + "builder": {"intent": {"Create": "trainedAlgorithmicMedia"}} +}) + +with Builder({}, context=ctx) as builder: + with open("ai_output.jpg", "rb") as source, open("signed_ai_output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Example: CREATE with additional manifest metadata + +`Context` and a manifest definition can be combined. The context handles the intent, while the manifest definition provides additional metadata and assertions: + +```py +ctx = Context.from_dict({ + "builder": { + "intent": {"Create": "digitalCapture"}, + "claim_generator_info": {"name": "my_app", "version": "1.0.0"}, + } +}) + +manifest_def = { + "title": "My New Image", + "assertions": [ + { + "label": "cawg.training-mining", + "data": { + "entries": { + "cawg.ai_inference": {"use": "notAllowed"}, + "cawg.ai_generative_training": {"use": "notAllowed"}, + } + }, + } + ], +} + +with Builder(manifest_def, context=ctx) as builder: + with open("photo.jpg", "rb") as source, open("signed_photo.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +## EDIT intent + +Use `EDIT` when modifying an existing asset. The Builder will: + +1. Check if a `parentOf` ingredient has already been added. If not, it **automatically creates one from the source stream** passed to `sign()`. +2. Add a `c2pa.opened` action linked to the parent ingredient. + +No `digital_source_type` parameter is needed. + +### Example: Editing an asset + +In this case, the source stream becomes the parent ingredient. + +Using `Context`: + +```py +ctx = Context.from_dict({"builder": {"intent": "edit"}}) + +with Builder({}, context=ctx) as builder: + with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Using `set_intent`: + +```py +with Builder({}) as builder: + builder.set_intent(C2paBuilderIntent.EDIT) + + # The Builder reads "original.jpg" as the parent ingredient, + # then writes the new manifest into "edited.jpg" + with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +The resulting manifest contains: + +- One ingredient with `relationship: "parentOf"` pointing to `original.jpg`. +- A `c2pa.opened` action referencing that ingredient. + +If the source file already has a C2PA manifest, the ingredient preserves the full provenance chain. + +### Example: Editing with a manually-added parent + +To control some the parent ingredient's metadata (for example, to set a title or use a different source), add it explicitly. The Builder will then use that ingredient: + +```py +ctx = Context.from_dict({"builder": {"intent": "edit"}}) + +with Builder({}, context=ctx) as builder: + with open("original.jpg", "rb") as original: + builder.add_ingredient( + {"title": "Original Photo", "relationship": "parentOf"}, + "image/jpeg", + original, + ) + + with open("canvas.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Example: Editing with additional component ingredients + +A parent ingredient can be combined with component or input ingredients. The intent creates the `c2pa.opened` action for the parent, and additional actions can be added as components (componentOf)/input (inputTo): + +```py +ctx = Context.from_dict({"builder": {"intent": "edit"}}) + +with Builder({ + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.placed", + "parameters": {"ingredientIds": ["overlay_label"]}, + } + ] + }, + } + ], +}, context=ctx) as builder: + + # The Builder auto-creates a parent from the source stream + # and generates a c2pa.opened action for it + + # Add a component ingredient manually + with open("overlay.png", "rb") as overlay: + builder.add_ingredient( + { + "title": "overlay.png", + "relationship": "componentOf", + "label": "overlay_label", + }, + "image/png", + overlay, + ) + + with open("original.jpg", "rb") as source, open("composite.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +## UPDATE intent + +Use `UPDATE` for non-editorial changes where the asset content itself is not modified, for example adding or changing metadata. This is a limited form of `EDIT`: + +- Allows exactly one ingredient (only the parent). +- Does not allow changes to the parent's hashed content. +- Produces a more compact manifest than `EDIT`. + +Like for the `EDIT` intent, the Builder auto-creates a parent ingredient from the source stream if one is not provided. + +### Example: Adding metadata to a signed asset + +Using `Context`: + +```py +ctx = Context.from_dict({"builder": {"intent": "update"}}) + +with Builder({}, context=ctx) as builder: + with open("signed_asset.jpg", "rb") as source, open("updated_asset.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Using `set_intent`: + +```py +with Builder({}) as builder: + builder.set_intent(C2paBuilderIntent.UPDATE) + + with open("signed_asset.jpg", "rb") as source, open("updated_asset.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +## Intent values in settings + +When configuring the intent settings, the intent is specified as a string or object in the `builder.intent` field: + +| Intent | Settings value | With digital source type | +|--------|---------------|--------------------------| +| Create | `{"Create": ""}` | Required. E.g., `{"Create": "digitalCapture"}` | +| Edit | `"edit"` | Not applicable | +| Update | `"update"` | Not applicable | + +Available digital source type values for Create: `"digitalCapture"`, `"digitalCreation"`, `"trainedAlgorithmicMedia"`, `"compositeSynthetic"`, `"screenCapture"`, `"algorithmicMedia"`, and others. + +## API reference + +### `Builder.set_intent(intent, digital_source_type=C2paDigitalSourceType.EMPTY)` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `intent` | `C2paBuilderIntent` | The intent: `CREATE`, `EDIT`, or `UPDATE`. | +| `digital_source_type` | `C2paDigitalSourceType` | Required for `CREATE`. Describes how the asset was made. Defaults to `EMPTY`. | + +**Raises:** `C2paError` if the intent cannot be set (e.g., a `parentOf` ingredient exists with `CREATE`, or no parent can be found for `EDIT`/`UPDATE`). diff --git a/docs/tmn-wip-docs/selective-manifests.md b/docs/tmn-wip-docs/selective-manifests.md new file mode 100644 index 00000000..b7131a33 --- /dev/null +++ b/docs/tmn-wip-docs/selective-manifests.md @@ -0,0 +1,708 @@ +# Selective manifest construction + +You can use `Builder` and `Reader` together to selectively construct manifests—keeping only the parts you need and omitting the rest. This is useful when you don't want to include all ingredients in a working store (for example, when some ingredient assets are not visible). + +This process is best described as *filtering* or *rebuilding* a working store: + +1. Read an existing manifest. +2. Choose which elements to retain. +3. Build a new manifest containing only those elements. + +A manifest is a signed data structure attached to an asset that records provenance and which source assets (ingredients) contributed to it. It contains assertions (statements about the asset), ingredients (references to other assets), and references to binary resources (such as thumbnails). + +Since both `Reader` and `Builder` are **read-only** by design (neither has a `remove()` method), to exclude content you must **read what exists, filter to keep what you need, and create a new** `Builder` **with only that information**. This produces a new `Builder` instance—a "rebuild." + +> [!IMPORTANT] +> This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. + +## Core concepts + +```mermaid +flowchart LR + A[Signed Asset] -->|Reader| B[JSON + Resources] + B -->|Filter| C[Filtered Data] + C -->|new Builder| D[New Builder] + D -->|sign| E[New Asset] +``` + + + +The fundamental workflow is: + +1. **Read** the existing manifest with `Reader` to get JSON and binary resources +2. **Identify and filter** the parts to keep (parse the JSON, select and gather elements) +3. **Create a new `Builder`** with only the selected parts based on the applied filtering rules +4. **Sign** the new `Builder` into the output asset + +## Reading an existing manifest + +Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The source asset is never modified. + +```py +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source) as reader: + # Get the full manifest store as JSON + manifest_store = json.loads(reader.json()) + + # Identify the active manifest, which is the current/latest manifest + active_label = manifest_store["active_manifest"] + manifest = manifest_store["manifests"][active_label] + + # Access specific parts + ingredients = manifest["ingredients"] + assertions = manifest["assertions"] + thumbnail_id = manifest["thumbnail"]["identifier"] +``` + +### Extracting binary resources + +The JSON returned by `reader.json()` only contains string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. Extract the actual binary content by using `resource_to_stream()`: + +```py +# Extract a thumbnail to an in-memory stream +thumb_stream = io.BytesIO() +reader.resource_to_stream(thumbnail_id, thumb_stream) + +# Or extract to a file +with open("thumbnail.jpg", "wb") as f: + reader.resource_to_stream(thumbnail_id, f) +``` + +## Filtering into a new Builder + +Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. + +When transferring ingredients from a `Reader` to a new `Builder`, you must transfer both the JSON metadata and the associated binary resources (thumbnails, manifest data). The JSON contains identifiers that reference those resources; the same identifiers must be used when calling `builder.add_resource()`. + +### Transferring binary resources + +Since ingredients reference binary data (thumbnails, manifest stores), you need to copy those resources from the `Reader` to the new `Builder`. This helper function encapsulates the pattern: + +```py +def transfer_ingredient_resources(reader, builder, ingredients): + """Copy binary resources for a list of ingredients from reader to builder.""" + for ingredient in ingredients: + for key in ("thumbnail", "manifest_data"): + if key in ingredient: + uri = ingredient[key]["identifier"] + buf = io.BytesIO() + reader.resource_to_stream(uri, buf) + buf.seek(0) + builder.add_resource(uri, buf) +``` + +This function is used throughout the examples below. + +### Keep only specific ingredients + +```py +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + # Filter: keep only ingredients with a specific relationship + kept = [ + ing for ing in active["ingredients"] + if ing["relationship"] == "parentOf" + ] + + # Create a new Builder with only the kept ingredients + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": kept, + }) as new_builder: + transfer_ingredient_resources(reader, new_builder, kept) + + # Sign the new Builder into an output asset + source.seek(0) + with open("output.jpg", "wb") as dest: + new_builder.sign(signer, "image/jpeg", source, dest) +``` + +### Keep only specific assertions + +```py +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + # Keep training-mining assertions, filter out everything else + kept = [ + a for a in active["assertions"] + if a["label"] == "cawg.training-mining" + ] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": kept, + }) as new_builder: + source.seek(0) + with open("output.jpg", "wb") as dest: + new_builder.sign(signer, "image/jpeg", source, dest) +``` + +### Start fresh and preserve provenance + +Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. This is done by creating a new `Builder` with a new manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. + +The function `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data inside the ingredient record. This means: + +- The new manifest has its own, independent set of assertions +- The original's full manifest is preserved inside the ingredient, so validators can inspect the full provenance history +- The provenance chain is unbroken: anyone reading the new asset can follow the ingredient link back to the original + +```mermaid +flowchart TD + subgraph Original["Original Signed Asset"] + OA["Assertions: A, B, C"] + OI["Ingredients: X, Y"] + end + subgraph NewBuilder["New Builder"] + NA["Assertions: (empty or new)"] + NI["Ingredient: original.jpg (contains full original manifest as binary data)"] + end + Original -->|"add_ingredient()"| NI + NI -.->|"validators can trace back"| Original + + style NA fill:#efe,stroke:#090 + style NI fill:#efe,stroke:#090 +``` + + + +```py +with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": [], +}) as new_builder: + # Add the original as an ingredient to preserve provenance chain. + # add_ingredient() stores the original's manifest as binary data inside + # the ingredient, but does NOT copy the original's assertions. + with open("original_signed.jpg", "rb") as original: + new_builder.add_ingredient( + {"title": "original.jpg", "relationship": "parentOf"}, + "image/jpeg", + original, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + new_builder.sign(signer, "image/jpeg", source, dest) +``` + +## Adding actions to a working store + +Actions record what was done to an asset (e.g., color adjustments, cropping, placing content). Use `builder.add_action()` to add them to a working store. + +```py +builder.add_action({ + "action": "c2pa.color_adjustments", + "parameters": {"name": "brightnesscontrast"}, +}) + +builder.add_action({ + "action": "c2pa.filtered", + "parameters": {"name": "A filter"}, + "description": "Filtering applied", +}) +``` + +### Action JSON fields + + +| Field | Required | Description | +| --- | --- | --- | +| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | +| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | +| `description` | No | Human-readable description of what happened | +| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | + + +### Linking actions to ingredients + +When an action involves a specific ingredient, the ingredient is linked to the action using `ingredientIds` (in the action's `parameters`), referencing a matching key in the ingredient. + +#### How `ingredientIds` resolution works + +The SDK matches each value in `ingredientIds` against ingredients using this priority: + +1. `label` on the ingredient (primary): if set and non-empty, this is used as the linking key. +2. `instance_id` on the ingredient (fallback): used when `label` is absent or empty. + +#### Linking with `label` + +The `label` field on an ingredient is the **primary** linking key. Set a `label` on the ingredient and reference it in the action's `ingredientIds`. The label can be any string: it acts as a linking key between the ingredient and the action. + +```py +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation", + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3"] + }, + }, + ] + }, + } + ], +} + +with Builder(manifest_json) as builder: + # The label on the ingredient matches the value in ingredientIds + with open("photo.jpg", "rb") as photo: + builder.add_ingredient( + { + "title": "photo.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3", + }, + "image/jpeg", + photo, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +##### Linking multiple ingredients + +When linking multiple ingredients, each ingredient needs a unique label. + +> [!NOTE] +> The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. + +```py +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.opened", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_1"] + }, + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_2"] + }, + }, + ] + }, + } + ], +} + +with Builder(manifest_json) as builder: + # parentOf ingredient linked to c2pa.opened + with open("original.jpg", "rb") as original: + builder.add_ingredient( + { + "title": "original.jpg", + "format": "image/jpeg", + "relationship": "parentOf", + "label": "c2pa.ingredient.v3_1", + }, + "image/jpeg", + original, + ) + + # componentOf ingredient linked to c2pa.placed + with open("overlay.jpg", "rb") as overlay: + builder.add_ingredient( + { + "title": "overlay.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3_2", + }, + "image/jpeg", + overlay, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +#### Linking with `instance_id` + +When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. + +```py +# instance_id is used as the linking identifier and must be unique +instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f" + +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredientIds": [instance_id] + }, + } + ] + }, + } + ], +} + +with Builder(manifest_json) as builder: + # No label set: instance_id is used as the linking key + with open("source_photo.jpg", "rb") as photo: + builder.add_ingredient( + { + "title": "source_photo.jpg", + "relationship": "parentOf", + "instance_id": instance_id, + }, + "image/jpeg", + photo, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +> [!NOTE] +> The `instance_id` can be read back from the ingredient JSON after signing. + +#### Reading linked ingredients + +After signing, `ingredientIds` is gone. The action's `parameters.ingredients[]` contains hashed JUMBF URIs pointing to ingredient assertions. To match an action to its ingredient, extract the label from the URL: + +```py +with open("signed_asset.jpg", "rb") as signed: + with Reader("image/jpeg", signed) as reader: + manifest_store = json.loads(reader.json()) + active_label = manifest_store["active_manifest"] + manifest = manifest_store["manifests"][active_label] + + # Build a map: label -> ingredient + label_to_ingredient = { + ing["label"]: ing for ing in manifest["ingredients"] + } + + # Match each action to its ingredients by extracting labels from URLs + for assertion in manifest["assertions"]: + if assertion["label"] != "c2pa.actions.v2": + continue + for action in assertion["data"]["actions"]: + for ref in action.get("parameters", {}).get("ingredients", []): + label = ref["url"].rsplit("/", 1)[-1] + matched = label_to_ingredient.get(label) + # matched is the ingredient linked to this action +``` + +#### When to use `label` vs `instance_id` + +| Property | `label` | `instance_id` | +| --- | --- | --- | +| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | +| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | +| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using XMP-based IDs | +| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | +| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | + + +**Use `label`** when defining manifests in JSON. +**Use `instance_id`** when working programmatically with ingredients whose identity comes from other sources, or when a stable identifier that persists unchanged across rebuilds is needed. + +## Working with archives + +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. For more background on working stores and archives, see [Working stores](https://opensource.contentauthenticity.org/docs/rust-sdk/docs/working-stores). + +There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working store archives) and ingredient archives. + +### Builder archives vs. ingredient archives + +A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder.from_archive()`. + +An **ingredient archive** contains the manifest store from an asset that was added as an ingredient. + +The key difference: a builder archive is a work-in-progress (unsigned). An ingredient archive carries the provenance history of a source asset for reuse as an ingredient in other working stores. + +### The ingredients catalog pattern + +An **ingredients catalog** is a collection of archived ingredients that can be selected when constructing a final manifest. Each archive holds ingredients; at build time the caller selects only the ones needed. + +```mermaid +flowchart TD + subgraph Catalog["Ingredients Catalog (archived)"] + A1["Archive: photos.c2pa (ingredients from photo shoot)"] + A2["Archive: graphics.c2pa (ingredients from design assets)"] + A3["Archive: audio.c2pa (ingredients from audio tracks)"] + end + subgraph Build["Final Builder"] + direction TB + SEL["Pick and choose ingredients from any archive in the catalog"] + FB["New Builder with selected ingredients only"] + end + A1 -->|"select photo_1, photo_3"| SEL + A2 -->|"select logo"| SEL + A3 -. "skip (not needed)" .-> X((not used)) + SEL --> FB + FB -->|sign| OUT[Signed Output Asset] + + style A3 fill:#eee,stroke:#999 + style X fill:#f99,stroke:#c00 +``` + + + +```py +# Read from a catalog of archived ingredients +archive_stream.seek(0) +with Reader("application/c2pa", archive_stream) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + # Pick only the needed ingredients + selected = [ + ing for ing in active["ingredients"] + if ing["title"] in {"photo_1.jpg", "logo.png"} + ] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": selected, + }) as new_builder: + transfer_ingredient_resources(reader, new_builder, selected) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + new_builder.sign(signer, "image/jpeg", source, dest) +``` + +### Overriding ingredient properties + +When adding an ingredient from an archive or from a file, the JSON passed to `add_ingredient()` can override properties like `title` and `relationship`. This is useful when reusing archived ingredients in a different context: + +```py +with open("signed_asset.jpg", "rb") as signed: + builder.add_ingredient( + { + "title": "my-custom-title.jpg", + "relationship": "parentOf", + "instance_id": "my-tracking-id:asset-example-id", + }, + "image/jpeg", + signed, + ) +``` + +The `title`, `relationship`, and `instance_id` fields in the provided JSON take priority. The library fills in the rest (thumbnail, manifest_data, format) from the source. This works with signed assets, `.c2pa` archives, or unsigned files. + +### Using custom vendor parameters in actions + +The C2PA specification allows **vendor-namespaced parameters** on actions using reverse domain notation. These parameters survive signing and can be read back, useful for tagging actions with IDs that support filtering. + +```py +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture", + "parameters": { + "com.mycompany.tool": "my-editor", + "com.mycompany.session_id": "session-abc-123", + }, + }, + { + "action": "c2pa.placed", + "description": "Placed an image", + "parameters": { + "com.mycompany.layer_id": "layer-42", + "ingredientIds": ["c2pa.ingredient.v3"], + }, + }, + ] + }, + } + ], +} +``` + +After signing, these custom parameters appear alongside the standard fields: + +```json +{ + "action": "c2pa.placed", + "parameters": { + "com.mycompany.layer_id": "layer-42", + "ingredients": [{"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"}] + } +} +``` + +Custom vendor parameters can be used to filter actions. For example, to find all actions related to a specific layer: + +```py +layer_actions = [ + action for action in actions + if action.get("parameters", {}).get("com.mycompany.layer_id") == "layer-42" +] +``` + +> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (e.g., `c2pa` or `cawg`) may be reserved. + +### Extracting ingredients from a working store + +An example workflow is to build up a working store with multiple ingredients, archive it, and then later extract specific ingredients from that archive to use in a new working store. + +```mermaid +flowchart TD + subgraph Step1["Step 1: Build a working store with ingredients"] + IA["add_ingredient(A.jpg)"] --> B1[Builder] + IB["add_ingredient(B.jpg)"] --> B1 + B1 -->|"to_archive()"| AR["archive.c2pa"] + end + subgraph Step2["Step 2: Extract ingredients from archive"] + AR -->|"Reader(application/c2pa)"| RD[JSON + resources] + RD -->|"pick ingredients"| SEL[Selected ingredients] + end + subgraph Step3["Step 3: Reuse in a new Builder"] + SEL -->|"new Builder + add_resource()"| B2[New Builder] + B2 -->|sign| OUT[Signed Output] + end +``` + + + +**Step 1:** Build a working store and archive it: + +```py +with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], +}) as builder: + # Add ingredients to the working store + with open("A.jpg", "rb") as ing_a: + builder.add_ingredient( + {"title": "A.jpg", "relationship": "componentOf"}, + "image/jpeg", + ing_a, + ) + + with open("B.jpg", "rb") as ing_b: + builder.add_ingredient( + {"title": "B.jpg", "relationship": "componentOf"}, + "image/jpeg", + ing_b, + ) + + # Save the working store as an archive + archive_stream = io.BytesIO() + builder.to_archive(archive_stream) +``` + +**Step 2:** Read the archive and extract ingredients: + +```py +archive_stream.seek(0) +with Reader("application/c2pa", archive_stream) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + ingredients = active["ingredients"] +``` + +**Step 3:** Create a new Builder with the extracted ingredients: + +```py + # Pick the desired ingredients + selected = [ing for ing in ingredients if ing["title"] == "A.jpg"] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": selected, + }) as new_builder: + transfer_ingredient_resources(reader, new_builder, selected) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + new_builder.sign(signer, "image/jpeg", source, dest) +``` + +### Merging multiple working stores + +In some cases you may need to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**—the recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated. + +When merging from multiple sources, resource identifier URIs can collide. Rename identifiers with a unique suffix when needed. Use two passes: (1) collect ingredients with collision handling, build the manifest, create the builder; (2) re-read each archive and transfer resources (use original ID for `resource_to_stream()`, renamed ID for `add_resource()` when collisions occurred). + +```py +used_ids: set[str] = set() +suffix_counter = 0 +all_ingredients = [] +archive_ingredient_counts = [] + +# Pass 1: Collect ingredients, renaming IDs on collision +for archive_stream in archives: + archive_stream.seek(0) + with Reader("application/c2pa", archive_stream) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + ingredients = active["ingredients"] + + for ingredient in ingredients: + for key in ("thumbnail", "manifest_data"): + if key not in ingredient: + continue + uri = ingredient[key]["identifier"] + if uri in used_ids: + suffix_counter += 1 + ingredient[key]["identifier"] = f"{uri}__{suffix_counter}" + used_ids.add(ingredient[key]["identifier"]) + all_ingredients.append(ingredient) + + archive_ingredient_counts.append(len(ingredients)) + +with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": all_ingredients, +}) as builder: + # Pass 2: Transfer resources (match by ingredient index) + offset = 0 + for archive_stream, count in zip(archives, archive_ingredient_counts): + archive_stream.seek(0) + with Reader("application/c2pa", archive_stream) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + originals = active["ingredients"] + + for original, merged in zip(originals, all_ingredients[offset:offset + count]): + for key in ("thumbnail", "manifest_data"): + if key not in original: + continue + buf = io.BytesIO() + reader.resource_to_stream(original[key]["identifier"], buf) + buf.seek(0) + builder.add_resource(merged[key]["identifier"], buf) + + offset += count + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` diff --git a/docs/tmn-wip-docs/settings.md b/docs/tmn-wip-docs/settings.md new file mode 100644 index 00000000..89f8c859 --- /dev/null +++ b/docs/tmn-wip-docs/settings.md @@ -0,0 +1,449 @@ +# Using settings + +You can configure SDK settings using a JSON format that controls many aspects of the library's behavior. +The settings JSON format is the same across all languages for the C2PA SDKs (Rust, C/C++, Python, and so on). + +This document describes how to use settings in Python. The Settings schema is the same as the [Rust library](https://github.com/contentauth/c2pa-rs); for the complete JSON schema, see the [Settings reference](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/). + +## Using settings with Context + +The recommended approach is to pass settings to a `Context` object and then use the `Context` with `Reader` and `Builder`. This gives you explicit, isolated configuration with no global state. For details on creating and using contexts, see [Using Context to configure the SDK](context.md). + +**Legacy approach:** The deprecated `load_settings()` function sets global settings. Don't use that approach; instead pass a `Context` (with settings) to `Reader` and `Builder`. See [Using Context with Reader](context.md#configuring-reader) and [Using Context with Builder](context.md#configuring-builder). + +## Settings API + +Create and configure settings: + +| Method | Description | +|--------|-------------| +| `Settings()` | Create default settings with SDK defaults. | +| `Settings.from_json(json_str)` | Create settings from a JSON string. Raises `C2paError` on parse error. | +| `Settings.from_dict(config)` | Create settings from a Python dictionary. | +| `set(path, value)` | Set a single value by dot-separated path (e.g. `"verify.verify_after_sign"`). Value must be a string. Returns `self` for chaining. Use this for programmatic configuration. | +| `update(data, format="json")` | Merge JSON configuration into existing settings. `data` can be a JSON string or a dict. Later keys override earlier ones. Use this to apply configuration files or JSON strings. Only `"json"` format is supported. | +| `settings["path"] = "value"` | Dict-like setter. Equivalent to `set(path, value)`. | +| `is_valid` | Property that returns `True` if the object holds valid resources (not closed). | +| `close()` | Release native resources. Called automatically when used as a context manager. | + +**Important notes:** + +- The `set()` and `update()` methods can be chained for sequential configuration. +- When using multiple configuration methods, later calls override earlier ones (last wins). +- Use the `with` statement for automatic resource cleanup. +- Only JSON format is supported for settings in the Python SDK. + +```py +from c2pa import Settings + +# Create with defaults +settings = Settings() + +# Set individual values by dot-notation path +settings.set("builder.thumbnail.enabled", "false") + +# Method chaining +settings.set("builder.thumbnail.enabled", "false").set("verify.verify_after_sign", "true") + +# Dict-like access +settings["builder.thumbnail.enabled"] = "false" + +# Create from JSON string +settings = Settings.from_json('{"builder": {"thumbnail": {"enabled": false}}}') + +# Create from a dictionary +settings = Settings.from_dict({"builder": {"thumbnail": {"enabled": False}}}) + +# Merge additional configuration +settings.update({"verify": {"remote_manifest_fetch": True}}) +``` + +## Overview of the Settings structure + +The Settings JSON has this top-level structure: + +```json +{ + "version": 1, + "trust": { ... }, + "cawg_trust": { ... }, + "core": { ... }, + "verify": { ... }, + "builder": { ... }, + "signer": { ... }, + "cawg_x509_signer": { ... } +} +``` + +### Settings format + +Settings are provided in **JSON** only. Pass JSON strings to `Settings.from_json()` or dictionaries to `Settings.from_dict()`. + +```py +# From JSON string +settings = Settings.from_json('{"verify": {"verify_after_sign": true}}') + +# From dict +settings = Settings.from_dict({"verify": {"verify_after_sign": True}}) + +# Context from JSON string +ctx = Context.from_json('{"verify": {"verify_after_sign": true}}') + +# Context from dict +ctx = Context.from_dict({"verify": {"verify_after_sign": True}}) +``` + +To load from a file, read the file contents and pass them to `Settings.from_json()`: + +```py +import json + +with open("config/settings.json", "r") as f: + settings = Settings.from_json(f.read()) +``` + +## Default configuration + +The settings JSON schema — including the complete default configuration with all properties and their default values — is shared with all languages in the SDK: + +```json +{ + "version": 1, + "builder": { + "claim_generator_info": null, + "created_assertion_labels": null, + "certificate_status_fetch": null, + "certificate_status_should_override": null, + "generate_c2pa_archive": true, + "intent": null, + "actions": { + "all_actions_included": null, + "templates": null, + "actions": null, + "auto_created_action": { + "enabled": true, + "source_type": "empty" + }, + "auto_opened_action": { + "enabled": true, + "source_type": null + }, + "auto_placed_action": { + "enabled": true, + "source_type": null + } + }, + "thumbnail": { + "enabled": true, + "ignore_errors": true, + "long_edge": 1024, + "format": null, + "prefer_smallest_format": true, + "quality": "medium" + } + }, + "cawg_trust": { + "verify_trust_list": true, + "user_anchors": null, + "trust_anchors": null, + "trust_config": null, + "allowed_list": null + }, + "cawg_x509_signer": null, + "core": { + "merkle_tree_chunk_size_in_kb": null, + "merkle_tree_max_proofs": 5, + "backing_store_memory_threshold_in_mb": 512, + "decode_identity_assertions": true, + "allowed_network_hosts": null + }, + "signer": null, + "trust": { + "user_anchors": null, + "trust_anchors": null, + "trust_config": null, + "allowed_list": null + }, + "verify": { + "verify_after_reading": true, + "verify_after_sign": true, + "verify_trust": true, + "verify_timestamp_trust": true, + "ocsp_fetch": false, + "remote_manifest_fetch": true, + "skip_ingredient_conflict_resolution": false, + "strict_v1_validation": false + } +} +``` + +## Overview of Settings + +For a complete reference to all the Settings properties, see the [SDK object reference - Settings](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema). + +| Property | Description | +|----------|-------------| +| `version` | Settings format version (integer). The default and only supported value is 1. | +| [`builder`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#buildersettings) | Configuration for Builder. | +| [`cawg_trust`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#trust) | Configuration for CAWG trust lists. | +| [`cawg_x509_signer`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#signersettings) | Configuration for the CAWG x.509 signer. | +| [`core`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#core) | Configuration for core features. | +| [`signer`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#signersettings) | Configuration for the base C2PA signer. | +| [`trust`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#trust) | Configuration for C2PA trust lists. | +| [`verify`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#verify) | Configuration for verification (validation). | + +The top-level `version` property must be `1`. All other properties are optional. + +For Boolean values, use JSON Booleans `true` and `false` in JSON strings, or Python `True` and `False` when using `from_dict()` or `update()` with a dict. + +> [!IMPORTANT] +> If you don't specify a value for a property, the SDK uses the default value. If you specify a value of `null` (or `None` in a dict), the property is explicitly set to `null`, not the default. This distinction is important when you want to override a default behavior. + +### Trust configuration + +The [`trust` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#trust) control which certificates are trusted when validating C2PA manifests. + +- Using `user_anchors`: recommended for development +- Using `allowed_list` (bypass chain validation) + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `trust.allowed_list` | string | Explicitly allowed certificates (PEM format). These certificates are trusted regardless of chain validation. Use for development/testing. | — | +| `trust.trust_anchors` | string | Default trust anchor root certificates (PEM format). **Replaces** the SDK's built-in trust anchors entirely. | — | +| `trust.trust_config` | string | Allowed Extended Key Usage (EKU) OIDs. Controls which certificate purposes are accepted (e.g., document signing: `1.3.6.1.4.1.311.76.59.1.9`). | — | +| `trust.user_anchors` | string | Additional user-provided root certificates (PEM format). Adds custom certificate authorities without replacing the SDK's built-in trust anchors. | — | + +When using self-signed certificates or custom certificate authorities during development, you need to configure trust settings so the SDK can validate your test signatures. + +#### Using `user_anchors` + +For development, you can add your test root CA to the trusted anchors without replacing the SDK's default trust store. +For example: + +```py +with open("test-ca.pem", "r") as f: + test_root_ca = f.read() + +ctx = Context.from_dict({ + "trust": { + "user_anchors": test_root_ca + } +}) + +reader = Reader("signed_asset.jpg", context=ctx) +``` + +#### Using `allowed_list` + +To bypass chain validation, for quick testing, explicitly allow a specific certificate without validating the chain. +For example: + +```py +with open("test_cert.pem", "r") as f: + test_cert = f.read() + +settings = Settings() +settings.update({ + "trust": { + "allowed_list": test_cert + } +}) + +ctx = Context(settings=settings) +reader = Reader("signed_asset.jpg", context=ctx) +``` + +### CAWG trust configuration + +The `cawg_trust` properties configure CAWG (Creator Assertions Working Group) validation of identity assertions in C2PA manifests. The `cawg_trust` object has the same properties as [`trust`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#trust). + +> [!NOTE] +> CAWG trust settings are only used when processing identity assertions with X.509 certificates. If your workflow doesn't use CAWG identity assertions, these settings have no effect. + +### Core + +The [`core` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#core) specify core SDK behavior and performance tuning options. + +Use cases: + +- **Performance tuning for large files**: Set `core.backing_store_memory_threshold_in_mb` to `2048` or higher if processing large video files with sufficient RAM. +- **Restricted network environments**: Set `core.allowed_network_hosts` to limit which domains the SDK can contact. + +### Verify + +The [`verify` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#verify) specify how the SDK validates C2PA manifests. These settings affect both reading existing manifests and verifying newly signed content. + +Common use cases include: + +- [Offline or air-gapped environments](#offline-or-air-gapped-environments). +- [Fast development iteration](#fast-development-iteration) with verification disabled. +- [Strict validation](#strict-validation) for certification or compliance testing. + +By default, the following `verify` properties are `true`, which enables verification: + +- `remote_manifest_fetch` - Fetch remote manifests referenced in the asset. Disable in offline or air-gapped environments. +- `verify_after_reading` - Automatically verify manifests when reading assets. Disable only if you want to manually control verification timing. +- `verify_after_sign` - Automatically verify manifests after signing. Recommended to keep enabled to catch signing errors immediately. +- `verify_timestamp_trust` - Verify timestamp authority (TSA) certificates. WARNING: Disabling this setting makes verification non-compliant. +- `verify_trust` - Verify signing certificates against configured trust anchors. WARNING: Disabling this setting makes verification non-compliant. + +> [!WARNING] +> Disabling verification options (changing `true` to `false`) can make verification non-compliant with the C2PA specification. Only modify these settings in controlled environments or when you have specific requirements. + +#### Offline or air-gapped environments + +Set `remote_manifest_fetch` and `ocsp_fetch` to `false` to disable network-dependent verification features: + +```py +ctx = Context.from_dict({ + "verify": { + "remote_manifest_fetch": False, + "ocsp_fetch": False + } +}) + +reader = Reader("signed_asset.jpg", context=ctx) +``` + +See also [Using Context with Reader](context.md#configuring-reader). + +#### Fast development iteration + +During active development, you can disable verification for faster iteration: + +```py +# WARNING: Only use during development, not in production! +settings = Settings() +settings.set("verify.verify_after_reading", "false") +settings.set("verify.verify_after_sign", "false") + +dev_ctx = Context(settings=settings) +``` + +#### Strict validation + +For certification or compliance testing, enable strict validation: + +```py +ctx = Context.from_dict({ + "verify": { + "strict_v1_validation": True, + "ocsp_fetch": True, + "verify_trust": True, + "verify_timestamp_trust": True + } +}) + +reader = Reader("asset_to_validate.jpg", context=ctx) +validation_result = reader.json() +``` + +### Builder + +The [`builder` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#buildersettings) control how the SDK creates and embeds C2PA manifests in assets. + +#### Claim generator information + +The `claim_generator_info` object identifies your application in the C2PA manifest. **Recommended fields:** + +- `name` (string, required): Your application name (e.g., `"My Photo Editor"`) +- `version` (string, recommended): Application version (e.g., `"2.1.0"`) +- `icon` (string, optional): Icon in C2PA format +- `operating_system` (string, optional): OS identifier or `"auto"` to auto-detect + +**Example:** + +```py +ctx = Context.from_dict({ + "builder": { + "claim_generator_info": { + "name": "My Photo Editor", + "version": "2.1.0", + "operating_system": "auto" + } + } +}) +``` + +#### Thumbnail settings + +The [`builder.thumbnail`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#thumbnailsettings) properties control automatic thumbnail generation. + +For examples of configuring thumbnails for mobile bandwidth or disabling them for batch processing, see [Controlling thumbnail generation](context.md#controlling-thumbnail-generation). + +#### Action tracking settings + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `builder.actions.auto_created_action.enabled` | Boolean | Automatically add a `c2pa.created` action when creating new content. | `true` | +| `builder.actions.auto_created_action.source_type` | string | Source type for the created action. Usually `"empty"` for new content. | `"empty"` | +| `builder.actions.auto_opened_action.enabled` | Boolean | Automatically add a `c2pa.opened` action when opening/reading content. | `true` | +| `builder.actions.auto_placed_action.enabled` | Boolean | Automatically add a `c2pa.placed` action when placing content as an ingredient. | `true` | + +#### Other builder settings + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `builder.intent` | object | Claim intent: `{"Create": "digitalCapture"}`, `{"Edit": null}`, or `{"Update": null}`. Describes the purpose of the claim. | `null` | +| `builder.generate_c2pa_archive` | Boolean | Generate content in C2PA archive format. Keep enabled for standard C2PA compliance. | `true` | + +##### Setting Builder intent + +You can use `Context` to set `Builder` intent for different workflows. + +For example, for original digital capture (photos from camera): + +```py +camera_ctx = Context.from_dict({ + "builder": { + "intent": {"Create": "digitalCapture"}, + "claim_generator_info": {"name": "Camera App", "version": "1.0"} + } +}) +``` + +Or for editing existing content: + +```py +editor_ctx = Context.from_dict({ + "builder": { + "intent": {"Edit": None}, + "claim_generator_info": {"name": "Photo Editor", "version": "2.0"} + } +}) +``` + +### Signer + +The [`signer` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signersettings) configure the primary C2PA signer configuration. Set it to `null` if you provide the signer at runtime, or configure as either a **local** or **remote** signer in settings. + +> [!NOTE] +> The typical approach in Python is to create a `Signer` object with `Signer.from_info()` and pass it directly to `Builder.sign()`. Alternatively, pass a `Signer` to `Context` for the signer-on-context pattern. See [Configuring a signer](context.md#configuring-a-signer) for details. + +#### Local signer + +Use a local signer when you have direct access to the private key and certificate. +For information on all `signer.local` properties, see [signer.local](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signerlocal) in the SDK object reference. + +#### Remote signer + +Use a remote signer when the private key is stored on a secure signing service (HSM, cloud KMS, and so on). +For information on all `signer.remote` properties, see [signer.remote](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signerremote) in the SDK object reference. + +### CAWG X.509 signer configuration + +The `cawg_x509_signer` property specifies configuration for identity assertions. This has the same structure as `signer` (can be local or remote). + +**When to use:** If you need to sign identity assertions separately from the main C2PA claim. When both `signer` and `cawg_x509_signer` are configured, the SDK uses a dual signer: + +- Main claim signature comes from `signer` +- Identity assertions are signed with `cawg_x509_signer` + +For additional JSON configuration examples (minimal configuration, local/remote signer, development/production configurations), see the [Rust SDK settings examples](https://github.com/contentauth/c2pa-rs/blob/main/docs/settings.md#examples). + +## See also + +- [Using Context to configure the SDK](context.md): how to create and use contexts with settings. +- [Usage](usage.md): reading and signing with `Reader` and `Builder`. +- [Rust SDK settings](https://github.com/contentauth/c2pa-rs/blob/main/docs/settings.md): the shared settings schema, default configuration, and JSON examples (language-independent). +- [CAI settings schema reference](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/): full schema reference. diff --git a/docs/tmn-wip-docs/usage.md b/docs/tmn-wip-docs/usage.md new file mode 100644 index 00000000..aeec23a4 --- /dev/null +++ b/docs/tmn-wip-docs/usage.md @@ -0,0 +1,196 @@ +# Using the Python library + +This package works with media files in the [supported formats](https://github.com/contentauth/c2pa-rs/blob/main/docs/supported-formats.md). + +For complete working examples, see the [examples folder](https://github.com/contentauth/c2pa-python/tree/main/examples) in the repository. + +## Import + +Import the objects needed from the API: + +```py +from c2pa import Builder, Reader, Signer, C2paSigningAlg, C2paSignerInfo +``` + +You can use both `Builder`, `Reader` and `Signer` classes with context managers by using a `with` statement. +Doing this is recommended to ensure proper resource and memory cleanup. + +## Define manifest JSON + +The Python library works with both file-based and stream-based operations. +In both cases, the manifest JSON string defines the C2PA manifest to add to an asset. For example: + +```py +manifest_json = json.dumps({ + "claim_generator": "python_test/0.1", + "assertions": [ + { + "label": "cawg.training-mining", + "data": { + "entries": { + "cawg.ai_inference": { + "use": "notAllowed" + }, + "cawg.ai_generative_training": { + "use": "notAllowed" + } + } + } + } + ] + }) +``` + +## File-based operation + +### Read and validate C2PA data + +Use the `Reader` to read C2PA data from the specified asset file. + +This examines the specified media file for C2PA data and generates a report of any data it finds. If there are validation errors, the report includes a `validation_status` field. + +An asset file may contain many manifests in a manifest store. The most recent manifest is identified by the value of the `active_manifest` field in the manifests map. The manifests may contain binary resources such as thumbnails which can be retrieved with `resource_to_stream` using the associated `identifier` field values and a `uri`. + +NOTE: For a comprehensive reference to the JSON manifest structure, see the [Manifest store reference](https://opensource.contentauthenticity.org/docs/manifest/manifest-ref). + +```py +try: + # Create a reader from a file path + with Reader("path/to/media_file.jpg") as reader: + # Print manifest store as JSON + print("Manifest store:", reader.json()) + + # Get the active manifest. + manifest = json.loads(reader.json()) + active_manifest = manifest["manifests"][manifest["active_manifest"]] + if active_manifest: + # Get the uri to the manifest's thumbnail and write it to a file + uri = active_manifest["thumbnail"]["identifier"] + with open("thumbnail_v2.jpg", "wb") as f: + reader.resource_to_stream(uri, f) + +except Exception as err: + print(err) +``` + +### Add a signed manifest + +**WARNING**: This example accesses the private key and security certificate directly from the local file system. This is fine during development, but doing so in production may be insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; for example as show in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). + +Use a `Builder` to add a manifest to an asset: + +```py +try: + # Create a signer from certificate and key files + with open("path/to/cert.pem", "rb") as cert_file, open("path/to/key.pem", "rb") as key_file: + cert_data = cert_file.read() + key_data = key_file.read() + + # Create signer info using cert and key info + signer_info = C2paSignerInfo( + alg=C2paSigningAlg.PS256, + cert=cert_data, + key=key_data, + timestamp_url="http://timestamp.digicert.com" + ) + + # Create signer using the defined SignerInfo + signer = Signer.from_info(signer_info) + + # Create builder with manifest and add ingredients + with Builder(manifest_json) as builder: + # Add any ingredients if needed + with open("path/to/ingredient.jpg", "rb") as ingredient_file: + ingredient_json = json.dumps({"title": "Ingredient Image"}) + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient_file) + + # Sign the file + with open("path/to/source.jpg", "rb") as source_file, open("path/to/output.jpg", "wb") as dest_file: + manifest_bytes = builder.sign(signer, "image/jpeg", source_file, dest_file) + + # Verify the signed file by reading data from the signed output file + with Reader("path/to/output.jpg") as reader: + manifest_store = json.loads(reader.json()) + active_manifest = manifest_store["manifests"][manifest_store["active_manifest"]] + print("Signed manifest:", active_manifest) + +except Exception as e: + print("Failed to sign manifest store: " + str(e)) +``` + +## Stream-based operation + +Instead of working with files, you can read, validate, and add a signed manifest to streamed data. This example is similar to what the file-based example does. + +### Read and validate C2PA data using streams + +```py +try: + # Create a reader from a format and stream + with open("path/to/media_file.jpg", "rb") as stream: + # First parameter should be the type of the file (here, we use the mimetype) + # But in any case we need something to identify the file type + with Reader("image/jpeg", stream) as reader: + # Print manifest store as JSON, as extracted by the Reader + print("manifest store:", reader.json()) + + # Get the active manifest + manifest = json.loads(reader.json()) + active_manifest = manifest["manifests"][manifest["active_manifest"]] + if active_manifest: + # get the uri to the manifest's thumbnail and write it to a file + uri = active_manifest["thumbnail"]["identifier"] + with open("thumbnail_v2.jpg", "wb") as f: + reader.resource_to_stream(uri, f) + +except Exception as err: + print(err) +``` + +### Add a signed manifest to a stream + +**WARNING**: This example accesses the private key and security certificate directly from the local file system. This is fine during development, but doing so in production may be insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; for example as show in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). + +Use a `Builder` to add a manifest to an asset: + +```py +try: + # Create a signer from certificate and key files + with open("path/to/cert.pem", "rb") as cert_file, open("path/to/key.pem", "rb") as key_file: + cert_data = cert_file.read() + key_data = key_file.read() + + # Create signer info using the read certificate and key data + signer_info = C2paSignerInfo( + alg=C2paSigningAlg.PS256, + cert=cert_data, + key=key_data, + timestamp_url="http://timestamp.digicert.com" + ) + + # Create a Signer using the SignerInfo defined previously + signer = Signer.from_info(signer_info) + + # Create a Builder with manifest and add ingredients + with Builder(manifest_json) as builder: + # Add any ingredients as needed + with open("path/to/ingredient.jpg", "rb") as ingredient_file: + ingredient_json = json.dumps({"title": "Ingredient Image"}) + # Here the ingredient is added using streams + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient_file) + + # Sign using streams + with open("path/to/source.jpg", "rb") as source_file, open("path/to/output.jpg", "wb") as dest_file: + manifest_bytes = builder.sign(signer, "image/jpeg", source_file, dest_file) + + # Verify the signed file + with open("path/to/output.jpg", "rb") as stream: + # Create a Reader to read data + with Reader("image/jpeg", stream) as reader: + manifest_store = json.loads(reader.json()) + active_manifest = manifest_store["manifests"][manifest_store["active_manifest"]] + print("Signed manifest:", active_manifest) + +except Exception as e: + print("Failed to sign manifest store: " + str(e)) +``` diff --git a/docs/tmn-wip-docs/working-stores.md b/docs/tmn-wip-docs/working-stores.md new file mode 100644 index 00000000..801816dd --- /dev/null +++ b/docs/tmn-wip-docs/working-stores.md @@ -0,0 +1,648 @@ +# Manifests, working stores, and archives + +This table summarizes the fundamental entities that you work with when using the CAI SDK. + +| Object | Description | Where it is | Primary API | +|--------|-------------|-------------|-------------| +| [**Manifest store**](#manifest-store) | Final signed provenance data. Contains one or more manifests. | Embedded in asset or remotely in cloud | `Reader` class | +| [**Working store**](#working-store) | Editable in-progress manifest. | `Builder` object | `Builder` class | +| [**Archive**](#archive) | Serialized working store | `.c2pa` file/stream | `Builder.to_archive()` / `Builder.from_archive()` | +| [**Resources**](#working-with-resources) | Binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. | In manifest. | `Builder.add_resource()` / `Reader.resource_to_stream()` | +| [**Ingredients**](#working-with-ingredients) | Source materials used to create an asset. | In manifest. | `Builder.add_ingredient()` | + +This diagram summarizes the relationships among these entities. + +```mermaid +graph TD + subgraph MS["Manifest Store"] + subgraph M1["Manifests"] + R1[Resources] + I1[Ingredients] + end + end + + A[Working Store
Builder object] -->|sign| MS + A -->|to_archive| C[C2PA Archive
.c2pa file] + C -->|from_archive| A +``` + +## Key entities + +### Manifest store + +A _manifest store_ is the data structure that's embedded in (or attached to) a signed asset. It contains one or more manifests that contain provenance data and cryptographic signatures. + +**Characteristics:** + +- Final, immutable signed data embedded in or attached to an asset. +- Contains one or more manifests (identified by URIs). +- Has exactly one `active_manifest` property pointing to the most recent manifest. +- Read it by using a `Reader` object. + +**Example:** When you open a signed JPEG file, the C2PA data embedded in it is the manifest store. + +For more information, see: + +- [Reading manifest stores from assets](#reading-manifest-stores-from-assets) +- [Creating and signing manifests](#creating-and-signing-manifests) +- [Embedded vs external manifests](#embedded-vs-external-manifests) + +### Working store + +A _working store_ is a `Builder` object representing an editable, in-progress manifest that has not yet been signed and bound to an asset. Think of it as a manifest in progress, or a manifest being built. + +**Characteristics:** + +- Editable, mutable state in memory (a Builder object). +- Contains claims, ingredients, and assertions that can be modified. +- Can be saved to a C2PA archive (`.c2pa` JUMBF binary format) for later use. + +**Example:** When you create a `Builder` object and add assertions to it, you're dealing with a working store, as it is an "in progress" manifest being built. + +For more information, see [Using Working stores](#using-working-stores). + +### Archive + +A _C2PA archive_ (or just _archive_) contains the serialized bytes of a working store saved to a file or stream (typically a `.c2pa` file). It uses the standard JUMBF `application/c2pa` format. + +**Characteristics:** + +- Portable serialization of a working store (Builder). +- Save an archive by using `Builder.to_archive()` and restore a full working store from an archive by using `Builder.from_archive()`. +- Useful for separating manifest preparation ("work in progress") from final signing. + +For more information, see [Working with archives](#working-with-archives). + +## Reading manifest stores from assets + +Use the `Reader` class to read manifest stores from signed assets. + +### Reading from a file + +```py +from c2pa import Reader + +try: + # Create a Reader from a signed asset file + reader = Reader("signed_image.jpg") + + # Get the manifest store as JSON + manifest_store_json = reader.json() +except Exception as e: + print(f"C2PA Error: {e}") +``` + +### Reading from a stream + +```py +with open("signed_image.jpg", "rb") as stream: + # Create Reader from stream with MIME type + reader = Reader("image/jpeg", stream) + manifest_json = reader.json() +``` + +### Using Context for configuration + +For more control over validation and trust settings, use a `Context`: + +```py +from c2pa import Context, Reader + +# Create context with custom validation settings +ctx = Context.from_dict({ + "verify": { + "verify_after_sign": True + } +}) + +# Use context when creating Reader +reader = Reader("signed_image.jpg", context=ctx) +manifest_json = reader.json() +``` + +## Using working stores + +A **working store** is represented by a `Builder` object. It contains "live" manifest data as you add information to it. + +### Creating a working store + +```py +import json +from c2pa import Builder, Context + +# Create a working store with a manifest definition +manifest_json = json.dumps({ + "claim_generator_info": [{ + "name": "example-app", + "version": "0.1.0" + }], + "title": "Example asset", + "assertions": [] +}) + +builder = Builder(manifest_json) + +# Or with custom context +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": True} + } +}) +builder = Builder(manifest_json, context=ctx) +``` + +### Modifying a working store + +Before signing, you can modify the working store (Builder): + +```py +import io + +# Add binary resources (like thumbnails) +with open("thumbnail.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) + +# Add ingredients (source files) +ingredient_json = json.dumps({ + "title": "Original asset", + "relationship": "parentOf" +}) +with open("source.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +# Add actions +action_json = { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" +} +builder.add_action(action_json) + +# Configure embedding behavior +builder.set_no_embed() # Don't embed manifest in asset +builder.set_remote_url("https://example.com/manifests/") +``` + +### From working store to manifest store + +When you sign an asset, the working store (Builder) becomes a manifest store embedded in the output: + +```py +from c2pa import Signer, C2paSignerInfo, C2paSigningAlg + +# Create a signer +signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, + sign_cert=certs, + private_key=private_key, + ta_url=b"http://timestamp.digicert.com" +) +signer = Signer.from_info(signer_info) + +# Sign the asset - working store becomes a manifest store +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) + +# Now "signed.jpg" contains a manifest store +# You can read it back with Reader +reader = Reader("signed.jpg") +manifest_store_json = reader.json() +``` + +## Creating and signing manifests + +### Creating a Builder (working store) + +```py +# Create with manifest definition +builder = Builder(manifest_json) + +# Or with custom context +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": True} + } +}) +builder = Builder(manifest_json, context=ctx) +``` + +### Creating a Signer + +For testing, create a `Signer` with certificates and private key: + +```py +from c2pa import Signer, C2paSignerInfo, C2paSigningAlg + +# Load credentials +with open("certs.pem", "rb") as f: + certs = f.read() +with open("private_key.pem", "rb") as f: + private_key = f.read() + +# Create signer +signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, # ES256, ES384, ES512, PS256, PS384, PS512, ED25519 + sign_cert=certs, # Certificate chain in PEM format + private_key=private_key, # Private key in PEM format + ta_url=b"http://timestamp.digicert.com" # Optional timestamp authority URL +) +signer = Signer.from_info(signer_info) +``` + +**WARNING**: Never hard-code or directly access private keys in production. Use a Hardware Security Module (HSM) or Key Management Service (KMS). + +### Signing an asset + +```py +try: + # Sign using streams + with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + manifest_bytes = builder.sign(signer, "image/jpeg", src, dst) + + print("Signed successfully!") + +except Exception as e: + print(f"Signing failed: {e}") +``` + +### Signing with file paths + +You can also sign using file paths directly: + +```py +# Sign using file paths (uses native Rust file I/O for better performance) +manifest_bytes = builder.sign_file( + "source.jpg", "signed.jpg", signer +) +``` + +### Complete example + +This code combines the above examples to create, sign, and read a manifest. + +```py +import json +from c2pa import Builder, Reader, Signer, C2paSignerInfo, C2paSigningAlg + +try: + # 1. Define manifest for working store + manifest_json = json.dumps({ + "claim_generator_info": [{"name": "demo-app", "version": "0.1.0"}], + "title": "Signed image", + "assertions": [] + }) + + # 2. Load credentials + with open("certs.pem", "rb") as f: + certs = f.read() + with open("private_key.pem", "rb") as f: + private_key = f.read() + + # 3. Create signer + signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, + sign_cert=certs, + private_key=private_key, + ta_url=b"http://timestamp.digicert.com" + ) + signer = Signer.from_info(signer_info) + + # 4. Create working store (Builder) and sign + builder = Builder(manifest_json) + with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) + + print("Asset signed - working store is now a manifest store") + + # 5. Read back the manifest store + reader = Reader("signed.jpg") + print(reader.json()) + +except Exception as e: + print(f"Error: {e}") +``` + +## Working with resources + +_Resources_ are binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. + +### Understanding resource identifiers + +When you add a resource to a working store (Builder), you assign it an identifier string. When the manifest store is created during signing, the SDK automatically converts this to a proper JUMBF URI. + +**Resource identifier workflow:** + +```mermaid +graph LR + A[Simple identifier
'thumbnail'] -->|add_resource| B[Working Store
Builder] + B -->|sign| C[JUMBF URI
'self#jumbf=...'] + C --> D[Manifest Store
in asset] +``` + +1. **During manifest creation**: You use a string identifier (e.g., `"thumbnail"`, `"thumbnail1"`). +2. **During signing**: The SDK converts these to JUMBF URIs (e.g., `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`). +3. **After signing**: The manifest store contains the full JUMBF URI that you use to extract the resource. + +### Extracting resources from a manifest store + +To extract a resource, you need its JUMBF URI from the manifest store: + +```py +import json + +reader = Reader("signed_image.jpg") +manifest_store = json.loads(reader.json()) + +# Get active manifest +active_uri = manifest_store["active_manifest"] +manifest = manifest_store["manifests"][active_uri] + +# Extract thumbnail if it exists +if "thumbnail" in manifest: + # The identifier is the JUMBF URI + thumbnail_uri = manifest["thumbnail"]["identifier"] + # Example: "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg" + + # Extract to a stream + with open("thumbnail.jpg", "wb") as f: + reader.resource_to_stream(thumbnail_uri, f) + print("Thumbnail extracted") +``` + +### Adding resources to a working store + +When building a manifest, you add resources using identifiers. The SDK will reference these in your manifest JSON and convert them to JUMBF URIs during signing. + +```py +builder = Builder(manifest_json) + +# Add resource from a stream +with open("thumbnail.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) + +# Sign: the "thumbnail" identifier becomes a JUMBF URI in the manifest store +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +## Working with ingredients + +Ingredients represent source materials used to create an asset, preserving the provenance chain. Ingredients themselves can be turned into ingredient archives (`.c2pa`). + +An ingredient archive is a serialized `Builder` with _exactly one_ ingredient. Once archived with only one ingredient, the Builder archive is an ingredient archive. Such ingredient archives can be used as ingredient in other working stores. + +### Adding ingredients to a working store + +When creating a manifest, add ingredients to preserve the provenance chain: + +```py +builder = Builder(manifest_json) + +# Define ingredient metadata +ingredient_json = json.dumps({ + "title": "Original asset", + "relationship": "parentOf" +}) + +# Add ingredient from a stream +with open("source.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +# Or add ingredient from a file path +builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", "source.jpg") + +# Sign: ingredients become part of the manifest store +with open("new_asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +### Ingredient relationships + +Specify the relationship between the ingredient and the current asset: + +| Relationship | Meaning | +|--------------|---------| +| `parentOf` | The ingredient is a direct parent of this asset | +| `componentOf` | The ingredient is a component used in this asset | +| `inputTo` | The ingredient was an input to creating this asset | + +Example with explicit relationship: + +```py +ingredient_json = json.dumps({ + "title": "Base layer", + "relationship": "componentOf" +}) + +with open("base_layer.png", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/png", ingredient) +``` + +## Working with archives + +An _archive_ (C2PA archive) is a serialized working store (`Builder` object) saved to a stream. + +Using archives provides these advantages: + +- **Save work-in-progress**: Persist a working store between sessions. +- **Separate creation from signing**: Prepare manifests on one machine, sign on another. +- **Share manifests**: Transfer working stores between systems. +- **Offline preparation**: Build manifests offline, sign them later. + +The default binary format of an archive is the **C2PA JUMBF binary format** (`application/c2pa`), which is the standard way to save and restore working stores. + +### Saving a working store to archive + +```py +import io + +# Create and configure a working store +builder = Builder(manifest_json) +with open("thumbnail.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) +with open("source.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +# Save working store to archive stream +archive = io.BytesIO() +builder.to_archive(archive) + +# Or save to a file +with open("manifest.c2pa", "wb") as f: + archive.seek(0) + f.write(archive.read()) + +print("Working store saved to archive") +``` + +A Builder containing **only one ingredient and only the ingredient data** (no other ingredient, no other actions) is an ingredient archive. Ingredient archives can be added directly as ingredient to other working stores too. + +### Restoring a working store from archive + +Create a new `Builder` (working store) from an archive: + +```py +# Restore from stream +with open("manifest.c2pa", "rb") as archive: + builder = Builder.from_archive(archive) + +# Now you can sign with the restored working store +with open("asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +### Restoring with context preservation + +Pass a `context` to `from_archive()` to preserve custom settings: + +```py +# Create context with custom settings +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False} + } +}) + +# Load archive with context +with open("manifest.c2pa", "rb") as archive: + builder = Builder.from_archive(archive, context=ctx) + +# The builder has the archived manifest but keeps the custom context +with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +### Two-phase workflow example + +#### Phase 1: Prepare manifest + +```py +import io +import json + +manifest_json = json.dumps({ + "title": "Artwork draft", + "assertions": [] +}) + +builder = Builder(manifest_json) +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 + ) + +# Save working store as archive +with open("artwork_manifest.c2pa", "wb") as f: + builder.to_archive(f) + +print("Working store saved to artwork_manifest.c2pa") +``` + +#### Phase 2: Sign the asset + +```py +# Restore the working store +with open("artwork_manifest.c2pa", "rb") as archive: + builder = Builder.from_archive(archive) + +# Sign +with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) + +print("Asset signed with manifest store") +``` + +## Embedded vs external manifests + +By default, manifest stores are **embedded** directly into the asset file. You can also use **external** or **remote** manifest stores. + +### Default: embedded manifest stores + +```py +builder = Builder(manifest_json) + +# Default behavior: manifest store is embedded in the output +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) + +# Read it back — manifest store is embedded +reader = Reader("signed.jpg") +``` + +### External manifest stores (no embed) + +Prevent embedding the manifest store in the asset: + +```py +builder = Builder(manifest_json) +builder.set_no_embed() # Don't embed the manifest store + +# Sign: manifest store is NOT embedded, manifest bytes are returned +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + manifest_bytes = builder.sign(signer, "image/jpeg", src, dst) + +# manifest_bytes contains the manifest store +# Save it separately (as a sidecar file or upload to server) +with open("output.c2pa", "wb") as f: + f.write(manifest_bytes) + +print("Manifest store saved externally to output.c2pa") +``` + +### Remote manifest stores + +Reference a manifest store stored at a remote URL: + +```py +builder = Builder(manifest_json) +builder.set_remote_url("https://example.com/manifests/") + +# The asset will contain a reference to the remote manifest store +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +## Best practices + +### Use Context for configuration + +Always use `Context` objects for SDK configuration: + +```py +ctx = Context.from_dict({ + "verify": { + "verify_after_sign": True + }, + "trust": { + "user_anchors": trust_anchors_pem + } +}) + +builder = Builder(manifest_json, context=ctx) +reader = Reader("asset.jpg", context=ctx) +``` + +### Use ingredients to build provenance chains + +Add ingredients to your manifests to maintain a clear provenance chain: + +```py +ingredient_json = json.dumps({ + "title": "Original source", + "relationship": "parentOf" +}) + +with open("original.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +with open("edited.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +## Additional resources + +- [Manifest reference](https://opensource.contentauthenticity.org/docs/manifest/manifest-ref) +- [X.509 certificates](https://opensource.contentauthenticity.org/docs/c2patool/x_509) +- [Trust lists](https://opensource.contentauthenticity.org/docs/conformance/trust-lists/) +- [CAWG identity](https://cawg.io/identity/) From 0f186a35b32518e6a1f3aef2fdd9d52e55072be2 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:47:53 -0800 Subject: [PATCH 02/18] fix: Doc in branch is reference, this one is done --- docs/tmn-wip-docs/usage.md | 196 ------------------------------------- 1 file changed, 196 deletions(-) delete mode 100644 docs/tmn-wip-docs/usage.md diff --git a/docs/tmn-wip-docs/usage.md b/docs/tmn-wip-docs/usage.md deleted file mode 100644 index aeec23a4..00000000 --- a/docs/tmn-wip-docs/usage.md +++ /dev/null @@ -1,196 +0,0 @@ -# Using the Python library - -This package works with media files in the [supported formats](https://github.com/contentauth/c2pa-rs/blob/main/docs/supported-formats.md). - -For complete working examples, see the [examples folder](https://github.com/contentauth/c2pa-python/tree/main/examples) in the repository. - -## Import - -Import the objects needed from the API: - -```py -from c2pa import Builder, Reader, Signer, C2paSigningAlg, C2paSignerInfo -``` - -You can use both `Builder`, `Reader` and `Signer` classes with context managers by using a `with` statement. -Doing this is recommended to ensure proper resource and memory cleanup. - -## Define manifest JSON - -The Python library works with both file-based and stream-based operations. -In both cases, the manifest JSON string defines the C2PA manifest to add to an asset. For example: - -```py -manifest_json = json.dumps({ - "claim_generator": "python_test/0.1", - "assertions": [ - { - "label": "cawg.training-mining", - "data": { - "entries": { - "cawg.ai_inference": { - "use": "notAllowed" - }, - "cawg.ai_generative_training": { - "use": "notAllowed" - } - } - } - } - ] - }) -``` - -## File-based operation - -### Read and validate C2PA data - -Use the `Reader` to read C2PA data from the specified asset file. - -This examines the specified media file for C2PA data and generates a report of any data it finds. If there are validation errors, the report includes a `validation_status` field. - -An asset file may contain many manifests in a manifest store. The most recent manifest is identified by the value of the `active_manifest` field in the manifests map. The manifests may contain binary resources such as thumbnails which can be retrieved with `resource_to_stream` using the associated `identifier` field values and a `uri`. - -NOTE: For a comprehensive reference to the JSON manifest structure, see the [Manifest store reference](https://opensource.contentauthenticity.org/docs/manifest/manifest-ref). - -```py -try: - # Create a reader from a file path - with Reader("path/to/media_file.jpg") as reader: - # Print manifest store as JSON - print("Manifest store:", reader.json()) - - # Get the active manifest. - manifest = json.loads(reader.json()) - active_manifest = manifest["manifests"][manifest["active_manifest"]] - if active_manifest: - # Get the uri to the manifest's thumbnail and write it to a file - uri = active_manifest["thumbnail"]["identifier"] - with open("thumbnail_v2.jpg", "wb") as f: - reader.resource_to_stream(uri, f) - -except Exception as err: - print(err) -``` - -### Add a signed manifest - -**WARNING**: This example accesses the private key and security certificate directly from the local file system. This is fine during development, but doing so in production may be insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; for example as show in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). - -Use a `Builder` to add a manifest to an asset: - -```py -try: - # Create a signer from certificate and key files - with open("path/to/cert.pem", "rb") as cert_file, open("path/to/key.pem", "rb") as key_file: - cert_data = cert_file.read() - key_data = key_file.read() - - # Create signer info using cert and key info - signer_info = C2paSignerInfo( - alg=C2paSigningAlg.PS256, - cert=cert_data, - key=key_data, - timestamp_url="http://timestamp.digicert.com" - ) - - # Create signer using the defined SignerInfo - signer = Signer.from_info(signer_info) - - # Create builder with manifest and add ingredients - with Builder(manifest_json) as builder: - # Add any ingredients if needed - with open("path/to/ingredient.jpg", "rb") as ingredient_file: - ingredient_json = json.dumps({"title": "Ingredient Image"}) - builder.add_ingredient(ingredient_json, "image/jpeg", ingredient_file) - - # Sign the file - with open("path/to/source.jpg", "rb") as source_file, open("path/to/output.jpg", "wb") as dest_file: - manifest_bytes = builder.sign(signer, "image/jpeg", source_file, dest_file) - - # Verify the signed file by reading data from the signed output file - with Reader("path/to/output.jpg") as reader: - manifest_store = json.loads(reader.json()) - active_manifest = manifest_store["manifests"][manifest_store["active_manifest"]] - print("Signed manifest:", active_manifest) - -except Exception as e: - print("Failed to sign manifest store: " + str(e)) -``` - -## Stream-based operation - -Instead of working with files, you can read, validate, and add a signed manifest to streamed data. This example is similar to what the file-based example does. - -### Read and validate C2PA data using streams - -```py -try: - # Create a reader from a format and stream - with open("path/to/media_file.jpg", "rb") as stream: - # First parameter should be the type of the file (here, we use the mimetype) - # But in any case we need something to identify the file type - with Reader("image/jpeg", stream) as reader: - # Print manifest store as JSON, as extracted by the Reader - print("manifest store:", reader.json()) - - # Get the active manifest - manifest = json.loads(reader.json()) - active_manifest = manifest["manifests"][manifest["active_manifest"]] - if active_manifest: - # get the uri to the manifest's thumbnail and write it to a file - uri = active_manifest["thumbnail"]["identifier"] - with open("thumbnail_v2.jpg", "wb") as f: - reader.resource_to_stream(uri, f) - -except Exception as err: - print(err) -``` - -### Add a signed manifest to a stream - -**WARNING**: This example accesses the private key and security certificate directly from the local file system. This is fine during development, but doing so in production may be insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; for example as show in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). - -Use a `Builder` to add a manifest to an asset: - -```py -try: - # Create a signer from certificate and key files - with open("path/to/cert.pem", "rb") as cert_file, open("path/to/key.pem", "rb") as key_file: - cert_data = cert_file.read() - key_data = key_file.read() - - # Create signer info using the read certificate and key data - signer_info = C2paSignerInfo( - alg=C2paSigningAlg.PS256, - cert=cert_data, - key=key_data, - timestamp_url="http://timestamp.digicert.com" - ) - - # Create a Signer using the SignerInfo defined previously - signer = Signer.from_info(signer_info) - - # Create a Builder with manifest and add ingredients - with Builder(manifest_json) as builder: - # Add any ingredients as needed - with open("path/to/ingredient.jpg", "rb") as ingredient_file: - ingredient_json = json.dumps({"title": "Ingredient Image"}) - # Here the ingredient is added using streams - builder.add_ingredient(ingredient_json, "image/jpeg", ingredient_file) - - # Sign using streams - with open("path/to/source.jpg", "rb") as source_file, open("path/to/output.jpg", "wb") as dest_file: - manifest_bytes = builder.sign(signer, "image/jpeg", source_file, dest_file) - - # Verify the signed file - with open("path/to/output.jpg", "rb") as stream: - # Create a Reader to read data - with Reader("image/jpeg", stream) as reader: - manifest_store = json.loads(reader.json()) - active_manifest = manifest_store["manifests"][manifest_store["active_manifest"]] - print("Signed manifest:", active_manifest) - -except Exception as e: - print("Failed to sign manifest store: " + str(e)) -``` From de6f4c90e9fa395b79d1a938d731b032e2f55ffd Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 18:30:56 -0800 Subject: [PATCH 03/18] fix: WIP docs --- docs/tmn-wip-docs/context.md | 102 +++++++++++++++++++++--- docs/tmn-wip-docs/settings.md | 7 +- docs/tmn-wip-docs/working-stores.md | 119 +++++++++++++++++++++++----- 3 files changed, 193 insertions(+), 35 deletions(-) diff --git a/docs/tmn-wip-docs/context.md b/docs/tmn-wip-docs/context.md index ccc84145..2bc4f331 100644 --- a/docs/tmn-wip-docs/context.md +++ b/docs/tmn-wip-docs/context.md @@ -32,14 +32,15 @@ classDiagram +from_json(json_str) Settings$ +from_dict(config) Settings$ +set(path, value) Settings - +update(data, format) Settings + +update(data) Settings +close() +is_valid bool } class ContextProvider { - <> - +is_valid bool + <> + +is_valid bool* + +execution_context* } class Context { @@ -67,7 +68,7 @@ classDiagram class Builder { +from_json(manifest_json, context) Builder$ - +from_archive(stream, context) Builder$ + +from_archive(stream) Builder$ +get_supported_mime_types() list~str~$ +set_no_embed() +set_remote_url(url) @@ -76,7 +77,9 @@ classDiagram +add_ingredient(json, format, source) +add_action(action_json) +to_archive(stream) + +with_archive(stream) Builder +sign(signer, format, source, dest) bytes + +sign_with_context(format, source, dest) bytes +sign_file(source_path, dest_path, signer) bytes +close() } @@ -138,7 +141,7 @@ classDiagram ... } - ContextProvider <|.. Context : satisfies + ContextProvider <|-- Context : extends Settings --> Context : optional input Signer --> Context : optional, consumed C2paSignerInfo --> Signer : creates via from_info @@ -155,6 +158,49 @@ classDiagram > [!NOTE] > The deprecated `load_settings()` function still works for backward compatibility but you are encouraged to migrate your code to use `Context`. See [Migrating from load_settings](#migrating-from-load_settings). +## Workflow overview + +The SDK supports two main workflows. `Settings` and `Context` are optional in both; `Reader` and `Builder` can be used directly with SDK defaults. + +### Reading provenance + +Read and inspect C2PA data already embedded in (or attached to) an asset: + +```text +Asset file ──► Reader ──► Manifest JSON (reader.json()) + └──► Binary resources (reader.resource_to_stream()) +``` + +```py +from c2pa import Reader + +reader = Reader("signed_image.jpg") +print(reader.json()) # Manifest store as JSON +``` + +### Signing content + +Create new C2PA provenance data and sign it into an asset: + +```text +Settings ──► Context ──► Builder ──► sign() ──► Signed asset +(optional) (optional) │ ▲ + │ │ + add assertions Signer + add ingredients +``` + +```py +from c2pa import Builder, Signer, C2paSignerInfo, C2paSigningAlg + +builder = Builder(manifest_json) +# ... add assertions, ingredients, resources ... +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +`Settings` and `Context` are needed only to customize behavior (trust configuration, thumbnail settings, claim generator info, and so on). Without them, the SDK uses sensible defaults. + ## Creating a Context There are several ways to create a `Context`, depending on your needs: @@ -399,6 +445,35 @@ For more information, see [Settings - Offline or air-gapped environments](settin > [!IMPORTANT] > The `Context` is used only when constructing the `Builder`. The `Builder` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Builder`. +### Context and archives + +Archives (`.c2pa` files) store only the manifest definition — they do **not** store settings or context. This means: + +- **`Builder.from_archive(stream)`** creates a context-free builder. All settings revert to SDK defaults regardless of what context the original builder had. +- **`Builder({}, context=ctx).with_archive(stream)`** creates a builder with a context first, then loads the archived manifest definition into it. The context settings are preserved. + +Use `with_archive()` when your workflow depends on specific settings (thumbnails, claim generator, intent, and so on). Use `from_archive()` only for quick prototyping where SDK defaults are acceptable. + +```py +# Recommended: with_archive preserves context settings +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "My App", "version": "1.0"} + } +}) + +with open("manifest.c2pa", "rb") as archive: + builder = Builder({}, context=ctx) + builder.with_archive(archive) + # builder now has the archived definition + context settings + +# NOT recommended when settings matter: +# builder = Builder.from_archive(archive) # context-free, SDK defaults apply +``` + +For more details on archive workflows, see [Working with archives](working-stores.md#working-with-archives). + ### Basic use ```py @@ -446,6 +521,15 @@ mobile_ctx = Context.from_dict({ ## Configuring a signer +### Signing concepts + +C2PA uses a certificate-based trust model to prove who signed an asset. When creating a `Signer`, the following parameters are required: + +- **Certificate chain** (`sign_cert`): An X.509 certificate chain in PEM format. The first certificate identifies the signer; subsequent certificates form a chain up to a trusted root (trust anchor). Verifiers use this chain to confirm that the signature comes from a trusted source. +- **Timestamp authority URL** (`ta_url`): An optional [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161) timestamp server URL. When provided, the SDK requests a trusted timestamp during signing. This proves _when_ the signature was made. Timestamping matters because signatures remain verifiable even after the signing certificate expires, as long as the certificate was valid at the time of signing. + +### Signer creation patterns + You can configure a signer in two ways: - [From Settings (signer-on-context)](#from-settings) @@ -474,7 +558,7 @@ ctx = Context(settings=settings, signer=signer) # Build and sign — no signer argument needed builder = Builder(manifest_json, context=ctx) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: - builder.sign(format="image/jpeg", source=src, dest=dst) + builder.sign_with_context("image/jpeg", src, dst) ``` > [!NOTE] @@ -540,14 +624,14 @@ dev_builder = Builder(manifest, context=dev_ctx) prod_builder = Builder(manifest, context=prod_ctx) ``` -### ContextProvider protocol +### ContextProvider abstract base class -The `ContextProvider` protocol allows third-party implementations of custom context providers. Any class that implements `is_valid` and `_c_context` properties satisfies the protocol and can be passed to `Reader` or `Builder` as `context`. +`ContextProvider` is an abstract base class (ABC) that enables custom context provider implementations. Subclass it and implement the `is_valid` and `execution_context` abstract properties to create a provider that can be passed to `Reader` or `Builder` as `context`. ```py from c2pa import ContextProvider, Context -# The built-in Context satisfies ContextProvider +# The built-in Context inherits from ContextProvider ctx = Context() assert isinstance(ctx, ContextProvider) # True ``` diff --git a/docs/tmn-wip-docs/settings.md b/docs/tmn-wip-docs/settings.md index 89f8c859..68d11a2c 100644 --- a/docs/tmn-wip-docs/settings.md +++ b/docs/tmn-wip-docs/settings.md @@ -21,8 +21,7 @@ Create and configure settings: | `Settings.from_json(json_str)` | Create settings from a JSON string. Raises `C2paError` on parse error. | | `Settings.from_dict(config)` | Create settings from a Python dictionary. | | `set(path, value)` | Set a single value by dot-separated path (e.g. `"verify.verify_after_sign"`). Value must be a string. Returns `self` for chaining. Use this for programmatic configuration. | -| `update(data, format="json")` | Merge JSON configuration into existing settings. `data` can be a JSON string or a dict. Later keys override earlier ones. Use this to apply configuration files or JSON strings. Only `"json"` format is supported. | -| `settings["path"] = "value"` | Dict-like setter. Equivalent to `set(path, value)`. | +| `update(data)` | Merge JSON configuration into existing settings. `data` can be a JSON string or a dict. Later keys override earlier ones. Use this to apply configuration files or JSON strings. | | `is_valid` | Property that returns `True` if the object holds valid resources (not closed). | | `close()` | Release native resources. Called automatically when used as a context manager. | @@ -31,7 +30,6 @@ Create and configure settings: - The `set()` and `update()` methods can be chained for sequential configuration. - When using multiple configuration methods, later calls override earlier ones (last wins). - Use the `with` statement for automatic resource cleanup. -- Only JSON format is supported for settings in the Python SDK. ```py from c2pa import Settings @@ -45,9 +43,6 @@ settings.set("builder.thumbnail.enabled", "false") # Method chaining settings.set("builder.thumbnail.enabled", "false").set("verify.verify_after_sign", "true") -# Dict-like access -settings["builder.thumbnail.enabled"] = "false" - # Create from JSON string settings = Settings.from_json('{"builder": {"thumbnail": {"enabled": false}}}') diff --git a/docs/tmn-wip-docs/working-stores.md b/docs/tmn-wip-docs/working-stores.md index 801816dd..3e6d7847 100644 --- a/docs/tmn-wip-docs/working-stores.md +++ b/docs/tmn-wip-docs/working-stores.md @@ -120,6 +120,55 @@ reader = Reader("signed_image.jpg", context=ctx) manifest_json = reader.json() ``` +### Understanding Reader output + +`Reader.json()` returns a JSON string representing the manifest store. The top-level structure looks like this: + +```json +{ + "active_manifest": "urn:uuid:...", + "manifests": { + "urn:uuid:...": { + "claim_generator": "MyApp/1.0", + "claim_generator_info": [{"name": "MyApp", "version": "1.0"}], + "title": "signed_image.jpg", + "assertions": [ + {"label": "c2pa.actions", "data": {"actions": [...]}}, + {"label": "c2pa.hash.data", "data": {...}} + ], + "ingredients": [...], + "signature_info": { + "alg": "Es256", + "issuer": "...", + "time": "2025-01-15T12:00:00Z" + } + } + } +} +``` + +- `active_manifest`: The URI label of the most recent manifest. This is typically the one to inspect first. +- `manifests`: A dictionary of all manifests in the store, keyed by their URI label. Assets that have been re-signed or that contain ingredient history may have multiple manifests. +- Within each manifest: `assertions` contain the provenance statements, `ingredients` list source materials, and `signature_info` provides details about who signed and when. + +The SDK also provides convenience methods to avoid manual JSON parsing: + +```py +reader = Reader("signed_image.jpg") + +# Get the active manifest directly as a dict +active = reader.get_active_manifest() + +# Get a specific manifest by label +manifest = reader.get_manifest("urn:uuid:...") + +# Check validation status +state = reader.get_validation_state() +results = reader.get_validation_results() +``` + +`Reader.detailed_json()` returns a more comprehensive JSON representation with a different structure than `json()`. It is useful when additional details about the manifest internals are needed. + ## Using working stores A **working store** is represented by a `Builder` object. It contains "live" manifest data as you add information to it. @@ -323,6 +372,8 @@ except Exception as e: ## Working with resources +C2PA manifest data is not just JSON. A manifest store also contains binary resources (thumbnails, ingredient data, and other embedded files) that are referenced from the JSON metadata by JUMBF URIs. When `reader.json()` is called, the JSON includes URI references (like `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`) that point to these binary resources. To retrieve the actual binary data, use `reader.resource_to_stream()` with the URI from the JSON. This separation keeps the JSON lightweight while allowing manifests to carry rich binary content alongside the metadata. + _Resources_ are binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. ### Understanding resource identifiers @@ -386,6 +437,14 @@ with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: ## Working with ingredients +### Why ingredients matter + +Ingredients are how C2PA tracks the history of content through edits, compositions, and transformations. Adding an ingredient to a manifest creates a verifiable link from the current asset back to its source material. This builds a **provenance chain**: original photo, then edited version, then composite, then published asset. Each link in the chain carries its own signed manifest, so anyone inspecting the final asset can trace its full history and verify that each step was authentic. + +The `relationship` field describes _how_ the source was used: `"parentOf"` for a direct edit, `"componentOf"` for an element composited into a larger work, or `"inputTo"` for a general input. This lets verifiers understand not just _what_ the sources were, but how they contributed to the final asset. + +### Overview + Ingredients represent source materials used to create an asset, preserving the provenance chain. Ingredients themselves can be turned into ingredient archives (`.c2pa`). An ingredient archive is a serialized `Builder` with _exactly one_ ingredient. Once archived with only one ingredient, the Builder archive is an ingredient archive. Such ingredient archives can be used as ingredient in other working stores. @@ -407,9 +466,6 @@ ingredient_json = json.dumps({ with open("source.jpg", "rb") as ingredient: builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) -# Or add ingredient from a file path -builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", "source.jpg") - # Sign: ingredients become part of the manifest store with open("new_asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: builder.sign(signer, "image/jpeg", src, dst) @@ -478,39 +534,62 @@ A Builder containing **only one ingredient and only the ingredient data** (no ot ### Restoring a working store from archive -Create a new `Builder` (working store) from an archive: - -```py -# Restore from stream -with open("manifest.c2pa", "rb") as archive: - builder = Builder.from_archive(archive) +There are two ways to load a working store from an archive. They differ in whether the builder's context (settings) is preserved. -# Now you can sign with the restored working store -with open("asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) -``` +#### `with_archive()` — recommended -### Restoring with context preservation - -Pass a `context` to `from_archive()` to preserve custom settings: +Use `with_archive()` when you need the restored builder to use specific settings (thumbnail configuration, claim generator info, intent, verification options, and so on). Create a `Builder` with a `Context` first, then call `with_archive()` to load the archived manifest definition into it. The archive replaces only the manifest definition; the builder's context and settings are preserved. ```py # Create context with custom settings ctx = Context.from_dict({ "builder": { - "thumbnail": {"enabled": False} + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "My App", "version": "1.0"} } }) -# Load archive with context +# Create builder with context, then load archive into it with open("manifest.c2pa", "rb") as archive: - builder = Builder.from_archive(archive, context=ctx) + builder = Builder({}, context=ctx) + builder.with_archive(archive) -# The builder has the archived manifest but keeps the custom context +# The builder has the archived manifest definition +# but keeps the context settings (no thumbnails, custom claim generator) with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: builder.sign(signer, "image/jpeg", src, dst) ``` +> [!IMPORTANT] +> `with_archive()` replaces the builder's manifest definition with the one from the archive. Any definition passed to `Builder()` is discarded. An empty dict `{}` is idiomatic for the initial definition when you plan to load an archive immediately after. + +#### `from_archive()` — context-free + +Use `from_archive()` for quick one-off operations where you don't need custom settings. It creates a **context-free** builder: no `Context` is attached, so all settings revert to SDK defaults (thumbnails enabled at 1024px, verification enabled, and so on). + +```py +# Restore from stream — no context, SDK defaults apply +with open("manifest.c2pa", "rb") as archive: + builder = Builder.from_archive(archive) + +# Sign with SDK default settings +with open("asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +> [!WARNING] +> `from_archive()` does not accept a `context` parameter. Any settings that were active when the archive was created are **not** stored in the archive and are lost. For example, if the original builder had thumbnails disabled via a `Context`, the builder returned by `from_archive()` will generate thumbnails using SDK defaults. Use `with_archive()` instead when you need to preserve settings. + +#### Choosing between `with_archive()` and `from_archive()` + +| | `with_archive()` | `from_archive()` | +|---|---|---| +| **Context preserved** | Yes — settings come from the builder's context | No — SDK defaults apply | +| **Usage pattern** | `Builder({}, context=ctx).with_archive(stream)` | `Builder.from_archive(stream)` | +| **When to use** | Production workflows, custom settings needed | Quick prototyping, SDK defaults are acceptable | +| **What the archive carries** | Only the manifest definition | Only the manifest definition | +| **What it does NOT carry** | Settings, signer, context | Settings, signer, context | + ### Two-phase workflow example #### Phase 1: Prepare manifest From d42c1381c24aa7c7d05d233af7272cdf14e8b9eb Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 19:30:37 -0800 Subject: [PATCH 04/18] fix: Docs --- docs/tmn-wip-docs/context.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/tmn-wip-docs/context.md b/docs/tmn-wip-docs/context.md index 2bc4f331..855075ff 100644 --- a/docs/tmn-wip-docs/context.md +++ b/docs/tmn-wip-docs/context.md @@ -147,8 +147,8 @@ classDiagram C2paSignerInfo --> Signer : creates via from_info C2paSigningAlg --> C2paSignerInfo : alg field C2paSigningAlg --> Signer : from_callback alg - Context --> Reader : optional context= - Context --> Builder : optional context= + Context --> Reader : context= + Context --> Builder : context= Signer --> Builder : sign(signer) C2paBuilderIntent --> Builder : set_intent C2paDigitalSourceType --> Builder : set_intent @@ -166,9 +166,11 @@ The SDK supports two main workflows. `Settings` and `Context` are optional in bo Read and inspect C2PA data already embedded in (or attached to) an asset: -```text -Asset file ──► Reader ──► Manifest JSON (reader.json()) - └──► Binary resources (reader.resource_to_stream()) +```mermaid +flowchart LR + A[Asset file] --> B[Reader] + B --> C["Manifest JSON (reader.json())"] + B --> D["Binary resources (reader.resource_to_stream())"] ``` ```py @@ -182,12 +184,14 @@ print(reader.json()) # Manifest store as JSON Create new C2PA provenance data and sign it into an asset: -```text -Settings ──► Context ──► Builder ──► sign() ──► Signed asset -(optional) (optional) │ ▲ - │ │ - add assertions Signer - add ingredients +```mermaid +flowchart LR + A["Settings (optional)"] --> B["Context (optional)"] + B --> C[Builder] + C --> D["sign()"] + D --> E[Signed asset] + F[Signer] --> D + G[add assertions\nadd ingredients] --> C ``` ```py From fa7d6ef3ff09af6b97adbb023988fcb0abd2a6b9 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 19:39:00 -0800 Subject: [PATCH 05/18] fix: Docs --- docs/tmn-wip-docs/context.md | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/tmn-wip-docs/context.md b/docs/tmn-wip-docs/context.md index 855075ff..0b88e601 100644 --- a/docs/tmn-wip-docs/context.md +++ b/docs/tmn-wip-docs/context.md @@ -18,7 +18,7 @@ Context encapsulates SDK configuration: - **Enables multiple configurations**: Run different configurations simultaneously. For example, one for development with test certificates, another for production with strict validation. - **Eliminates global state**: Each `Reader` and `Builder` gets its configuration from the `Context` you pass, avoiding subtle bugs from shared state. - **Simplifies testing**: Create isolated configurations for tests without worrying about cleanup or interference between them. -- **Improves code clarity**: Reading `Builder(manifest_json, context=ctx)` immediately shows that configuration is being used. +- **Improves code clarity**: Reading `Builder(manifest_json, ctx)` immediately shows that configuration is being used. ### Class diagram @@ -160,7 +160,7 @@ classDiagram ## Workflow overview -The SDK supports two main workflows. `Settings` and `Context` are optional in both; `Reader` and `Builder` can be used directly with SDK defaults. +The SDK supports two main workflows. `Settings` and `Context` are currently optional in both (but recommended). `Reader` and `Builder` can still be used directly with SDK defaults. ### Reading provenance @@ -168,7 +168,7 @@ Read and inspect C2PA data already embedded in (or attached to) an asset: ```mermaid flowchart LR - A[Asset file] --> B[Reader] + A[Asset file] --> B["Reader (with Context containing Settings)"] B --> C["Manifest JSON (reader.json())"] B --> D["Binary resources (reader.resource_to_stream())"] ``` @@ -186,7 +186,7 @@ Create new C2PA provenance data and sign it into an asset: ```mermaid flowchart LR - A["Settings (optional)"] --> B["Context (optional)"] + A["Settings"] --> B["Context"] B --> C[Builder] C --> D["sign()"] D --> E[Signed asset] @@ -203,7 +203,7 @@ with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: builder.sign(signer, "image/jpeg", src, dst) ``` -`Settings` and `Context` are needed only to customize behavior (trust configuration, thumbnail settings, claim generator info, and so on). Without them, the SDK uses sensible defaults. +`Settings` and `Context` enable to customize behavior (trust configuration, thumbnail settings, claim generator info, and so on). ## Creating a Context @@ -218,7 +218,7 @@ There are several ways to create a `Context`, depending on your needs: The simplest approach is using [SDK default settings](settings.md#default-configuration). -**When to use:** For quick prototyping, or when you're happy with default behavior (verification enabled, thumbnails enabled at 1024px, and so on). +**When to use:** For quick prototyping, or when you're happy with SDK default behavior (verification enabled, thumbnails enabled at 1024px, and so on). ```py from c2pa import Context @@ -276,7 +276,7 @@ settings.update({ } }) -ctx = Context(settings=settings) +ctx = Context(settings) ``` ## Common configuration patterns @@ -322,7 +322,7 @@ if env == "production": else: settings.update({"verify": {"remote_manifest_fetch": False}}) -ctx = Context(settings=settings) +ctx = Context(settings) ``` ### Layered configuration @@ -341,7 +341,7 @@ settings = Settings.from_dict(base_config) # Apply environment-specific overrides settings.update({"builder": {"claim_generator_info": {"version": app_version}}}) -ctx = Context(settings=settings) +ctx = Context(settings) ``` For the full list of settings and defaults, see [Using settings](settings.md). @@ -355,7 +355,7 @@ Use `Context` to control how `Reader` validates manifests and handles remote res - [**Network access**](#offline-operation): Whether to fetch remote manifests or OCSP responses. > [!IMPORTANT] -> `Context` is used only at construction. `Reader` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Reader`. +> `Context` is used only at construction. `Reader` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Reader`. A `Context` object can also be reused for multiple `Reader` object instances. ```py ctx = Context.from_dict({"verify": {"remote_manifest_fetch": False}}) @@ -447,14 +447,14 @@ For more information, see [Settings - Offline or air-gapped environments](settin - **Signer configuration** (optional): Credentials can be stored in the context for reuse. > [!IMPORTANT] -> The `Context` is used only when constructing the `Builder`. The `Builder` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Builder`. +> The `Context` is used only when constructing the `Builder`. The `Builder` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Builder`. A `Context` object can also be reused for multiple `Builder` object instances. ### Context and archives Archives (`.c2pa` files) store only the manifest definition — they do **not** store settings or context. This means: - **`Builder.from_archive(stream)`** creates a context-free builder. All settings revert to SDK defaults regardless of what context the original builder had. -- **`Builder({}, context=ctx).with_archive(stream)`** creates a builder with a context first, then loads the archived manifest definition into it. The context settings are preserved. +- **`Builder({}, ctx).with_archive(stream)`** creates a builder with a context first, then loads the archived manifest definition into it. The context settings are preserved and propagated to this Builder instance. Use `with_archive()` when your workflow depends on specific settings (thumbnails, claim generator, intent, and so on). Use `from_archive()` only for quick prototyping where SDK defaults are acceptable. @@ -468,7 +468,7 @@ ctx = Context.from_dict({ }) with open("manifest.c2pa", "rb") as archive: - builder = Builder({}, context=ctx) + builder = Builder({}, ctx) builder.with_archive(archive) # builder now has the archived definition + context settings @@ -491,7 +491,7 @@ ctx = Context.from_dict({ } }) -builder = Builder(manifest_json, context=ctx) +builder = Builder(manifest_json, ctx) # Pass signer explicitly at signing time with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: @@ -556,11 +556,11 @@ signer_info = C2paSignerInfo( signer = Signer.from_info(signer_info) # Create context with signer (signer is consumed) -ctx = Context(settings=settings, signer=signer) +ctx = Context(settings, signer) # signer is now invalid and must not be used again # Build and sign — no signer argument needed -builder = Builder(manifest_json, context=ctx) +builder = Builder(manifest_json, ctx) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: builder.sign_with_context("image/jpeg", src, dst) ``` @@ -574,7 +574,7 @@ For full programmatic control, create a `Signer` and pass it directly to `Builde ```py signer = Signer.from_info(signer_info) -builder = Builder(manifest_json, context=ctx) +builder = Builder(manifest_json, ctx) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: builder.sign(signer, "image/jpeg", src, dst) @@ -605,11 +605,11 @@ with Context() as ctx: You can reuse the same `Context` to create multiple readers and builders: ```py -ctx = Context(settings=settings) +ctx = Context(settings) # All three use the same configuration -builder1 = Builder(manifest1, context=ctx) -builder2 = Builder(manifest2, context=ctx) +builder1 = Builder(manifest1, ctx) +builder2 = Builder(manifest2, ctx) reader = Reader("image.jpg", context=ctx) # Context can be closed after construction; readers/builders still work @@ -620,12 +620,12 @@ reader = Reader("image.jpg", context=ctx) Use different `Context` objects when you need different settings; for example, for development vs. production, or different trust configurations: ```py -dev_ctx = Context(settings=dev_settings) -prod_ctx = Context(settings=prod_settings) +dev_ctx = Context(dev_settings) +prod_ctx = Context(prod_settings) # Different builders with different configurations -dev_builder = Builder(manifest, context=dev_ctx) -prod_builder = Builder(manifest, context=prod_ctx) +dev_builder = Builder(manifest, dev_ctx) +prod_builder = Builder(manifest, prod_ctx) ``` ### ContextProvider abstract base class @@ -665,7 +665,7 @@ reader = Reader("image.jpg") # uses global settings from c2pa import Settings, Context, Reader settings = Settings.from_dict({"builder": {"thumbnail": {"enabled": False}}}) -ctx = Context(settings=settings) +ctx = Context(settings) reader = Reader("image.jpg", context=ctx) ``` From f3ed23777ae9217c60666bb65675fbd6047952b2 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 19:46:03 -0800 Subject: [PATCH 06/18] fix: Docs --- docs/tmn-wip-docs/context.md | 44 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/tmn-wip-docs/context.md b/docs/tmn-wip-docs/context.md index 0b88e601..f05ee70d 100644 --- a/docs/tmn-wip-docs/context.md +++ b/docs/tmn-wip-docs/context.md @@ -459,7 +459,7 @@ Archives (`.c2pa` files) store only the manifest definition — they do **not** Use `with_archive()` when your workflow depends on specific settings (thumbnails, claim generator, intent, and so on). Use `from_archive()` only for quick prototyping where SDK defaults are acceptable. ```py -# Recommended: with_archive preserves context settings +# Recommended: with_archive propagates context settings ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, @@ -471,9 +471,6 @@ with open("manifest.c2pa", "rb") as archive: builder = Builder({}, ctx) builder.with_archive(archive) # builder now has the archived definition + context settings - -# NOT recommended when settings matter: -# builder = Builder.from_archive(archive) # context-free, SDK defaults apply ``` For more details on archive workflows, see [Working with archives](working-stores.md#working-with-archives). @@ -509,7 +506,7 @@ no_thumbnails_ctx = Context.from_dict({ } }) -# Or customize thumbnail size and quality for mobile +# Or customize thumbnail size and quality e.g. for mobile mobile_ctx = Context.from_dict({ "builder": { "claim_generator_info": {"name": "Mobile App"}, @@ -523,7 +520,7 @@ mobile_ctx = Context.from_dict({ }) ``` -## Configuring a signer +## Configuring a Signer ### Signing concepts @@ -534,14 +531,14 @@ C2PA uses a certificate-based trust model to prove who signed an asset. When cre ### Signer creation patterns -You can configure a signer in two ways: +A Signer can be configured two ways: -- [From Settings (signer-on-context)](#from-settings) -- [Explicit signer passed to sign()](#explicit-signer) +- [From Settings (signer-on-context)](#from-settings) — pass the signer when creating the `Context`. +- [Explicit signer passed to sign()](#explicit-programmatic-signer) — pass the signer directly at signing time. ### From Settings -Create a `Signer` and pass it to the `Context`. The signer is **consumed** — the `Signer` object becomes invalid after this call and must not be reused. The `Context` takes ownership of the underlying native signer. +Create a `Signer` and pass it to the `Context`. The signer is **consumed**: the `Signer` object becomes invalid after this call and must not be reused directly after that point. The `Context` takes ownership of the underlying native signer. ```py from c2pa import Context, Settings, Builder, Signer, C2paSignerInfo, C2paSigningAlg @@ -557,18 +554,15 @@ signer = Signer.from_info(signer_info) # Create context with signer (signer is consumed) ctx = Context(settings, signer) -# signer is now invalid and must not be used again +# signer is now invalid and must not be used directly again -# Build and sign — no signer argument needed +# Build and sign, no signer argument needed since a Signer is in the Context builder = Builder(manifest_json, ctx) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: builder.sign_with_context("image/jpeg", src, dst) ``` -> [!NOTE] -> Signer-on-context requires a compatible version of the native c2pa-c library. If the library does not support this feature, a `C2paError` is raised when passing a `Signer` to `Context`. - -### Explicit signer +### Explicit (programmatic) signer For full programmatic control, create a `Signer` and pass it directly to `Builder.sign()`: @@ -580,6 +574,14 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: builder.sign(signer, "image/jpeg", src, dst) ``` +You can also use the fluent `ContextBuilder` API to attach a signer programmatically via `with_signer`: + +```py +ctx = Context.builder().with_settings(settings).with_signer(signer).build() +``` + +### Precedence rules for Signer configuration + If both an explicit signer and a context signer are available, the explicit signer always takes precedence: ```py @@ -589,7 +591,7 @@ builder.sign(explicit_signer, "image/jpeg", source, dest) ## Context lifetime and usage -### Context as a context manager +### `with` statement `Context` supports the `with` statement for automatic resource cleanup: @@ -607,7 +609,7 @@ You can reuse the same `Context` to create multiple readers and builders: ```py ctx = Context(settings) -# All three use the same configuration +# All three use the same configuration through usage of the same context builder1 = Builder(manifest1, ctx) builder2 = Builder(manifest2, ctx) reader = Reader("image.jpg", context=ctx) @@ -617,7 +619,7 @@ reader = Reader("image.jpg", context=ctx) ### Multiple contexts for different purposes -Use different `Context` objects when you need different settings; for example, for development vs. production, or different trust configurations: +Use different `Context` objects when you need different settings. Ror example, for development vs. production, or different trust configurations: ```py dev_ctx = Context(dev_settings) @@ -630,7 +632,7 @@ prod_builder = Builder(manifest, prod_ctx) ### ContextProvider abstract base class -`ContextProvider` is an abstract base class (ABC) that enables custom context provider implementations. Subclass it and implement the `is_valid` and `execution_context` abstract properties to create a provider that can be passed to `Reader` or `Builder` as `context`. +`ContextProvider` is an abstract base class (ABC) that enables context provider implementations. Subclass it and implement the `is_valid` and `execution_context` abstract properties to create a provider that can be passed to `Reader` or `Builder` as `Context`. ```py from c2pa import ContextProvider, Context @@ -642,7 +644,7 @@ assert isinstance(ctx, ContextProvider) # True ## Migrating from load_settings -The `load_settings()` function is deprecated. Replace it with `Settings` and `Context`: +The `load_settings()` function is deprecated. Replace it with `Settings` and `Context` APIs instead: | Aspect | load_settings (legacy) | Context | |--------|------------------------|---------| From 862015a29fc44fca197456f9a2d3d576c7b29f87 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 19:46:14 -0800 Subject: [PATCH 07/18] fix: Docs --- docs/{tmn-wip-docs => }/context.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{tmn-wip-docs => }/context.md (100%) diff --git a/docs/tmn-wip-docs/context.md b/docs/context.md similarity index 100% rename from docs/tmn-wip-docs/context.md rename to docs/context.md From b3accbd29471d83e1cdd27edd2b9d27eff85e3c1 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 19:53:11 -0800 Subject: [PATCH 08/18] fix: Docs --- docs/tmn-wip-docs/settings.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/tmn-wip-docs/settings.md b/docs/tmn-wip-docs/settings.md index 68d11a2c..bb248c69 100644 --- a/docs/tmn-wip-docs/settings.md +++ b/docs/tmn-wip-docs/settings.md @@ -22,14 +22,11 @@ Create and configure settings: | `Settings.from_dict(config)` | Create settings from a Python dictionary. | | `set(path, value)` | Set a single value by dot-separated path (e.g. `"verify.verify_after_sign"`). Value must be a string. Returns `self` for chaining. Use this for programmatic configuration. | | `update(data)` | Merge JSON configuration into existing settings. `data` can be a JSON string or a dict. Later keys override earlier ones. Use this to apply configuration files or JSON strings. | -| `is_valid` | Property that returns `True` if the object holds valid resources (not closed). | -| `close()` | Release native resources. Called automatically when used as a context manager. | **Important notes:** -- The `set()` and `update()` methods can be chained for sequential configuration. -- When using multiple configuration methods, later calls override earlier ones (last wins). -- Use the `with` statement for automatic resource cleanup. +- The `set()` and `update()` methods can be chained for incremental configuration. +- When using multiple configuration methods, later calls override earlier ones (last call wins when same setting is set multiple times). ```py from c2pa import Settings @@ -72,7 +69,7 @@ The Settings JSON has this top-level structure: ### Settings format -Settings are provided in **JSON** only. Pass JSON strings to `Settings.from_json()` or dictionaries to `Settings.from_dict()`. +Settings are provided in **JSON** format only. Pass JSON strings (serialized JSON stings) to `Settings.from_json()` or dictionaries to `Settings.from_dict()`. `from_dict` will convert the dictionary in a format compatible with what the udnerlying native libraries expect. ```py # From JSON string @@ -99,7 +96,7 @@ with open("config/settings.json", "r") as f: ## Default configuration -The settings JSON schema — including the complete default configuration with all properties and their default values — is shared with all languages in the SDK: +The settings JSON schema — including the complete default configuration with all properties and their default values — is shared by all languages in the SDK: ```json { @@ -397,7 +394,7 @@ camera_ctx = Context.from_dict({ }) ``` -Or for editing existing content: +Or another example for editing existing content: ```py editor_ctx = Context.from_dict({ @@ -412,8 +409,7 @@ editor_ctx = Context.from_dict({ The [`signer` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signersettings) configure the primary C2PA signer configuration. Set it to `null` if you provide the signer at runtime, or configure as either a **local** or **remote** signer in settings. -> [!NOTE] -> The typical approach in Python is to create a `Signer` object with `Signer.from_info()` and pass it directly to `Builder.sign()`. Alternatively, pass a `Signer` to `Context` for the signer-on-context pattern. See [Configuring a signer](context.md#configuring-a-signer) for details. +See [Configuring a signer](context.md#configuring-a-signer) for details on how to configure a Signer. #### Local signer From b3eb697e2399d5e10f27e2c64900201fd91166d7 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:02:11 -0800 Subject: [PATCH 09/18] fix: Docs --- docs/{tmn-wip-docs => }/settings.md | 0 docs/tmn-wip-docs/working-stores.md | 146 ++++++++++++++++++++++------ 2 files changed, 115 insertions(+), 31 deletions(-) rename docs/{tmn-wip-docs => }/settings.md (100%) diff --git a/docs/tmn-wip-docs/settings.md b/docs/settings.md similarity index 100% rename from docs/tmn-wip-docs/settings.md rename to docs/settings.md diff --git a/docs/tmn-wip-docs/working-stores.md b/docs/tmn-wip-docs/working-stores.md index 3e6d7847..498be397 100644 --- a/docs/tmn-wip-docs/working-stores.md +++ b/docs/tmn-wip-docs/working-stores.md @@ -83,43 +83,44 @@ Use the `Reader` class to read manifest stores from signed assets. from c2pa import Reader try: - # Create a Reader from a signed asset file + # Without Context reader = Reader("signed_image.jpg") - - # Get the manifest store as JSON manifest_store_json = reader.json() except Exception as e: print(f"C2PA Error: {e}") ``` -### Reading from a stream - -```py -with open("signed_image.jpg", "rb") as stream: - # Create Reader from stream with MIME type - reader = Reader("image/jpeg", stream) - manifest_json = reader.json() -``` - -### Using Context for configuration - -For more control over validation and trust settings, use a `Context`: - ```py from c2pa import Context, Reader -# Create context with custom validation settings +# With Context (custom validation and trust settings) ctx = Context.from_dict({ "verify": { "verify_after_sign": True } }) - -# Use context when creating Reader reader = Reader("signed_image.jpg", context=ctx) -manifest_json = reader.json() +manifest_store_json = reader.json() +``` + +### Reading from a stream + +```py +# Without Context +with open("signed_image.jpg", "rb") as stream: + reader = Reader("image/jpeg", stream) + manifest_json = reader.json() +``` + +```py +# With Context +with open("signed_image.jpg", "rb") as stream: + reader = Reader("image/jpeg", stream, context=ctx) + manifest_json = reader.json() ``` +For full details on `Context` and `Settings`, see [Using Context to configure the SDK](../context.md). + ### Understanding Reader output `Reader.json()` returns a JSON string representing the manifest store. The top-level structure looks like this: @@ -177,9 +178,9 @@ A **working store** is represented by a `Builder` object. It contains "live" man ```py import json -from c2pa import Builder, Context +from c2pa import Builder -# Create a working store with a manifest definition +# Without Context manifest_json = json.dumps({ "claim_generator_info": [{ "name": "example-app", @@ -190,8 +191,12 @@ manifest_json = json.dumps({ }) builder = Builder(manifest_json) +``` -# Or with custom context +```py +from c2pa import Builder, Context + +# With Context (custom settings applied) ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": True} @@ -262,10 +267,12 @@ manifest_store_json = reader.json() ### Creating a Builder (working store) ```py -# Create with manifest definition +# Without Context builder = Builder(manifest_json) +``` -# Or with custom context +```py +# With Context ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": True} @@ -302,13 +309,22 @@ signer = Signer.from_info(signer_info) ### Signing an asset ```py +# Without Context (explicit signer) try: - # Sign using streams with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: manifest_bytes = builder.sign(signer, "image/jpeg", src, dst) - print("Signed successfully!") +except Exception as e: + print(f"Signing failed: {e}") +``` +```py +# With Context (signer configured in context) +# The Builder must have been created with a Context that has a signer. +try: + with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + manifest_bytes = builder.sign_with_context("image/jpeg", src, dst) + print("Signed successfully!") except Exception as e: print(f"Signing failed: {e}") ``` @@ -318,10 +334,13 @@ except Exception as e: You can also sign using file paths directly: ```py -# Sign using file paths (uses native Rust file I/O for better performance) -manifest_bytes = builder.sign_file( - "source.jpg", "signed.jpg", signer -) +# Without Context (explicit signer) +manifest_bytes = builder.sign_file("source.jpg", "signed.jpg", signer) +``` + +```py +# With Context (uses the context's signer when no signer argument is passed) +manifest_bytes = builder.sign_file("source.jpg", "signed.jpg") ``` ### Complete example @@ -370,6 +389,54 @@ except Exception as e: print(f"Error: {e}") ``` +### Complete example with Context + +```py +import json +from c2pa import Builder, Reader, Context, Signer, C2paSignerInfo, C2paSigningAlg + +try: + # 1. Define manifest + manifest_json = json.dumps({ + "claim_generator_info": [{"name": "demo-app", "version": "0.1.0"}], + "title": "Signed image", + "assertions": [] + }) + + # 2. Load credentials and create signer + with open("certs.pem", "rb") as f: + certs = f.read() + with open("private_key.pem", "rb") as f: + private_key = f.read() + + signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, + sign_cert=certs, + private_key=private_key, + ta_url=b"http://timestamp.digicert.com" + ) + signer = Signer.from_info(signer_info) + + # 3. Create context with settings and signer + ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": True}} + }, signer=signer) + + # 4. Create Builder with context and sign + builder = Builder(manifest_json, context=ctx) + with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign_with_context("image/jpeg", src, dst) + + print("Asset signed with context settings") + + # 5. Read back the manifest store + reader = Reader("signed.jpg", context=ctx) + print(reader.json()) + +except Exception as e: + print(f"Error: {e}") +``` + ## Working with resources C2PA manifest data is not just JSON. A manifest store also contains binary resources (thumbnails, ingredient data, and other embedded files) that are referenced from the JSON metadata by JUMBF URIs. When `reader.json()` is called, the JSON includes URI references (like `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`) that point to these binary resources. To retrieve the actual binary data, use `reader.resource_to_stream()` with the URI from the JSON. This separation keeps the JSON lightweight while allowing manifests to carry rich binary content alongside the metadata. @@ -632,6 +699,23 @@ with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst: print("Asset signed with manifest store") ``` +#### Phase 2 alternative: Sign with context + +```py +# Restore the working store with context settings preserved +ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": False}} +}, signer=signer) + +with open("artwork_manifest.c2pa", "rb") as archive: + builder = Builder({}, context=ctx) + builder.with_archive(archive) + +# Sign using the context's signer +with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst: + builder.sign_with_context("image/jpeg", src, dst) +``` + ## Embedded vs external manifests By default, manifest stores are **embedded** directly into the asset file. You can also use **external** or **remote** manifest stores. From 6cf020772254b516a5ce8ac9c52d05ddef7a4364 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:03:05 -0800 Subject: [PATCH 10/18] fix: Docs --- docs/tmn-wip-docs/working-stores.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tmn-wip-docs/working-stores.md b/docs/tmn-wip-docs/working-stores.md index 498be397..3b325e8e 100644 --- a/docs/tmn-wip-docs/working-stores.md +++ b/docs/tmn-wip-docs/working-stores.md @@ -23,7 +23,7 @@ graph TD A[Working Store
Builder object] -->|sign| MS A -->|to_archive| C[C2PA Archive
.c2pa file] - C -->|from_archive| A + C -->|from_archive or with_archive| A ``` ## Key entities From 10cdfe9801a7eb0e629ce35454b63fc6d99e0d65 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:19:23 -0800 Subject: [PATCH 11/18] fix: Docs --- docs/{tmn-wip-docs => }/working-stores.md | 93 +++++++++++++---------- 1 file changed, 52 insertions(+), 41 deletions(-) rename docs/{tmn-wip-docs => }/working-stores.md (89%) diff --git a/docs/tmn-wip-docs/working-stores.md b/docs/working-stores.md similarity index 89% rename from docs/tmn-wip-docs/working-stores.md rename to docs/working-stores.md index 3b325e8e..1552345d 100644 --- a/docs/tmn-wip-docs/working-stores.md +++ b/docs/working-stores.md @@ -343,29 +343,28 @@ manifest_bytes = builder.sign_file("source.jpg", "signed.jpg", signer) manifest_bytes = builder.sign_file("source.jpg", "signed.jpg") ``` -### Complete example +### Complete example with Context This code combines the above examples to create, sign, and read a manifest. ```py import json -from c2pa import Builder, Reader, Signer, C2paSignerInfo, C2paSigningAlg +from c2pa import Builder, Reader, Context, Signer, C2paSignerInfo, C2paSigningAlg try: - # 1. Define manifest for working store + # 1. Define manifest manifest_json = json.dumps({ "claim_generator_info": [{"name": "demo-app", "version": "0.1.0"}], "title": "Signed image", "assertions": [] }) - # 2. Load credentials + # 2. Load credentials and create signer with open("certs.pem", "rb") as f: certs = f.read() with open("private_key.pem", "rb") as f: private_key = f.read() - # 3. Create signer signer_info = C2paSignerInfo( alg=C2paSigningAlg.ES256, sign_cert=certs, @@ -374,41 +373,49 @@ try: ) signer = Signer.from_info(signer_info) - # 4. Create working store (Builder) and sign - builder = Builder(manifest_json) + # 3. Create context with settings and signer + ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": True}} + }, signer=signer) + + # 4. Create Builder with context and sign + builder = Builder(manifest_json, context=ctx) with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign_with_context("image/jpeg", src, dst) - print("Asset signed - working store is now a manifest store") + print("Asset signed with context settings") # 5. Read back the manifest store - reader = Reader("signed.jpg") + reader = Reader("signed.jpg", context=ctx) print(reader.json()) except Exception as e: print(f"Error: {e}") ``` -### Complete example with Context +### Complete example (legacy, without Context) + +This code combines the examples to create, sign, and read a manifest. ```py import json -from c2pa import Builder, Reader, Context, Signer, C2paSignerInfo, C2paSigningAlg +from c2pa import Builder, Reader, Signer, C2paSignerInfo, C2paSigningAlg try: - # 1. Define manifest + # 1. Define manifest for working store manifest_json = json.dumps({ "claim_generator_info": [{"name": "demo-app", "version": "0.1.0"}], "title": "Signed image", "assertions": [] }) - # 2. Load credentials and create signer + # 2. Load credentials with open("certs.pem", "rb") as f: certs = f.read() with open("private_key.pem", "rb") as f: private_key = f.read() + # 3. Create signer signer_info = C2paSignerInfo( alg=C2paSigningAlg.ES256, sign_cert=certs, @@ -417,20 +424,15 @@ try: ) signer = Signer.from_info(signer_info) - # 3. Create context with settings and signer - ctx = Context.from_dict({ - "builder": {"thumbnail": {"enabled": True}} - }, signer=signer) - - # 4. Create Builder with context and sign - builder = Builder(manifest_json, context=ctx) + # 4. Create working store (Builder) and sign + builder = Builder(manifest_json) with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign_with_context("image/jpeg", src, dst) + builder.sign(signer, "image/jpeg", src, dst) - print("Asset signed with context settings") + print("Asset signed - working store is now a manifest store") # 5. Read back the manifest store - reader = Reader("signed.jpg", context=ctx) + reader = Reader("signed.jpg") print(reader.json()) except Exception as e: @@ -439,10 +441,10 @@ except Exception as e: ## Working with resources -C2PA manifest data is not just JSON. A manifest store also contains binary resources (thumbnails, ingredient data, and other embedded files) that are referenced from the JSON metadata by JUMBF URIs. When `reader.json()` is called, the JSON includes URI references (like `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`) that point to these binary resources. To retrieve the actual binary data, use `reader.resource_to_stream()` with the URI from the JSON. This separation keeps the JSON lightweight while allowing manifests to carry rich binary content alongside the metadata. - _Resources_ are binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. +C2PA manifest data is not just JSON. A manifest store also contains binary resources (thumbnails, ingredient data, and other embedded files) that are referenced from the JSON metadata by JUMBF URIs. When `reader.json()` is called, the JSON includes URI references (like `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`) that point to these binary resources. To retrieve the actual binary data, use `reader.resource_to_stream()` with the URI from the JSON. This separation keeps the JSON lightweight while allowing manifests to carry rich binary content alongside the metadata. + ### Understanding resource identifiers When you add a resource to a working store (Builder), you assign it an identifier string. When the manifest store is created during signing, the SDK automatically converts this to a proper JUMBF URI. @@ -506,15 +508,13 @@ with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: ### Why ingredients matter -Ingredients are how C2PA tracks the history of content through edits, compositions, and transformations. Adding an ingredient to a manifest creates a verifiable link from the current asset back to its source material. This builds a **provenance chain**: original photo, then edited version, then composite, then published asset. Each link in the chain carries its own signed manifest, so anyone inspecting the final asset can trace its full history and verify that each step was authentic. +Ingredients are how C2PA tracks the history of content through edits, compositions, and transformations to build a content provenance chain represented by the manifest store. Adding an ingredient to a manifest creates a verifiable link from the current asset back to its source material. This builds a **provenance chain**: original photo, then edited version, then composite, then published asset, etc. -The `relationship` field describes _how_ the source was used: `"parentOf"` for a direct edit, `"componentOf"` for an element composited into a larger work, or `"inputTo"` for a general input. This lets verifiers understand not just _what_ the sources were, but how they contributed to the final asset. +The `relationship` field describes how the source (ingredient) was used: `"parentOf"` for a direct edit, `"componentOf"` for an element composited into a larger work, or `"inputTo"` for a general input. This lets verifiers understand not just what the sources were, but how they contributed to the final asset. ### Overview -Ingredients represent source materials used to create an asset, preserving the provenance chain. Ingredients themselves can be turned into ingredient archives (`.c2pa`). - -An ingredient archive is a serialized `Builder` with _exactly one_ ingredient. Once archived with only one ingredient, the Builder archive is an ingredient archive. Such ingredient archives can be used as ingredient in other working stores. +Ingredients represent source materials used to create an asset, preserving the provenance chain. Ingredients themselves can be turned into ingredient archives (`.c2pa`). An ingredient archive is a serialized `Builder` with _exactly one and only one_ ingredient. Once archived with only one ingredient, the Builder archive is an ingredient archive. Such ingredient archives can be used as ingredient in other working stores, as an ingredient archive can be added back directly to a working store (no un-archiving of the ingredient needed, use `application/c2pa` format when adding an ingredient archive to a Builder instance). ### Adding ingredients to a working store @@ -601,11 +601,11 @@ A Builder containing **only one ingredient and only the ingredient data** (no ot ### Restoring a working store from archive -There are two ways to load a working store from an archive. They differ in whether the builder's context (settings) is preserved. +There are two ways to load a working store from an archive. They differ in whether the builder's current context (settings) is preserved or not. -#### `with_archive()` — recommended +#### `with_archive()` -Use `with_archive()` when you need the restored builder to use specific settings (thumbnail configuration, claim generator info, intent, verification options, and so on). Create a `Builder` with a `Context` first, then call `with_archive()` to load the archived manifest definition into it. The archive replaces only the manifest definition; the builder's context and settings are preserved. +Use `with_archive()` when you need the restored builder to use specific settings that you put on the Builder on instanciation by using a context as parameter of the Builder constructor. Create a `Builder` with a `Context` first, then call `with_archive()` to load the archived manifest definition into it. The archive replaces only the manifest definition; the builder's context and settings are preserved. ```py # Create context with custom settings @@ -628,11 +628,11 @@ with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: ``` > [!IMPORTANT] -> `with_archive()` replaces the builder's manifest definition with the one from the archive. Any definition passed to `Builder()` is discarded. An empty dict `{}` is idiomatic for the initial definition when you plan to load an archive immediately after. +> `with_archive()` replaces the builder's manifest definition with the one from the archive. Any definition passed to `Builder()` on instanciation is discarded. An empty dict `{}` is idiomatic for the initial definition when you plan to load an archive immediately after. -#### `from_archive()` — context-free +#### `from_archive()` (legacy) -Use `from_archive()` for quick one-off operations where you don't need custom settings. It creates a **context-free** builder: no `Context` is attached, so all settings revert to SDK defaults (thumbnails enabled at 1024px, verification enabled, and so on). +Use `from_archive()` for quick one-off operations where you don't need custom settings. It creates a **context-free** builder: no `Context` is attached, so all settings revert to SDK defaults. ```py # Restore from stream — no context, SDK defaults apply @@ -645,7 +645,7 @@ with open("asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: ``` > [!WARNING] -> `from_archive()` does not accept a `context` parameter. Any settings that were active when the archive was created are **not** stored in the archive and are lost. For example, if the original builder had thumbnails disabled via a `Context`, the builder returned by `from_archive()` will generate thumbnails using SDK defaults. Use `with_archive()` instead when you need to preserve settings. +> `from_archive()` does not accept a `context` parameter. Any settings that were active when the archive was created are not stored in the archive and are therefore lost. For example, if the original builder had thumbnails disabled via a `Context`, the builder returned by `from_archive()` will generate thumbnails using SDK defaults. Use `with_archive()` instead when you need to preserve settings on the Builder instance you are loading an archive into. #### Choosing between `with_archive()` and `from_archive()` @@ -661,6 +661,8 @@ with open("asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: #### Phase 1: Prepare manifest +This step prepares the manifest on a Builder, and archives it into a Builder archive for later reuse. + ```py import io import json @@ -670,7 +672,6 @@ manifest_json = json.dumps({ "assertions": [] }) -builder = Builder(manifest_json) with open("thumb.jpg", "rb") as thumb: builder.add_resource("thumbnail", thumb) with open("sketch.png", "rb") as sketch: @@ -701,6 +702,8 @@ print("Asset signed with manifest store") #### Phase 2 alternative: Sign with context +In this step, after reloading the working store into a Builder instance configured with a context, settings on the Builder context can configure signing settings (e.g. thumbnails on/off). + ```py # Restore the working store with context settings preserved ctx = Context.from_dict({ @@ -724,6 +727,8 @@ By default, manifest stores are **embedded** directly into the asset file. You c ```py builder = Builder(manifest_json) +# A builder object in this case can also be created +# using an additional Context parameter for settings propagation # Default behavior: manifest store is embedded in the output with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: @@ -739,6 +744,9 @@ Prevent embedding the manifest store in the asset: ```py builder = Builder(manifest_json) +# A builder object in this case can also be created +# using an additional Context parameter for settings propagation + builder.set_no_embed() # Don't embed the manifest store # Sign: manifest store is NOT embedded, manifest bytes are returned @@ -759,6 +767,9 @@ Reference a manifest store stored at a remote URL: ```py builder = Builder(manifest_json) +# A builder object in this case can also be created +# using an additional Context parameter for settings propagation + builder.set_remote_url("https://example.com/manifests/") # The asset will contain a reference to the remote manifest store @@ -770,7 +781,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: ### Use Context for configuration -Always use `Context` objects for SDK configuration: +Use `Context` objects for SDK configuration: ```py ctx = Context.from_dict({ @@ -788,7 +799,7 @@ reader = Reader("asset.jpg", context=ctx) ### Use ingredients to build provenance chains -Add ingredients to your manifests to maintain a clear provenance chain: +Add ingredients to your manifests to maintain a provenance chain: ```py ingredient_json = json.dumps({ From 75aa399be6e3b59320f1ffd5033e4f07b308d4ae Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:28:54 -0800 Subject: [PATCH 12/18] fix: Docs --- docs/{tmn-wip-docs => }/intents.md.md | 31 +++++++++++---------------- 1 file changed, 12 insertions(+), 19 deletions(-) rename docs/{tmn-wip-docs => }/intents.md.md (92%) diff --git a/docs/tmn-wip-docs/intents.md.md b/docs/intents.md.md similarity index 92% rename from docs/tmn-wip-docs/intents.md.md rename to docs/intents.md.md index 5b331d9f..6a150d59 100644 --- a/docs/tmn-wip-docs/intents.md.md +++ b/docs/intents.md.md @@ -39,7 +39,7 @@ with Builder({}) as builder: builder.sign(signer, "image/jpeg", source, dest) ``` -Both ways of writing the code produce the same signed manifest. With intents the Builder validates the setup and fills in the spec-required structure. +Both ways of writing the code produce the same signed manifest. With intents, the Builder validates the setup and fills in the spec-required structure. ## Setting the intent @@ -47,7 +47,7 @@ There are three ways to set the intent on a `Builder` object instance. The inten ### Using Context -Pass the intent through a `Context` object when creating the `Builder`. This is the an approach that keeps intent configuration alongside other builder settings such as `claim_generator_info` and `thumbnail`. Note that the context is created from settings, so you can modulate the settings for each context. +Pass the intent through a `Context` object when creating the `Builder`. This is an approach that keeps intent configuration alongside other builder settings such as `claim_generator_info` and `thumbnail`. Note that the context is created from settings, so you can modulate the settings for each context. ```py from c2pa import Context, Builder @@ -93,16 +93,14 @@ with Builder({}) as builder: builder.sign(signer, "image/jpeg", source, dest) ``` -If both a `Context` intent and a `set_intent` call are present, the `set_intent` call takes precedence. - ### Using `load_settings` (deprecated) -The global `load_settings` function can configure the intent for all subsequent `Builder` instances. This approach is deprecated in favor of context-based APIs: +The legacy `load_settings` function can configure the intent for all subsequent `Builder` instances (thread-local configuration). This approach is deprecated in favor of context-based APIs: ```py from c2pa import load_settings, Builder -# Deprecated: sets intent globally +# Deprecated: sets intent settings per thread load_settings({"builder": {"intent": "edit"}}) with Builder({}) as builder: @@ -112,7 +110,7 @@ with Builder({}) as builder: ### Intent setting precedence -When an intent is configured in multiple places, the most specific setting wins: +When an intent is configured in multiple places , the most specific setting wins: ```mermaid flowchart TD @@ -131,9 +129,11 @@ flowchart TD manually in manifest JSON."] ``` +Notably, if a `set_intent` call is present on the Builder, the `set_intent` call takes precedence. + ## How intents relate to the source stream -The intent **operates on the source stream** passed to `sign()`. It does not target a specific ingredient added via `add_ingredient`: it targets the source asset itself (and ONLY the source). +The intent **operates on the source** passed to `sign()`. It does not target a specific ingredient added via `add_ingredient`: it targets the source asset itself (and ONLY the source). The following diagram shows what happens at sign time for each intent: @@ -207,15 +207,10 @@ flowchart TD ## Import +Intents and digital source types are provided as enums by two imports. + ```py from c2pa import ( - Builder, - Reader, - Signer, - Context, - Settings, - C2paSignerInfo, - C2paSigningAlg, C2paBuilderIntent, C2paDigitalSourceType, ) @@ -301,7 +296,7 @@ with Builder({}, context=ctx) as builder: ctx = Context.from_dict({ "builder": { "intent": {"Create": "digitalCapture"}, - "claim_generator_info": {"name": "my_app", "version": "1.0.0"}, + "claim_generator_info": {"name": "an_app", "version": "0.1.0"}, } }) @@ -462,7 +457,7 @@ with Builder({}) as builder: ## Intent values in settings -When configuring the intent settings, the intent is specified as a string or object in the `builder.intent` field: +When configuring the intent settings, the intent is specified as a string or object in the `builder.intent` field, matching the C2PA SDK settings JSON schema: | Intent | Settings value | With digital source type | |--------|---------------|--------------------------| @@ -470,8 +465,6 @@ When configuring the intent settings, the intent is specified as a string or obj | Edit | `"edit"` | Not applicable | | Update | `"update"` | Not applicable | -Available digital source type values for Create: `"digitalCapture"`, `"digitalCreation"`, `"trainedAlgorithmicMedia"`, `"compositeSynthetic"`, `"screenCapture"`, `"algorithmicMedia"`, and others. - ## API reference ### `Builder.set_intent(intent, digital_source_type=C2paDigitalSourceType.EMPTY)` From c8040e986d6fae61f7a979abe052a3d9213e886b Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:37:04 -0800 Subject: [PATCH 13/18] fix: Docs --- docs/tmn-wip-docs/selective-manifests.md | 74 +++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/tmn-wip-docs/selective-manifests.md b/docs/tmn-wip-docs/selective-manifests.md index b7131a33..8e988c92 100644 --- a/docs/tmn-wip-docs/selective-manifests.md +++ b/docs/tmn-wip-docs/selective-manifests.md @@ -70,6 +70,9 @@ with open("thumbnail.jpg", "wb") as f: ## Filtering into a new Builder +> [!NOTE] +> All `Builder` examples on this page also work with a `Context` for custom settings (thumbnails, claim generator, intent). A context can be passed to the constructor with `Builder(manifest_json, context=ctx)`. For signing with a context-provided signer, `builder.sign_with_context()` can be used instead of `builder.sign()`. See [Context](../context.md) for details. + Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. When transferring ingredients from a `Reader` to a new `Builder`, you must transfer both the JSON metadata and the associated binary resources (thumbnails, manifest data). The JSON contains identifiers that reference those resources; the same identifiers must be used when calling `builder.add_resource()`. @@ -454,6 +457,7 @@ flowchart TD A2["Archive: graphics.c2pa (ingredients from design assets)"] A3["Archive: audio.c2pa (ingredients from audio tracks)"] end + CTX["Context (optional)"] subgraph Build["Final Builder"] direction TB SEL["Pick and choose ingredients from any archive in the catalog"] @@ -462,11 +466,13 @@ flowchart TD A1 -->|"select photo_1, photo_3"| SEL A2 -->|"select logo"| SEL A3 -. "skip (not needed)" .-> X((not used)) + CTX -.->|"settings"| FB SEL --> FB FB -->|sign| OUT[Signed Output Asset] style A3 fill:#eee,stroke:#999 style X fill:#f99,stroke:#c00 + style CTX fill:#e8f4fd,stroke:#4a90d9 ``` @@ -494,6 +500,38 @@ with Reader("application/c2pa", archive_stream) as reader: new_builder.sign(signer, "image/jpeg", source, dest) ``` +#### With context + +The same workflow can use a `Context` for custom settings. The context controls thumbnail generation, claim generator info, and other Builder settings. When a signer is configured in the context, `sign_with_context()` can be used instead of passing a signer explicitly. + +```py +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "My App", "version": "1.0"} + } +}) + +archive_stream.seek(0) +with Reader("application/c2pa", archive_stream) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + selected = [ + ing for ing in active["ingredients"] + if ing["title"] in {"photo_1.jpg", "logo.png"} + ] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": selected, + }, context=ctx) as new_builder: + transfer_ingredient_resources(reader, new_builder, selected) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + new_builder.sign_with_context("image/jpeg", source, dest) +``` + ### Overriding ingredient properties When adding an ingredient from an archive or from a file, the JSON passed to `add_ingredient()` can override properties like `title` and `relationship`. This is useful when reusing archived ingredients in a different context: @@ -586,10 +624,14 @@ flowchart TD AR -->|"Reader(application/c2pa)"| RD[JSON + resources] RD -->|"pick ingredients"| SEL[Selected ingredients] end + CTX["Context (optional)"] subgraph Step3["Step 3: Reuse in a new Builder"] SEL -->|"new Builder + add_resource()"| B2[New Builder] + CTX -.->|"settings"| B2 B2 -->|sign| OUT[Signed Output] end + + style CTX fill:#e8f4fd,stroke:#4a90d9 ``` @@ -620,6 +662,9 @@ with Builder({ builder.to_archive(archive_stream) ``` +> [!NOTE] +> When restoring from an archive, `with_archive()` preserves context settings while `from_archive()` does not. See [Working with archives](../working-stores.md#working-with-archives) for the full comparison. + **Step 2:** Read the archive and extract ingredients: ```py @@ -646,9 +691,36 @@ with Reader("application/c2pa", archive_stream) as reader: new_builder.sign(signer, "image/jpeg", source, dest) ``` +#### Step 3 with context + +When context settings need to be applied (such as thumbnail configuration or claim generator info), the builder can be created with a `Context`. If the archive was previously saved with `to_archive()`, it can be loaded with `with_archive()` to preserve those settings. + +```py + ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "My App", "version": "1.0"} + } + }) + + selected = [ing for ing in ingredients if ing["title"] == "A.jpg"] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": selected, + }, context=ctx) as new_builder: + transfer_ingredient_resources(reader, new_builder, selected) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + new_builder.sign_with_context("image/jpeg", source, dest) +``` + ### Merging multiple working stores -In some cases you may need to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**—the recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated. +> [!NOTE] +> The `Builder` construction and signing in the merge workflow also support `Context`. The caller can pass `context=ctx` to `Builder()` and use `sign_with_context()` instead of `sign()`. See [Context](../context.md) for details. + +In some cases it is necessary to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**. The recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated. When merging from multiple sources, resource identifier URIs can collide. Rename identifiers with a unique suffix when needed. Use two passes: (1) collect ingredients with collision handling, build the manifest, create the builder; (2) re-read each archive and transfer resources (use original ID for `resource_to_stream()`, renamed ID for `add_resource()` when collisions occurred). From a10bb6555d0cc6eb07af210f6252239febf137a9 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:41:37 -0800 Subject: [PATCH 14/18] fix: Docs --- docs/tmn-wip-docs/selective-manifests.md | 30 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/tmn-wip-docs/selective-manifests.md b/docs/tmn-wip-docs/selective-manifests.md index 8e988c92..5e422f54 100644 --- a/docs/tmn-wip-docs/selective-manifests.md +++ b/docs/tmn-wip-docs/selective-manifests.md @@ -38,7 +38,10 @@ The fundamental workflow is: Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The source asset is never modified. +`Reader` also accepts an optional `context` parameter. This is especially important for trust configuration, which controls which certificates are trusted when validating signatures. Without a context, `Reader` uses SDK defaults. See [Configuring Reader](../context.md#configuring-reader) and [Trust configuration](../context.md#trust-configuration) for details. + ```py +# Without context (SDK default trust settings) with open("signed_asset.jpg", "rb") as source: with Reader("image/jpeg", source) as reader: # Get the full manifest store as JSON @@ -54,6 +57,27 @@ with open("signed_asset.jpg", "rb") as source: thumbnail_id = manifest["thumbnail"]["identifier"] ``` +### With context (trust configuration) + +To control which certificates are trusted during validation, pass a `Context` with trust settings to `Reader`: + +```py +ctx = Context.from_dict({ + "trust": { + "user_anchors": "-----BEGIN CERTIFICATE-----\nMIICEzCCA...\n-----END CERTIFICATE-----", + }, + "verify": { + "verify_trust": True + } +}) + +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active_label = manifest_store["active_manifest"] + manifest = manifest_store["manifests"][active_label] +``` + ### Extracting binary resources The JSON returned by `reader.json()` only contains string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. Extract the actual binary content by using `resource_to_stream()`: @@ -71,7 +95,7 @@ with open("thumbnail.jpg", "wb") as f: ## Filtering into a new Builder > [!NOTE] -> All `Builder` examples on this page also work with a `Context` for custom settings (thumbnails, claim generator, intent). A context can be passed to the constructor with `Builder(manifest_json, context=ctx)`. For signing with a context-provided signer, `builder.sign_with_context()` can be used instead of `builder.sign()`. See [Context](../context.md) for details. +> All `Builder` and `Reader` examples on this page also work with a `Context`. For `Reader`, a context provides trust configuration and verification settings: `Reader(format, source, context=ctx)`. For `Builder`, a context provides custom settings (thumbnails, claim generator, intent): `Builder(manifest_json, context=ctx)`. For signing with a context-provided signer, `builder.sign_with_context()` can be used instead of `builder.sign()`. See [Context](../context.md) for details. Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. @@ -440,7 +464,7 @@ There are two distinct types of archives, sharing the same binary format but bei ### Builder archives vs. ingredient archives -A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder.from_archive()`. +A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder.from_archive()` to create a new builder instance from an archive, or `builder.with_archive()` to load a working store from a builder archive into an existing builder instance. An **ingredient archive** contains the manifest store from an asset that was added as an ingredient. @@ -457,7 +481,7 @@ flowchart TD A2["Archive: graphics.c2pa (ingredients from design assets)"] A3["Archive: audio.c2pa (ingredients from audio tracks)"] end - CTX["Context (optional)"] + CTX["Context (to propagate settings and configuration)"] subgraph Build["Final Builder"] direction TB SEL["Pick and choose ingredients from any archive in the catalog"] From df7d75c38326040ef6fcede8d7d4bb11caf67334 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 6 Mar 2026 20:44:55 -0800 Subject: [PATCH 15/18] fix: Docs --- docs/{tmn-wip-docs => }/selective-manifests.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{tmn-wip-docs => }/selective-manifests.md (100%) diff --git a/docs/tmn-wip-docs/selective-manifests.md b/docs/selective-manifests.md similarity index 100% rename from docs/tmn-wip-docs/selective-manifests.md rename to docs/selective-manifests.md From 5c0063949491f3616dc737c0edea4ea79d59ab54 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Sat, 7 Mar 2026 20:19:40 -0800 Subject: [PATCH 16/18] fix: Docs --- docs/context.md | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/context.md b/docs/context.md index f05ee70d..7e9d9b88 100644 --- a/docs/context.md +++ b/docs/context.md @@ -187,11 +187,14 @@ Create new C2PA provenance data and sign it into an asset: ```mermaid flowchart LR A["Settings"] --> B["Context"] + F[Signer] --> B B --> C[Builder] - C --> D["sign()"] + G["add assertions
add ingredients"] --> C + C --> D["sign_with_context()"] D --> E[Signed asset] - F[Signer] --> D - G[add assertions\nadd ingredients] --> C + F2[Signer] -.-> D2["sign()"] + C --> D2 + D2 --> E ``` ```py @@ -216,7 +219,7 @@ There are several ways to create a `Context`, depending on your needs: ### Using SDK default settings -The simplest approach is using [SDK default settings](settings.md#default-configuration). +Without additional parameters, a default context is using [SDK default settings](settings.md#default-configuration). **When to use:** For quick prototyping, or when you're happy with SDK default behavior (verification enabled, thumbnails enabled at 1024px, and so on). @@ -451,7 +454,7 @@ For more information, see [Settings - Offline or air-gapped environments](settin ### Context and archives -Archives (`.c2pa` files) store only the manifest definition — they do **not** store settings or context. This means: +Archives (`.c2pa` files) store only the manifest definition. They do **not** store settings or context. This means: - **`Builder.from_archive(stream)`** creates a context-free builder. All settings revert to SDK defaults regardless of what context the original builder had. - **`Builder({}, ctx).with_archive(stream)`** creates a builder with a context first, then loads the archived manifest definition into it. The context settings are preserved and propagated to this Builder instance. @@ -533,8 +536,8 @@ C2PA uses a certificate-based trust model to prove who signed an asset. When cre A Signer can be configured two ways: -- [From Settings (signer-on-context)](#from-settings) — pass the signer when creating the `Context`. -- [Explicit signer passed to sign()](#explicit-programmatic-signer) — pass the signer directly at signing time. +- [From Settings (signer-on-context)](#from-settings): pass the signer when creating the `Context`. +- [Explicit signer passed to sign()](#explicit-programmatic-signer): pass the signer directly at signing time. ### From Settings @@ -545,10 +548,7 @@ from c2pa import Context, Settings, Builder, Signer, C2paSignerInfo, C2paSigning # Create a signer signer_info = C2paSignerInfo( - alg=C2paSigningAlg.ES256, - sign_cert=cert_data, - private_key=key_data, - ta_url=b"http://timestamp.digicert.com" + C2paSigningAlg.ES256, cert_data, key_data, b"http://timestamp.digicert.com" ) signer = Signer.from_info(signer_info) @@ -617,6 +617,16 @@ reader = Reader("image.jpg", context=ctx) # Context can be closed after construction; readers/builders still work ``` +Using the `with` statement for automatic cleanup: + +```py +with Context(settings) as ctx: + builder1 = Builder(manifest1, ctx) + builder2 = Builder(manifest2, ctx) + reader = Reader("image.jpg", context=ctx) +# Resources are automatically released +``` + ### Multiple contexts for different purposes Use different `Context` objects when you need different settings. Ror example, for development vs. production, or different trust configurations: @@ -673,6 +683,6 @@ reader = Reader("image.jpg", context=ctx) ## See also -- [Using settings](settings.md) — schema, property reference, and examples. -- [Usage](usage.md) — reading and signing with Reader and Builder. +- [Using settings](settings.md): schema, property reference, and examples. +- [Usage](usage.md): reading and signing with Reader and Builder. - [CAI settings schema](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/): full schema reference. From 36a6441c6847c70cec216f0f1667c3f9247a52d7 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Sun, 8 Mar 2026 17:08:05 -0700 Subject: [PATCH 17/18] fix: Wording and examples --- docs/context.md | 12 +- docs/{intents.md.md => intents.md} | 45 ++--- docs/selective-manifests.md | 22 +- docs/settings.md | 4 +- docs/working-stores.md | 10 +- tests/test_unit_tests_threaded.py | 309 ----------------------------- 6 files changed, 41 insertions(+), 361 deletions(-) rename docs/{intents.md.md => intents.md} (82%) diff --git a/docs/context.md b/docs/context.md index 7e9d9b88..fa36c615 100644 --- a/docs/context.md +++ b/docs/context.md @@ -79,7 +79,7 @@ classDiagram +to_archive(stream) +with_archive(stream) Builder +sign(signer, format, source, dest) bytes - +sign_with_context(format, source, dest) bytes + +sign(format, source, dest) bytes +sign_file(source_path, dest_path, signer) bytes +close() } @@ -190,11 +190,9 @@ flowchart LR F[Signer] --> B B --> C[Builder] G["add assertions
add ingredients"] --> C - C --> D["sign_with_context()"] + C --> D["sign()"] D --> E[Signed asset] - F2[Signer] -.-> D2["sign()"] - C --> D2 - D2 --> E + F2[Signer] -.-> D ``` ```py @@ -466,7 +464,7 @@ Use `with_archive()` when your workflow depends on specific settings (thumbnails ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, - "claim_generator_info": {"name": "My App", "version": "1.0"} + "claim_generator_info": {"name": "My App", "version": "0.1.0"} } }) @@ -559,7 +557,7 @@ ctx = Context(settings, signer) # Build and sign, no signer argument needed since a Signer is in the Context builder = Builder(manifest_json, ctx) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: - builder.sign_with_context("image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` ### Explicit (programmatic) signer diff --git a/docs/intents.md.md b/docs/intents.md similarity index 82% rename from docs/intents.md.md rename to docs/intents.md index 6a150d59..9fb90be3 100644 --- a/docs/intents.md.md +++ b/docs/intents.md @@ -4,7 +4,7 @@ Intents enable validation, add required default actions, and help prevent invali ## Why use intents? -Without intents, the caller must manually construct the correct manifest structure to be compliant with the specification: adding the required actions (`c2pa.created` or `c2pa.opened` as first action as per specification), setting digital source types, managing ingredients, and linking actions to ingredients. Getting any of this wrong produces a manifest that does not comply with the C2PA specification. +Without intents, the caller must manually construct the correct manifest structure: adding the required actions (`c2pa.created` or `c2pa.opened` as the first action per the specification), setting digital source types, managing ingredients, and linking actions to ingredients. Getting any of this wrong produces a non-compliant manifest. With intents, the caller declares *what is being done* and the Builder handles the rest: @@ -43,11 +43,11 @@ Both ways of writing the code produce the same signed manifest. With intents, th ## Setting the intent -There are three ways to set the intent on a `Builder` object instance. The intent determines which actions the Builder auto-generates at sign time. +There are three ways to set the intent on a `Builder` object instance. ### Using Context -Pass the intent through a `Context` object when creating the `Builder`. This is an approach that keeps intent configuration alongside other builder settings such as `claim_generator_info` and `thumbnail`. Note that the context is created from settings, so you can modulate the settings for each context. +Pass the intent through a `Context` object when creating the `Builder`. This keeps intent configuration alongside other builder settings such as `claim_generator_info` and `thumbnail`. ```py from c2pa import Context, Builder @@ -55,7 +55,7 @@ from c2pa import Context, Builder ctx = Context.from_dict({ "builder": { "intent": {"Create": "digitalCapture"}, - "claim_generator_info": {"name": "My App", "version": "1.0.0"}, + "claim_generator_info": {"name": "My App", "version": "0.1.0"}, } }) @@ -129,11 +129,11 @@ flowchart TD manually in manifest JSON."] ``` -Notably, if a `set_intent` call is present on the Builder, the `set_intent` call takes precedence. +If a `set_intent` call is present on the Builder, it takes precedence over all other sources. ## How intents relate to the source stream -The intent **operates on the source** passed to `sign()`. It does not target a specific ingredient added via `add_ingredient`: it targets the source asset itself (and ONLY the source). +The intent operates on the source passed to `sign()`, not on any ingredient added via `add_ingredient`. The following diagram shows what happens at sign time for each intent: @@ -176,7 +176,7 @@ For **EDIT** and **UPDATE**, the Builder looks at the source stream, and if no ` ### How intent relates to `add_ingredient` -The intent and manually-added ingredients serve different roles. **The intent controls what the Builder does with the source stream** at sign time. The `add_ingredient` method **adds other ingredients explicitly**. +The intent controls what the Builder does with the source stream at sign time. The `add_ingredient` method adds other ingredients explicitly. These are separate concerns. ```mermaid flowchart TD @@ -244,10 +244,10 @@ flowchart TD ## CREATE intent -Use `CREATE` when the asset is a brand-new creation with no prior history. In this case, a `C2paDigitalSourceType` is required (by the specification) to describe how the asset was produced. The Builder will: +Use `CREATE` when the asset has no prior history. A `C2paDigitalSourceType` is required to describe how the asset was produced. The Builder will: - Add a `c2pa.created` action with the specified digital source type. -- Reject the operation if a `parentOf` ingredient exists (new creations cannot have parents). +- Reject the operation if a `parentOf` ingredient exists. ### Example: New digital creation @@ -290,7 +290,7 @@ with Builder({}, context=ctx) as builder: ### Example: CREATE with additional manifest metadata -`Context` and a manifest definition can be combined. The context handles the intent, while the manifest definition provides additional metadata and assertions: +A `Context` and a manifest definition can be combined. The context handles the intent; the manifest definition provides additional metadata and assertions: ```py ctx = Context.from_dict({ @@ -324,15 +324,13 @@ with Builder(manifest_def, context=ctx) as builder: Use `EDIT` when modifying an existing asset. The Builder will: -1. Check if a `parentOf` ingredient has already been added. If not, it **automatically creates one from the source stream** passed to `sign()`. +1. Check if a `parentOf` ingredient has already been added. If not, it automatically creates one from the source stream passed to `sign()`. 2. Add a `c2pa.opened` action linked to the parent ingredient. No `digital_source_type` parameter is needed. ### Example: Editing an asset -In this case, the source stream becomes the parent ingredient. - Using `Context`: ```py @@ -355,16 +353,11 @@ with Builder({}) as builder: builder.sign(signer, "image/jpeg", source, dest) ``` -The resulting manifest contains: - -- One ingredient with `relationship: "parentOf"` pointing to `original.jpg`. -- A `c2pa.opened` action referencing that ingredient. - -If the source file already has a C2PA manifest, the ingredient preserves the full provenance chain. +The resulting manifest contains one ingredient with `relationship: "parentOf"` pointing to `original.jpg` and a `c2pa.opened` action referencing that ingredient. If the source file already has a C2PA manifest, the ingredient preserves the full provenance chain. ### Example: Editing with a manually-added parent -To control some the parent ingredient's metadata (for example, to set a title or use a different source), add it explicitly. The Builder will then use that ingredient: +To control the parent ingredient's metadata (for example, to set a title or use a different source), add it explicitly: ```py ctx = Context.from_dict({"builder": {"intent": "edit"}}) @@ -383,7 +376,7 @@ with Builder({}, context=ctx) as builder: ### Example: Editing with additional component ingredients -A parent ingredient can be combined with component or input ingredients. The intent creates the `c2pa.opened` action for the parent, and additional actions can be added as components (componentOf)/input (inputTo): +A parent ingredient can be combined with component or input ingredients. The intent creates the `c2pa.opened` action for the parent; additional actions can reference components (`componentOf`) or inputs (`inputTo`): ```py ctx = Context.from_dict({"builder": {"intent": "edit"}}) @@ -425,13 +418,13 @@ with Builder({ ## UPDATE intent -Use `UPDATE` for non-editorial changes where the asset content itself is not modified, for example adding or changing metadata. This is a limited form of `EDIT`: +Use `UPDATE` for metadata-only changes where the asset content itself is not modified. This is a restricted form of `EDIT`: - Allows exactly one ingredient (only the parent). - Does not allow changes to the parent's hashed content. - Produces a more compact manifest than `EDIT`. -Like for the `EDIT` intent, the Builder auto-creates a parent ingredient from the source stream if one is not provided. +As with `EDIT`, the Builder auto-creates a parent ingredient from the source stream if one is not provided. ### Example: Adding metadata to a signed asset @@ -457,7 +450,7 @@ with Builder({}) as builder: ## Intent values in settings -When configuring the intent settings, the intent is specified as a string or object in the `builder.intent` field, matching the C2PA SDK settings JSON schema: +When configuring settings, the intent is specified as a string or object in the `builder.intent` field: | Intent | Settings value | With digital source type | |--------|---------------|--------------------------| @@ -469,11 +462,9 @@ When configuring the intent settings, the intent is specified as a string or obj ### `Builder.set_intent(intent, digital_source_type=C2paDigitalSourceType.EMPTY)` -**Parameters:** - | Parameter | Type | Description | |-----------|------|-------------| | `intent` | `C2paBuilderIntent` | The intent: `CREATE`, `EDIT`, or `UPDATE`. | | `digital_source_type` | `C2paDigitalSourceType` | Required for `CREATE`. Describes how the asset was made. Defaults to `EMPTY`. | -**Raises:** `C2paError` if the intent cannot be set (e.g., a `parentOf` ingredient exists with `CREATE`, or no parent can be found for `EDIT`/`UPDATE`). +Raises `C2paError` if the intent cannot be set (for example, a `parentOf` ingredient exists with `CREATE`). diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 5e422f54..7971e27a 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -95,7 +95,7 @@ with open("thumbnail.jpg", "wb") as f: ## Filtering into a new Builder > [!NOTE] -> All `Builder` and `Reader` examples on this page also work with a `Context`. For `Reader`, a context provides trust configuration and verification settings: `Reader(format, source, context=ctx)`. For `Builder`, a context provides custom settings (thumbnails, claim generator, intent): `Builder(manifest_json, context=ctx)`. For signing with a context-provided signer, `builder.sign_with_context()` can be used instead of `builder.sign()`. See [Context](../context.md) for details. +> All `Builder` and `Reader` examples on this page also work with a `Context`. For `Reader`, a context provides trust configuration and verification settings: `Reader(format, source, context=ctx)`. For `Builder`, a context provides custom settings (thumbnails, claim generator, intent): `Builder(manifest_json, context=ctx)`. When a signer is configured in the context, `builder.sign()` can be called without an signer instance as argument. See [Context](../context.md) for details. Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. @@ -263,7 +263,7 @@ The `label` field on an ingredient is the **primary** linking key. Set a `label` ```py manifest_json = { - "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [ { "label": "c2pa.actions.v2", @@ -312,7 +312,7 @@ When linking multiple ingredients, each ingredient needs a unique label. ```py manifest_json = { - "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [ { "label": "c2pa.actions.v2", @@ -377,7 +377,7 @@ When no `label` is set on an ingredient, the SDK matches `ingredientIds` against instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f" manifest_json = { - "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [ { "label": "c2pa.actions", @@ -526,13 +526,13 @@ with Reader("application/c2pa", archive_stream) as reader: #### With context -The same workflow can use a `Context` for custom settings. The context controls thumbnail generation, claim generator info, and other Builder settings. When a signer is configured in the context, `sign_with_context()` can be used instead of passing a signer explicitly. +The same workflow can use a `Context` for custom settings. The context controls thumbnail generation, claim generator info, and other Builder settings. When a signer is configured in the context, `sign()` can be called without an explicit signer instance passed in as argument. ```py ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, - "claim_generator_info": {"name": "My App", "version": "1.0"} + "claim_generator_info": {"name": "an-application", "version": "0.1.0"} } }) @@ -553,7 +553,7 @@ with Reader("application/c2pa", archive_stream) as reader: transfer_ingredient_resources(reader, new_builder, selected) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - new_builder.sign_with_context("image/jpeg", source, dest) + new_builder.sign("image/jpeg", source, dest) ``` ### Overriding ingredient properties @@ -581,7 +581,7 @@ The C2PA specification allows **vendor-namespaced parameters** on actions using ```py manifest_json = { - "claim_generator_info": [{"name": "an-application", "version": "1.0"}], + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [ { "label": "c2pa.actions.v2", @@ -723,7 +723,7 @@ When context settings need to be applied (such as thumbnail configuration or cla ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, - "claim_generator_info": {"name": "My App", "version": "1.0"} + "claim_generator_info": {"name": "an-application", "version": "0.1.0"} } }) @@ -736,13 +736,13 @@ When context settings need to be applied (such as thumbnail configuration or cla transfer_ingredient_resources(reader, new_builder, selected) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - new_builder.sign_with_context("image/jpeg", source, dest) + new_builder.sign("image/jpeg", source, dest) ``` ### Merging multiple working stores > [!NOTE] -> The `Builder` construction and signing in the merge workflow also support `Context`. The caller can pass `context=ctx` to `Builder()` and use `sign_with_context()` instead of `sign()`. See [Context](../context.md) for details. +> The `Builder` construction and signing in the merge workflow also support `Context`. The caller can pass `context=ctx` to `Builder()` and call `sign()` without a signer argument when the context has one. See [Context](../context.md) for details. In some cases it is necessary to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**. The recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated. diff --git a/docs/settings.md b/docs/settings.md index bb248c69..28bad61b 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -389,7 +389,7 @@ For example, for original digital capture (photos from camera): camera_ctx = Context.from_dict({ "builder": { "intent": {"Create": "digitalCapture"}, - "claim_generator_info": {"name": "Camera App", "version": "1.0"} + "claim_generator_info": {"name": "Camera App", "version": "0.1.0"} } }) ``` @@ -400,7 +400,7 @@ Or another example for editing existing content: editor_ctx = Context.from_dict({ "builder": { "intent": {"Edit": None}, - "claim_generator_info": {"name": "Photo Editor", "version": "2.0"} + "claim_generator_info": {"name": "Photo Editor", "version": "0.2.0"} } }) ``` diff --git a/docs/working-stores.md b/docs/working-stores.md index 1552345d..e8c0666a 100644 --- a/docs/working-stores.md +++ b/docs/working-stores.md @@ -131,7 +131,7 @@ For full details on `Context` and `Settings`, see [Using Context to configure th "manifests": { "urn:uuid:...": { "claim_generator": "MyApp/1.0", - "claim_generator_info": [{"name": "MyApp", "version": "1.0"}], + "claim_generator_info": [{"name": "MyApp", "version": "0.1.0"}], "title": "signed_image.jpg", "assertions": [ {"label": "c2pa.actions", "data": {"actions": [...]}}, @@ -323,7 +323,7 @@ except Exception as e: # The Builder must have been created with a Context that has a signer. try: with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - manifest_bytes = builder.sign_with_context("image/jpeg", src, dst) + manifest_bytes = builder.sign("image/jpeg", src, dst) print("Signed successfully!") except Exception as e: print(f"Signing failed: {e}") @@ -381,7 +381,7 @@ try: # 4. Create Builder with context and sign builder = Builder(manifest_json, context=ctx) with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign_with_context("image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) print("Asset signed with context settings") @@ -612,7 +612,7 @@ Use `with_archive()` when you need the restored builder to use specific settings ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, - "claim_generator_info": {"name": "My App", "version": "1.0"} + "claim_generator_info": {"name": "My App", "version": "0.1.0"} } }) @@ -716,7 +716,7 @@ with open("artwork_manifest.c2pa", "rb") as archive: # Sign using the context's signer with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst: - builder.sign_with_context("image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` ## Embedded vs external manifests diff --git a/tests/test_unit_tests_threaded.py b/tests/test_unit_tests_threaded.py index eab6de8d..bfb54c8f 100644 --- a/tests/test_unit_tests_threaded.py +++ b/tests/test_unit_tests_threaded.py @@ -1818,203 +1818,6 @@ async def run_async_tests(): # Verify all readers completed self.assertEqual(active_readers, 0, "Not all readers completed") - def test_builder_sign_with_multiple_ingredients_from_stream(self): - """Test Builder class operations with multiple ingredients using streams.""" - # Test creating builder from JSON - builder = Builder.from_json(self.manifestDefinition) - assert builder._handle is not None - - # Thread synchronization - add_errors = [] - add_lock = threading.Lock() - completed_threads = 0 - completion_lock = threading.Lock() - - def add_ingredient_from_stream(ingredient_json, file_path, thread_id): - nonlocal completed_threads - try: - with open(file_path, 'rb') as f: - builder.add_ingredient_from_stream( - ingredient_json, "image/jpeg", f) - with add_lock: - add_errors.append(None) # Success case - except Exception as e: - with add_lock: - add_errors.append(f"Thread {thread_id} error: {str(e)}") - finally: - with completion_lock: - completed_threads += 1 - - # Create and start two threads for parallel ingredient addition - thread1 = threading.Thread( - target=add_ingredient_from_stream, - args=('{"title": "Test Ingredient Stream 1"}', self.testPath3, 1) - ) - thread2 = threading.Thread( - target=add_ingredient_from_stream, - args=('{"title": "Test Ingredient Stream 2"}', self.testPath4, 2) - ) - - # Start both threads - thread1.start() - thread2.start() - - # Wait for both threads to complete - thread1.join() - thread2.join() - - # Check for errors during ingredient addition - if any(error for error in add_errors if error is not None): - self.fail( - "\n".join( - error for error in add_errors if error is not None)) - - # Verify both ingredients were added successfully - self.assertEqual( - completed_threads, - 2, - "Both threads should have completed") - self.assertEqual( - len(add_errors), - 2, - "Both threads should have completed without errors") - - # Now sign the manifest with the added ingredients - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - reader = Reader("image/jpeg", output) - json_data = reader.json() - manifest_data = json.loads(json_data) - - # Verify active manifest exists - self.assertIn("active_manifest", manifest_data) - active_manifest_id = manifest_data["active_manifest"] - - # Verify active manifest object exists - self.assertIn("manifests", manifest_data) - self.assertIn(active_manifest_id, manifest_data["manifests"]) - active_manifest = manifest_data["manifests"][active_manifest_id] - - # Verify ingredients array exists in active manifest - self.assertIn("ingredients", active_manifest) - self.assertIsInstance(active_manifest["ingredients"], list) - self.assertEqual(len(active_manifest["ingredients"]), 2) - - # Verify both ingredients exist in the array (order doesn't matter) - ingredient_titles = [ing["title"] - for ing in active_manifest["ingredients"]] - self.assertIn("Test Ingredient Stream 1", ingredient_titles) - self.assertIn("Test Ingredient Stream 2", ingredient_titles) - - builder.close() - - def test_builder_sign_with_same_ingredient_multiple_times(self): - """Test Builder class operations with the same ingredient added multiple times from different threads.""" - # Test creating builder from JSON - builder = Builder.from_json(self.manifestDefinition) - assert builder._handle is not None - - # Thread synchronization - add_errors = [] - add_lock = threading.Lock() - completed_threads = 0 - completion_lock = threading.Lock() - - def add_ingredient(ingredient_json, thread_id): - nonlocal completed_threads - try: - with open(self.testPath3, 'rb') as f: - builder.add_ingredient(ingredient_json, "image/jpeg", f) - with add_lock: - add_errors.append(None) # Success case - except Exception as e: - with add_lock: - add_errors.append(f"Thread {thread_id} error: {str(e)}") - finally: - with completion_lock: - completed_threads += 1 - - # Create and start 5 threads for parallel ingredient addition - threads = [] - for i in range(1, 6): - # Create unique manifest JSON for each thread - ingredient_json = json.dumps({ - "title": f"Test Ingredient Thread {i}" - }) - - thread = threading.Thread( - target=add_ingredient, - args=(ingredient_json, i) - ) - threads.append(thread) - thread.start() - - # Wait for all threads to complete - for thread in threads: - thread.join() - - # Check for errors during ingredient addition - if any(error for error in add_errors if error is not None): - self.fail( - "\n".join( - error for error in add_errors if error is not None)) - - # Verify all ingredients were added successfully - self.assertEqual( - completed_threads, - 5, - "All 5 threads should have completed") - self.assertEqual( - len(add_errors), - 5, - "All 5 threads should have completed without errors") - - # Now sign the manifest with the added ingredients - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - reader = Reader("image/jpeg", output) - json_data = reader.json() - manifest_data = json.loads(json_data) - - # Verify active manifest exists - self.assertIn("active_manifest", manifest_data) - active_manifest_id = manifest_data["active_manifest"] - - # Verify active manifest object exists - self.assertIn("manifests", manifest_data) - self.assertIn(active_manifest_id, manifest_data["manifests"]) - active_manifest = manifest_data["manifests"][active_manifest_id] - - # Verify ingredients array exists in active manifest - self.assertIn("ingredients", active_manifest) - self.assertIsInstance(active_manifest["ingredients"], list) - self.assertEqual(len(active_manifest["ingredients"]), 5) - - # Verify all ingredients exist in the array with correct thread IDs - # and unique metadata - ingredient_titles = [ing["title"] - for ing in active_manifest["ingredients"]] - - # Check that we have 5 unique titles - self.assertEqual(len(set(ingredient_titles)), 5, - "Should have 5 unique ingredient titles") - - # Verify each thread's ingredient exists with correct metadata - for i in range(1, 6): - # Find ingredients with this thread ID - thread_ingredients = [ing for ing in active_manifest["ingredients"] - if ing["title"] == f"Test Ingredient Thread {i}"] - self.assertEqual( - len(thread_ingredients), - 1, - f"Should find exactly one ingredient for thread {i}") - - builder.close() - def test_builder_sign_with_multiple_ingredient_random_many_threads(self): """Test Builder class operations with 12 threads, each adding 3 specific ingredients and signing a file.""" # Number of threads to use in the test @@ -2857,118 +2660,6 @@ async def run_async_tests(): self.fail("\n".join(read_errors)) self.assertEqual(active_readers, 0) - def test_builder_sign_with_multiple_ingredients_from_stream(self): - """Test Builder with multiple ingredients from streams using context APIs""" - ctx = Context() - builder = Builder.from_json(self.manifestDefinition, context=ctx) - assert builder._handle is not None - add_errors = [] - add_lock = threading.Lock() - completed_threads = 0 - completion_lock = threading.Lock() - - def add_ingredient_from_stream(ingredient_json, file_path, thread_id): - nonlocal completed_threads - try: - with open(file_path, 'rb') as f: - builder.add_ingredient_from_stream(ingredient_json, "image/jpeg", f) - with add_lock: - add_errors.append(None) - except Exception as e: - with add_lock: - add_errors.append(f"Thread {thread_id} error: {str(e)}") - finally: - with completion_lock: - completed_threads += 1 - - thread1 = threading.Thread(target=add_ingredient_from_stream, args=('{"title": "Test Ingredient Stream 1"}', self.testPath3, 1)) - thread2 = threading.Thread(target=add_ingredient_from_stream, args=('{"title": "Test Ingredient Stream 2"}', self.testPath4, 2)) - thread1.start() - thread2.start() - thread1.join() - thread2.join() - if any(e for e in add_errors if e is not None): - self.fail("\n".join(e for e in add_errors if e is not None)) - self.assertEqual(completed_threads, 2) - self.assertEqual(len(add_errors), 2) - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - read_ctx = Context() - reader = Reader("image/jpeg", output, context=read_ctx) - json_data = reader.json() - manifest_data = json.loads(json_data) - self.assertIn("active_manifest", manifest_data) - active_manifest_id = manifest_data["active_manifest"] - self.assertIn("manifests", manifest_data) - self.assertIn(active_manifest_id, manifest_data["manifests"]) - active_manifest = manifest_data["manifests"][active_manifest_id] - self.assertIn("ingredients", active_manifest) - self.assertEqual(len(active_manifest["ingredients"]), 2) - ingredient_titles = [ing["title"] for ing in active_manifest["ingredients"]] - self.assertIn("Test Ingredient Stream 1", ingredient_titles) - self.assertIn("Test Ingredient Stream 2", ingredient_titles) - builder.close() - - def test_builder_sign_with_same_ingredient_multiple_times(self): - """Test Builder with same ingredient added multiple times from different threads using context APIs""" - ctx = Context() - builder = Builder.from_json(self.manifestDefinition, context=ctx) - assert builder._handle is not None - add_errors = [] - add_lock = threading.Lock() - completed_threads = 0 - completion_lock = threading.Lock() - - def add_ingredient(ingredient_json, thread_id): - nonlocal completed_threads - try: - with open(self.testPath3, 'rb') as f: - builder.add_ingredient(ingredient_json, "image/jpeg", f) - with add_lock: - add_errors.append(None) - except Exception as e: - with add_lock: - add_errors.append(f"Thread {thread_id} error: {str(e)}") - finally: - with completion_lock: - completed_threads += 1 - - threads = [] - for i in range(1, 6): - ingredient_json = json.dumps({"title": f"Test Ingredient Thread {i}"}) - thread = threading.Thread(target=add_ingredient, args=(ingredient_json, i)) - threads.append(thread) - thread.start() - for thread in threads: - thread.join() - if any(e for e in add_errors if e is not None): - self.fail("\n".join(e for e in add_errors if e is not None)) - self.assertEqual(completed_threads, 5) - self.assertEqual(len(add_errors), 5) - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - read_ctx = Context() - reader = Reader("image/jpeg", output, context=read_ctx) - json_data = reader.json() - manifest_data = json.loads(json_data) - self.assertIn("active_manifest", manifest_data) - active_manifest_id = manifest_data["active_manifest"] - self.assertIn("manifests", manifest_data) - self.assertIn(active_manifest_id, manifest_data["manifests"]) - active_manifest = manifest_data["manifests"][active_manifest_id] - self.assertIn("ingredients", active_manifest) - self.assertEqual(len(active_manifest["ingredients"]), 5) - ingredient_titles = [ing["title"] for ing in active_manifest["ingredients"]] - self.assertEqual(len(set(ingredient_titles)), 5) - for i in range(1, 6): - thread_ingredients = [ing for ing in active_manifest["ingredients"] if ing["title"] == f"Test Ingredient Thread {i}"] - self.assertEqual(len(thread_ingredients), 1) - builder.close() - def test_builder_sign_with_multiple_ingredient_random_many_threads(self): """Test Builder with 12 threads adding ingredients and signing using context APIs""" TOTAL_THREADS_USED = 12 From e12750c5f0daf802c27ab4581ab0bde231642f7b Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Mon, 9 Mar 2026 15:46:16 -0700 Subject: [PATCH 18/18] fix: Only show APIs with cotnext --- docs/intents.md | 15 --- docs/selective-manifests.md | 195 +++++++++++++++--------------- docs/working-stores.md | 230 +++++++++--------------------------- 3 files changed, 155 insertions(+), 285 deletions(-) diff --git a/docs/intents.md b/docs/intents.md index 9fb90be3..e227a792 100644 --- a/docs/intents.md +++ b/docs/intents.md @@ -93,21 +93,6 @@ with Builder({}) as builder: builder.sign(signer, "image/jpeg", source, dest) ``` -### Using `load_settings` (deprecated) - -The legacy `load_settings` function can configure the intent for all subsequent `Builder` instances (thread-local configuration). This approach is deprecated in favor of context-based APIs: - -```py -from c2pa import load_settings, Builder - -# Deprecated: sets intent settings per thread -load_settings({"builder": {"intent": "edit"}}) - -with Builder({}) as builder: - with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: - builder.sign(signer, "image/jpeg", source, dest) -``` - ### Intent setting precedence When an intent is configured in multiple places , the most specific setting wins: diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 7971e27a..5ffa140f 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -36,30 +36,7 @@ The fundamental workflow is: ## Reading an existing manifest -Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The source asset is never modified. - -`Reader` also accepts an optional `context` parameter. This is especially important for trust configuration, which controls which certificates are trusted when validating signatures. Without a context, `Reader` uses SDK defaults. See [Configuring Reader](../context.md#configuring-reader) and [Trust configuration](../context.md#trust-configuration) for details. - -```py -# Without context (SDK default trust settings) -with open("signed_asset.jpg", "rb") as source: - with Reader("image/jpeg", source) as reader: - # Get the full manifest store as JSON - manifest_store = json.loads(reader.json()) - - # Identify the active manifest, which is the current/latest manifest - active_label = manifest_store["active_manifest"] - manifest = manifest_store["manifests"][active_label] - - # Access specific parts - ingredients = manifest["ingredients"] - assertions = manifest["assertions"] - thumbnail_id = manifest["thumbnail"]["identifier"] -``` - -### With context (trust configuration) - -To control which certificates are trusted during validation, pass a `Context` with trust settings to `Reader`: +Use `Reader` with a `Context` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The source asset is never modified. The context is used for trust configuration (which certificates are trusted when validating signatures) and verification settings. See [Configuring Reader](../context.md#configuring-reader) and [Trust configuration](../context.md#trust-configuration) for details. ```py ctx = Context.from_dict({ @@ -95,7 +72,7 @@ with open("thumbnail.jpg", "wb") as f: ## Filtering into a new Builder > [!NOTE] -> All `Builder` and `Reader` examples on this page also work with a `Context`. For `Reader`, a context provides trust configuration and verification settings: `Reader(format, source, context=ctx)`. For `Builder`, a context provides custom settings (thumbnails, claim generator, intent): `Builder(manifest_json, context=ctx)`. When a signer is configured in the context, `builder.sign()` can be called without an signer instance as argument. See [Context](../context.md) for details. +> All examples on this page use `Context` with `Reader` and `Builder`. For `Reader`, the context provides trust configuration and verification settings: `Reader(format, source, context=ctx)`. For `Builder`, the context provides custom settings (thumbnails, claim generator, intent): `Builder(manifest_json, context=ctx)`. When a signer is configured in the context, `builder.sign()` is called without a signer instance. See [Context](../context.md) for details. Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. @@ -123,8 +100,13 @@ This function is used throughout the examples below. ### Keep only specific ingredients ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + with open("signed_asset.jpg", "rb") as source: - with Reader("image/jpeg", source) as reader: + with Reader("image/jpeg", source, context=ctx) as reader: manifest_store = json.loads(reader.json()) active = manifest_store["manifests"][manifest_store["active_manifest"]] @@ -138,20 +120,27 @@ with open("signed_asset.jpg", "rb") as source: with Builder({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "ingredients": kept, - }) as new_builder: + }, context=ctx) as new_builder: transfer_ingredient_resources(reader, new_builder, kept) - # Sign the new Builder into an output asset source.seek(0) with open("output.jpg", "wb") as dest: - new_builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + new_builder.sign("image/jpeg", source, dest) ``` ### Keep only specific assertions ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + with open("signed_asset.jpg", "rb") as source: - with Reader("image/jpeg", source) as reader: + with Reader("image/jpeg", source, context=ctx) as reader: manifest_store = json.loads(reader.json()) active = manifest_store["manifests"][manifest_store["active_manifest"]] @@ -164,10 +153,13 @@ with open("signed_asset.jpg", "rb") as source: with Builder({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": kept, - }) as new_builder: + }, context=ctx) as new_builder: source.seek(0) with open("output.jpg", "wb") as dest: - new_builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + new_builder.sign("image/jpeg", source, dest) ``` ### Start fresh and preserve provenance @@ -200,10 +192,15 @@ flowchart TD ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + with Builder({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [], -}) as new_builder: +}, context=ctx) as new_builder: # Add the original as an ingredient to preserve provenance chain. # add_ingredient() stores the original's manifest as binary data inside # the ingredient, but does NOT copy the original's assertions. @@ -215,7 +212,10 @@ with Builder({ ) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - new_builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a Signer explicitly. + new_builder.sign("image/jpeg", source, dest) ``` ## Adding actions to a working store @@ -262,6 +262,11 @@ The SDK matches each value in `ingredientIds` against ingredients using this pri The `label` field on an ingredient is the **primary** linking key. Set a `label` on the ingredient and reference it in the action's `ingredientIds`. The label can be any string: it acts as a linking key between the ingredient and the action. ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + manifest_json = { "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [ @@ -285,7 +290,7 @@ manifest_json = { ], } -with Builder(manifest_json) as builder: +with Builder(manifest_json, context=ctx) as builder: # The label on the ingredient matches the value in ingredientIds with open("photo.jpg", "rb") as photo: builder.add_ingredient( @@ -300,7 +305,10 @@ with Builder(manifest_json) as builder: ) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) ``` ##### Linking multiple ingredients @@ -311,6 +319,11 @@ When linking multiple ingredients, each ingredient needs a unique label. > The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + manifest_json = { "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [ @@ -337,7 +350,7 @@ manifest_json = { ], } -with Builder(manifest_json) as builder: +with Builder(manifest_json, context=ctx) as builder: # parentOf ingredient linked to c2pa.opened with open("original.jpg", "rb") as original: builder.add_ingredient( @@ -365,7 +378,10 @@ with Builder(manifest_json) as builder: ) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) ``` #### Linking with `instance_id` @@ -373,6 +389,11 @@ with Builder(manifest_json) as builder: When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + # instance_id is used as the linking identifier and must be unique instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f" @@ -395,7 +416,7 @@ manifest_json = { ], } -with Builder(manifest_json) as builder: +with Builder(manifest_json, context=ctx) as builder: # No label set: instance_id is used as the linking key with open("source_photo.jpg", "rb") as photo: builder.add_ingredient( @@ -409,7 +430,10 @@ with Builder(manifest_json) as builder: ) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) ``` > [!NOTE] @@ -420,8 +444,10 @@ with Builder(manifest_json) as builder: After signing, `ingredientIds` is gone. The action's `parameters.ingredients[]` contains hashed JUMBF URIs pointing to ingredient assertions. To match an action to its ingredient, extract the label from the URL: ```py +ctx = Context.from_dict({"verify": {"verify_trust": True}}) + with open("signed_asset.jpg", "rb") as signed: - with Reader("image/jpeg", signed) as reader: + with Reader("image/jpeg", signed, context=ctx) as reader: manifest_store = json.loads(reader.json()) active_label = manifest_store["active_manifest"] manifest = manifest_store["manifests"][active_label] @@ -501,43 +527,17 @@ flowchart TD -```py -# Read from a catalog of archived ingredients -archive_stream.seek(0) -with Reader("application/c2pa", archive_stream) as reader: - manifest_store = json.loads(reader.json()) - active = manifest_store["manifests"][manifest_store["active_manifest"]] - - # Pick only the needed ingredients - selected = [ - ing for ing in active["ingredients"] - if ing["title"] in {"photo_1.jpg", "logo.png"} - ] - - with Builder({ - "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], - "ingredients": selected, - }) as new_builder: - transfer_ingredient_resources(reader, new_builder, selected) - - with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - new_builder.sign(signer, "image/jpeg", source, dest) -``` - -#### With context - -The same workflow can use a `Context` for custom settings. The context controls thumbnail generation, claim generator info, and other Builder settings. When a signer is configured in the context, `sign()` can be called without an explicit signer instance passed in as argument. - ```py ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, "claim_generator_info": {"name": "an-application", "version": "0.1.0"} - } + }, + "signer": signer, }) archive_stream.seek(0) -with Reader("application/c2pa", archive_stream) as reader: +with Reader("application/c2pa", archive_stream, context=ctx) as reader: manifest_store = json.loads(reader.json()) active = manifest_store["manifests"][manifest_store["active_manifest"]] @@ -553,6 +553,9 @@ with Reader("application/c2pa", archive_stream) as reader: transfer_ingredient_resources(reader, new_builder, selected) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. new_builder.sign("image/jpeg", source, dest) ``` @@ -663,9 +666,13 @@ flowchart TD **Step 1:** Build a working store and archive it: ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, +}) + with Builder({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], -}) as builder: +}, context=ctx) as builder: # Add ingredients to the working store with open("A.jpg", "rb") as ing_a: builder.add_ingredient( @@ -693,7 +700,7 @@ with Builder({ ```py archive_stream.seek(0) -with Reader("application/c2pa", archive_stream) as reader: +with Reader("application/c2pa", archive_stream, context=ctx) as reader: manifest_store = json.loads(reader.json()) active = manifest_store["manifests"][manifest_store["active_manifest"]] ingredients = active["ingredients"] @@ -702,29 +709,12 @@ with Reader("application/c2pa", archive_stream) as reader: **Step 3:** Create a new Builder with the extracted ingredients: ```py - # Pick the desired ingredients - selected = [ing for ing in ingredients if ing["title"] == "A.jpg"] - - with Builder({ - "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], - "ingredients": selected, - }) as new_builder: - transfer_ingredient_resources(reader, new_builder, selected) - - with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - new_builder.sign(signer, "image/jpeg", source, dest) -``` - -#### Step 3 with context - -When context settings need to be applied (such as thumbnail configuration or claim generator info), the builder can be created with a `Context`. If the archive was previously saved with `to_archive()`, it can be loaded with `with_archive()` to preserve those settings. - -```py - ctx = Context.from_dict({ + sign_ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, "claim_generator_info": {"name": "an-application", "version": "0.1.0"} - } + }, + "signer": signer, }) selected = [ing for ing in ingredients if ing["title"] == "A.jpg"] @@ -732,10 +722,13 @@ When context settings need to be applied (such as thumbnail configuration or cla with Builder({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "ingredients": selected, - }, context=ctx) as new_builder: + }, context=sign_ctx) as new_builder: transfer_ingredient_resources(reader, new_builder, selected) with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. new_builder.sign("image/jpeg", source, dest) ``` @@ -749,6 +742,11 @@ In some cases it is necessary to merge ingredients from multiple working stores When merging from multiple sources, resource identifier URIs can collide. Rename identifiers with a unique suffix when needed. Use two passes: (1) collect ingredients with collision handling, build the manifest, create the builder; (2) re-read each archive and transfer resources (use original ID for `resource_to_stream()`, renamed ID for `add_resource()` when collisions occurred). ```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + used_ids: set[str] = set() suffix_counter = 0 all_ingredients = [] @@ -757,7 +755,7 @@ archive_ingredient_counts = [] # Pass 1: Collect ingredients, renaming IDs on collision for archive_stream in archives: archive_stream.seek(0) - with Reader("application/c2pa", archive_stream) as reader: + with Reader("application/c2pa", archive_stream, context=ctx) as reader: manifest_store = json.loads(reader.json()) active = manifest_store["manifests"][manifest_store["active_manifest"]] ingredients = active["ingredients"] @@ -778,12 +776,12 @@ for archive_stream in archives: with Builder({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "ingredients": all_ingredients, -}) as builder: +}, context=ctx) as builder: # Pass 2: Transfer resources (match by ingredient index) offset = 0 for archive_stream, count in zip(archives, archive_ingredient_counts): archive_stream.seek(0) - with Reader("application/c2pa", archive_stream) as reader: + with Reader("application/c2pa", archive_stream, context=ctx) as reader: manifest_store = json.loads(reader.json()) active = manifest_store["manifests"][manifest_store["active_manifest"]] originals = active["ingredients"] @@ -800,5 +798,8 @@ with Builder({ offset += count with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: - builder.sign(signer, "image/jpeg", source, dest) + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) ``` diff --git a/docs/working-stores.md b/docs/working-stores.md index e8c0666a..1921b809 100644 --- a/docs/working-stores.md +++ b/docs/working-stores.md @@ -6,7 +6,7 @@ This table summarizes the fundamental entities that you work with when using the |--------|-------------|-------------|-------------| | [**Manifest store**](#manifest-store) | Final signed provenance data. Contains one or more manifests. | Embedded in asset or remotely in cloud | `Reader` class | | [**Working store**](#working-store) | Editable in-progress manifest. | `Builder` object | `Builder` class | -| [**Archive**](#archive) | Serialized working store | `.c2pa` file/stream | `Builder.to_archive()` / `Builder.from_archive()` | +| [**Archive**](#archive) | Serialized working store | `.c2pa` file/stream | `Builder.to_archive()` / `Builder.with_archive()` | | [**Resources**](#working-with-resources) | Binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. | In manifest. | `Builder.add_resource()` / `Reader.resource_to_stream()` | | [**Ingredients**](#working-with-ingredients) | Source materials used to create an asset. | In manifest. | `Builder.add_ingredient()` | @@ -23,7 +23,7 @@ graph TD A[Working Store
Builder object] -->|sign| MS A -->|to_archive| C[C2PA Archive
.c2pa file] - C -->|from_archive or with_archive| A + C -->|with_archive| A ``` ## Key entities @@ -68,7 +68,7 @@ A _C2PA archive_ (or just _archive_) contains the serialized bytes of a working **Characteristics:** - Portable serialization of a working store (Builder). -- Save an archive by using `Builder.to_archive()` and restore a full working store from an archive by using `Builder.from_archive()`. +- Save an archive by using `Builder.to_archive()` and restore a full working store from an archive by using `Builder.with_archive()` (with a Builder created from a Context). - Useful for separating manifest preparation ("work in progress") from final signing. For more information, see [Working with archives](#working-with-archives). @@ -79,21 +79,9 @@ Use the `Reader` class to read manifest stores from signed assets. ### Reading from a file -```py -from c2pa import Reader - -try: - # Without Context - reader = Reader("signed_image.jpg") - manifest_store_json = reader.json() -except Exception as e: - print(f"C2PA Error: {e}") -``` - ```py from c2pa import Context, Reader -# With Context (custom validation and trust settings) ctx = Context.from_dict({ "verify": { "verify_after_sign": True @@ -106,14 +94,6 @@ manifest_store_json = reader.json() ### Reading from a stream ```py -# Without Context -with open("signed_image.jpg", "rb") as stream: - reader = Reader("image/jpeg", stream) - manifest_json = reader.json() -``` - -```py -# With Context with open("signed_image.jpg", "rb") as stream: reader = Reader("image/jpeg", stream, context=ctx) manifest_json = reader.json() @@ -155,7 +135,7 @@ For full details on `Context` and `Settings`, see [Using Context to configure th The SDK also provides convenience methods to avoid manual JSON parsing: ```py -reader = Reader("signed_image.jpg") +reader = Reader("signed_image.jpg", context=ctx) # Get the active manifest directly as a dict active = reader.get_active_manifest() @@ -178,9 +158,8 @@ A **working store** is represented by a `Builder` object. It contains "live" man ```py import json -from c2pa import Builder +from c2pa import Builder, Context -# Without Context manifest_json = json.dumps({ "claim_generator_info": [{ "name": "example-app", @@ -190,13 +169,6 @@ manifest_json = json.dumps({ "assertions": [] }) -builder = Builder(manifest_json) -``` - -```py -from c2pa import Builder, Context - -# With Context (custom settings applied) ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": True} @@ -241,7 +213,7 @@ builder.set_remote_url("https://example.com/manifests/") When you sign an asset, the working store (Builder) becomes a manifest store embedded in the output: ```py -from c2pa import Signer, C2paSignerInfo, C2paSigningAlg +from c2pa import Signer, C2paSignerInfo, C2paSigningAlg, Context # Create a signer signer_info = C2paSignerInfo( @@ -252,13 +224,19 @@ signer_info = C2paSignerInfo( ) signer = Signer.from_info(signer_info) +ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": True}}, + "signer": signer, +}) +builder = Builder(manifest_json, context=ctx) + # Sign the asset - working store becomes a manifest store with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) # Now "signed.jpg" contains a manifest store # You can read it back with Reader -reader = Reader("signed.jpg") +reader = Reader("signed.jpg", context=ctx) manifest_store_json = reader.json() ``` @@ -267,12 +245,6 @@ manifest_store_json = reader.json() ### Creating a Builder (working store) ```py -# Without Context -builder = Builder(manifest_json) -``` - -```py -# With Context ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": True} @@ -308,19 +280,9 @@ signer = Signer.from_info(signer_info) ### Signing an asset -```py -# Without Context (explicit signer) -try: - with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - manifest_bytes = builder.sign(signer, "image/jpeg", src, dst) - print("Signed successfully!") -except Exception as e: - print(f"Signing failed: {e}") -``` +The Builder must be created with a Context that includes a signer. Then call `sign()` without passing a signer argument: ```py -# With Context (signer configured in context) -# The Builder must have been created with a Context that has a signer. try: with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: manifest_bytes = builder.sign("image/jpeg", src, dst) @@ -334,16 +296,10 @@ except Exception as e: You can also sign using file paths directly: ```py -# Without Context (explicit signer) -manifest_bytes = builder.sign_file("source.jpg", "signed.jpg", signer) -``` - -```py -# With Context (uses the context's signer when no signer argument is passed) manifest_bytes = builder.sign_file("source.jpg", "signed.jpg") ``` -### Complete example with Context +### Complete example This code combines the above examples to create, sign, and read a manifest. @@ -393,52 +349,6 @@ except Exception as e: print(f"Error: {e}") ``` -### Complete example (legacy, without Context) - -This code combines the examples to create, sign, and read a manifest. - -```py -import json -from c2pa import Builder, Reader, Signer, C2paSignerInfo, C2paSigningAlg - -try: - # 1. Define manifest for working store - manifest_json = json.dumps({ - "claim_generator_info": [{"name": "demo-app", "version": "0.1.0"}], - "title": "Signed image", - "assertions": [] - }) - - # 2. Load credentials - with open("certs.pem", "rb") as f: - certs = f.read() - with open("private_key.pem", "rb") as f: - private_key = f.read() - - # 3. Create signer - signer_info = C2paSignerInfo( - alg=C2paSigningAlg.ES256, - sign_cert=certs, - private_key=private_key, - ta_url=b"http://timestamp.digicert.com" - ) - signer = Signer.from_info(signer_info) - - # 4. Create working store (Builder) and sign - builder = Builder(manifest_json) - with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) - - print("Asset signed - working store is now a manifest store") - - # 5. Read back the manifest store - reader = Reader("signed.jpg") - print(reader.json()) - -except Exception as e: - print(f"Error: {e}") -``` - ## Working with resources _Resources_ are binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. @@ -469,7 +379,7 @@ To extract a resource, you need its JUMBF URI from the manifest store: ```py import json -reader = Reader("signed_image.jpg") +reader = Reader("signed_image.jpg", context=ctx) manifest_store = json.loads(reader.json()) # Get active manifest @@ -493,7 +403,8 @@ if "thumbnail" in manifest: When building a manifest, you add resources using identifiers. The SDK will reference these in your manifest JSON and convert them to JUMBF URIs during signing. ```py -builder = Builder(manifest_json) +ctx = Context.from_dict({"builder": {"thumbnail": {"enabled": True}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) # Add resource from a stream with open("thumbnail.jpg", "rb") as thumb: @@ -501,7 +412,7 @@ with open("thumbnail.jpg", "rb") as thumb: # Sign: the "thumbnail" identifier becomes a JUMBF URI in the manifest store with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` ## Working with ingredients @@ -521,7 +432,8 @@ Ingredients represent source materials used to create an asset, preserving the p When creating a manifest, add ingredients to preserve the provenance chain: ```py -builder = Builder(manifest_json) +ctx = Context.from_dict({"builder": {"claim_generator_info": {"name": "my-app", "version": "0.1.0"}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) # Define ingredient metadata ingredient_json = json.dumps({ @@ -535,7 +447,7 @@ with open("source.jpg", "rb") as ingredient: # Sign: ingredients become part of the manifest store with open("new_asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` ### Ingredient relationships @@ -548,7 +460,7 @@ Specify the relationship between the ingredient and the current asset: | `componentOf` | The ingredient is a component used in this asset | | `inputTo` | The ingredient was an input to creating this asset | -Example with explicit relationship: +Example with explicit relationship (builder is created with a Context as in the examples above): ```py ingredient_json = json.dumps({ @@ -578,8 +490,8 @@ The default binary format of an archive is the **C2PA JUMBF binary format** (`ap ```py import io -# Create and configure a working store -builder = Builder(manifest_json) +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) with open("source.jpg", "rb") as ingredient: @@ -608,12 +520,13 @@ There are two ways to load a working store from an archive. They differ in wheth Use `with_archive()` when you need the restored builder to use specific settings that you put on the Builder on instanciation by using a context as parameter of the Builder constructor. Create a `Builder` with a `Context` first, then call `with_archive()` to load the archived manifest definition into it. The archive replaces only the manifest definition; the builder's context and settings are preserved. ```py -# Create context with custom settings +# Create context with custom settings and signer ctx = Context.from_dict({ "builder": { "thumbnail": {"enabled": False}, "claim_generator_info": {"name": "My App", "version": "0.1.0"} - } + }, + "signer": signer, }) # Create builder with context, then load archive into it @@ -624,38 +537,21 @@ with open("manifest.c2pa", "rb") as archive: # The builder has the archived manifest definition # but keeps the context settings (no thumbnails, custom claim generator) with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` > [!IMPORTANT] > `with_archive()` replaces the builder's manifest definition with the one from the archive. Any definition passed to `Builder()` on instanciation is discarded. An empty dict `{}` is idiomatic for the initial definition when you plan to load an archive immediately after. -#### `from_archive()` (legacy) - -Use `from_archive()` for quick one-off operations where you don't need custom settings. It creates a **context-free** builder: no `Context` is attached, so all settings revert to SDK defaults. - -```py -# Restore from stream — no context, SDK defaults apply -with open("manifest.c2pa", "rb") as archive: - builder = Builder.from_archive(archive) - -# Sign with SDK default settings -with open("asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) -``` +#### Choosing how to restore from an archive -> [!WARNING] -> `from_archive()` does not accept a `context` parameter. Any settings that were active when the archive was created are not stored in the archive and are therefore lost. For example, if the original builder had thumbnails disabled via a `Context`, the builder returned by `from_archive()` will generate thumbnails using SDK defaults. Use `with_archive()` instead when you need to preserve settings on the Builder instance you are loading an archive into. +Use `with_archive()` so that the restored builder uses your `Context` (custom settings and signer). The archive carries only the manifest definition; it does not store context or settings. By creating a `Builder` with a `Context` and then calling `with_archive()`, you ensure the restored builder keeps your settings. -#### Choosing between `with_archive()` and `from_archive()` - -| | `with_archive()` | `from_archive()` | -|---|---|---| -| **Context preserved** | Yes — settings come from the builder's context | No — SDK defaults apply | -| **Usage pattern** | `Builder({}, context=ctx).with_archive(stream)` | `Builder.from_archive(stream)` | -| **When to use** | Production workflows, custom settings needed | Quick prototyping, SDK defaults are acceptable | -| **What the archive carries** | Only the manifest definition | Only the manifest definition | -| **What it does NOT carry** | Settings, signer, context | Settings, signer, context | +| | `with_archive()` | +|---|---| +| **Context preserved** | Yes — settings come from the builder's context | +| **Usage pattern** | `Builder({}, context=ctx).with_archive(stream)` | +| **What the archive carries** | Only the manifest definition (not settings, signer, or context) | ### Two-phase workflow example @@ -667,10 +563,12 @@ This step prepares the manifest on a Builder, and archives it into a Builder arc import io import json +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) @@ -688,27 +586,13 @@ print("Working store saved to artwork_manifest.c2pa") #### Phase 2: Sign the asset -```py -# Restore the working store -with open("artwork_manifest.c2pa", "rb") as archive: - builder = Builder.from_archive(archive) - -# Sign -with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) - -print("Asset signed with manifest store") -``` - -#### Phase 2 alternative: Sign with context - -In this step, after reloading the working store into a Builder instance configured with a context, settings on the Builder context can configure signing settings (e.g. thumbnails on/off). +Restore the working store with a Context so that settings (e.g. thumbnails on/off) and the signer are applied: ```py -# Restore the working store with context settings preserved ctx = Context.from_dict({ - "builder": {"thumbnail": {"enabled": False}} -}, signer=signer) + "builder": {"thumbnail": {"enabled": False}}, + "signer": signer, +}) with open("artwork_manifest.c2pa", "rb") as archive: builder = Builder({}, context=ctx) @@ -726,16 +610,15 @@ By default, manifest stores are **embedded** directly into the asset file. You c ### Default: embedded manifest stores ```py -builder = Builder(manifest_json) -# A builder object in this case can also be created -# using an additional Context parameter for settings propagation +ctx = Context.from_dict({"builder": {"thumbnail": {"enabled": True}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) # Default behavior: manifest store is embedded in the output with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) # Read it back — manifest store is embedded -reader = Reader("signed.jpg") +reader = Reader("signed.jpg", context=ctx) ``` ### External manifest stores (no embed) @@ -743,15 +626,14 @@ reader = Reader("signed.jpg") Prevent embedding the manifest store in the asset: ```py -builder = Builder(manifest_json) -# A builder object in this case can also be created -# using an additional Context parameter for settings propagation +ctx = Context.from_dict({"signer": signer}) +builder = Builder(manifest_json, context=ctx) builder.set_no_embed() # Don't embed the manifest store # Sign: manifest store is NOT embedded, manifest bytes are returned with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: - manifest_bytes = builder.sign(signer, "image/jpeg", src, dst) + manifest_bytes = builder.sign("image/jpeg", src, dst) # manifest_bytes contains the manifest store # Save it separately (as a sidecar file or upload to server) @@ -766,15 +648,14 @@ print("Manifest store saved externally to output.c2pa") Reference a manifest store stored at a remote URL: ```py -builder = Builder(manifest_json) -# A builder object in this case can also be created -# using an additional Context parameter for settings propagation +ctx = Context.from_dict({"signer": signer}) +builder = Builder(manifest_json, context=ctx) builder.set_remote_url("https://example.com/manifests/") # The asset will contain a reference to the remote manifest store with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` ## Best practices @@ -802,6 +683,9 @@ reader = Reader("asset.jpg", context=ctx) Add ingredients to your manifests to maintain a provenance chain: ```py +ctx = Context.from_dict({"builder": {"claim_generator_info": {"name": "my-app", "version": "0.1.0"}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) + ingredient_json = json.dumps({ "title": "Original source", "relationship": "parentOf" @@ -811,7 +695,7 @@ with open("original.jpg", "rb") as ingredient: builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) with open("edited.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: - builder.sign(signer, "image/jpeg", src, dst) + builder.sign("image/jpeg", src, dst) ``` ## Additional resources