diff --git a/README.md b/README.md index 0328ee93..043cac50 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Features: - Add assertions and ingredients to assets. - Examples and unit tests to demonstrate usage. -
+
For the best experience, read the docs on the [CAI Open Source SDK documentation website](https://opensource.contentauthenticity.org/docs/c2pa-c). @@ -21,7 +21,7 @@ If you want to view the documentation in GitHub, see: - [Supported formats](https://github.com/contentauth/c2pa-rs/blob/main/docs/supported-formats.md) - [Configuring the SDK using `Context` and `Settings`](docs/context-settings.md) - [Using Builder intents](docs/intents.md) to ensure spec-compliant manifests -- Using [working stores and archvies](docs/working-stores.md) +- Using [working stores and archives](docs/working-stores.md) - Selectively constructing manifests by [filtering actions and ingredients](docs/selective-manifests.md) - [Diagram of public classes in the Python library and their relationships](docs/class-diagram.md) - [Release notes](docs/release-notes.md) @@ -72,7 +72,7 @@ export C2PA_RS_PATH=/path/to/c2pa-rs make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH ``` -This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. The release profile is used by default; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`: +This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. By default, the build uses the release profile; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`: ```sh make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH EXTRA_BUILD_ARGS="--debug" diff --git a/docs/class-diagram.md b/docs/class-diagram.md index 5d9ad541..33cfd308 100644 --- a/docs/class-diagram.md +++ b/docs/class-diagram.md @@ -1,4 +1,3 @@ - # Class diagram This diagram shows the public classes in the Python library and their relationships. diff --git a/docs/context-settings.md b/docs/context-settings.md index 1ff17dff..35692a82 100644 --- a/docs/context-settings.md +++ b/docs/context-settings.md @@ -274,10 +274,10 @@ The Settings JSON has this top-level structure: The settings format is **JSON** only. Pass JSON strings to `Settings.from_json()` or `Context.from_json()`, and dictionaries to `Settings.from_dict()` or `Context.from_dict()`. The `from_dict()` methods convert Python dictionaries to a format compatible with the underlying native libraries. -Notes: -- All properties are optional. If you don't specify a value, 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. -- For Boolean values, use JSON Booleans `true`/`false` in JSON strings, or Python `True`/`False` in dicts. +> [!NOTE] +> - All properties are optional. If you don't specify a value, 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. +> - For Boolean values, use JSON Booleans `true`/`false` in JSON strings, or Python `True`/`False` in dicts. The settings JSON schema is shared across all C2PA SDKs (Rust, C/C++, Python, and so on). For a complete reference to all properties, see the [SDK object reference - Settings](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema). @@ -420,8 +420,8 @@ The following properties default to `true` (verification enabled): - `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_trust` - Verify signing certificates against configured trust anchors. WARNING: Disabling makes verification non-compliant. -- `verify_timestamp_trust` - Verify timestamp authority (TSA) certificates. WARNING: Disabling makes verification non-compliant. +- `verify_trust` - Verify signing certificates against configured trust anchors. **Warning:** Disabling makes verification non-compliant. +- `verify_timestamp_trust` - Verify timestamp authority (TSA) certificates. **Warning:** Disabling makes verification non-compliant. - `remote_manifest_fetch` - Fetch remote manifests referenced in the asset. Disable in offline or air-gapped environments. > [!WARNING] @@ -628,9 +628,9 @@ C2PA uses a certificate-based trust model to prove who signed an asset. When cre - **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. Verifiers use this chain to confirm 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, proving _when_ the signature was made. This keeps signatures verifiable even after the signing certificate expires. -### Signer from settings (recommended) +### Signer from settings -Configure signer credentials directly in settings. This is the most common approach: +Configure signer credentials directly in settings. This is the most common and recommended approach: ```py ctx = Context.from_dict({ @@ -649,7 +649,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: builder.sign("image/jpeg", src, dst) ``` -### Signer on Context (signer object) +### Signer object on Context Create a `Signer` object and pass it to the `Context`. The signer is **consumed**: the `Signer` object becomes invalid after this call and the `Context` takes ownership. diff --git a/docs/intents.md b/docs/intents.md index 1126be37..bd970185 100644 --- a/docs/intents.md +++ b/docs/intents.md @@ -87,7 +87,7 @@ for path in image_paths: builder.sign_file(path, output_path(path), signer) ``` -### Using `set_intent` on the Builder +### Using set_intent on the Builder Call `set_intent` directly on a `Builder` instance for one-off operations or when the intent is determined at runtime. For example: @@ -166,7 +166,7 @@ flowchart LR For `Edit` and `Update` intents, `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`), `Builder` uses that one instead and does not automatically create one from the source. -### How intent relates to `add_ingredient` +### How intent relates to add_ingredient The `Builder` intent controls what the `Builder` does with the source stream (source asset) at sign time. The `add_ingredient` method adds other ingredients explicitly. These are separate concerns. @@ -208,7 +208,7 @@ from c2pa import ( ) ``` -### Using `set_intent` +### Using set_intent Use the `Builder.set_intent` method to specify the intent: diff --git a/docs/native-resources-management.md b/docs/native-resources-management.md index 100936d0..15d9c8f9 100644 --- a/docs/native-resources-management.md +++ b/docs/native-resources-management.md @@ -1,8 +1,8 @@ -# Native resource management (ManagedResource class) +# Native resource management `ManagedResource` is the internal base class used by the C2PA Python SDK to wrap native (Rust/FFI) pointers. When adding new wrappers around native resources `ManagedResource` should be subclassed and follow the documented lifecycle rules. -## Why `ManagedResource`? +## Why ManagedResource? `ManagedResource` is the internal base class responsible for managing native pointers owned by the C2PA Python SDK. It guarantees: @@ -27,7 +27,7 @@ Python manages its own objects' memory automatically through garbage collection. This system works well for pure Python objects, but native memory sits outside of it entirely. The garbage collector sees the Python wrapper object (e.g. a `Reader` instance) and tracks references to it, but it has no visibility into the native memory that the wrapper's `_handle` attribute points to. Memory allocated by native libraries is invisible to the garbage collector: it does not know the size of that native allocation, cannot tell when it is no longer needed, and will not call the native library's `c2pa_free` function to release it. If the Python wrapper of those native resources is collected without first calling `c2pa_free`, the native memory is never released and leaks. -### Why `__del__` is not reliable enough +### Why __del__ is not reliable enough Python does offer `__del__` as a hook that runs when an object is collected (finalizer), and `ManagedResource` uses it as a fallback to possibly clean up leftover resources at that point. But `__del__` cannot be relied on as the primary cleanup mechanism: its timing is unpredictable (due to being called when the garbage collection runs, which is non-deterministic itself), it may not run at all during interpreter shutdown, and other Python implementations (PyPy, GraalPy) that do not use reference counting make its behavior even less deterministic. @@ -70,7 +70,7 @@ Notes: > The MRO is computed using C3 linearization, which enforces two rules: children appear before their parents, and left-to-right order from the class definition is preserved. For `class Context(ManagedResource, ContextProvider)`: > > 1. `Context`: the class itself always comes first. -> 2. `ManagedResource` :first listed parent, nothing else requires it to appear later. +> 2. `ManagedResource`: first listed parent, nothing else requires it to appear later. > 3. `ContextProvider`: second listed parent, must come after `ManagedResource` to preserve declaration order. > 4. `ABC`: parent of `ContextProvider`, must come after its child. > 5. `object`: root of everything (all objects), always last. @@ -134,7 +134,7 @@ Every public method calls `_ensure_valid_state()` before doing any work. Besides ## Ways to clean up -### Context manager (`with` statement) +### Using a context manager ```python with Reader("image.jpg") as reader: @@ -144,7 +144,7 @@ with Reader("image.jpg") as reader: When the `with` block exits, `__exit__` calls `close()`, which frees the native pointer. This is the safest approach because cleanup happens even if the code inside the block raises an exception. -### Explicit `.close()` +### Explicit close ```python reader = Reader("image.jpg") @@ -156,7 +156,7 @@ finally: Calling `.close()` directly is equivalent to exiting a `with` block. It is idempotent: calling it multiple times is safe and does nothing after the first call. -### Destructor fallback (`__del__`) +### Destructor fallback If neither of the above is used, `__del__` attempts to free the native pointer when Python garbage-collects the object. As described above, `__del__` timing is unpredictable and it may not run at all, so it is a safety net rather than a primary cleanup mechanism. @@ -269,7 +269,7 @@ self._handle = new_ptr The object stays `ACTIVE` throughout because the Python-side object is still valid: it has a live native pointer, its public methods still work, and callers may continue using it (e.g. reading the updated manifest or feeding in another fragment). The lifecycle state does not change because from `ManagedResource`'s perspective nothing has closed. Only the underlying native pointer has been swapped. This is different from `_mark_consumed()`, where the object transitions to `CLOSED` and becomes unusable. The old pointer must not be freed by `ManagedResource` because the native library already consumed it as part of the FFI call. -## Subclass-specific cleanup with `_release()` +## Subclass-specific cleanup Each subclass can override `_release()` to clean up its own resources before the native pointer is freed. The base implementation does nothing. @@ -285,7 +285,7 @@ Examples from the codebase: The cleanup order matters: `_release()` runs first (closing streams, dropping callbacks), then `c2pa_free` frees the native pointer. This order prevents the native library from accessing Python objects that no longer exist. -## Why is `Stream` not a `ManagedResource`? +## Why is Stream not a ManagedResource? `Stream` wraps a Python stream-like object (file stream or memory stream) so the native library can read from and write to it via callbacks. It does not inherit from `ManagedResource`, and it uses `c2pa_release_stream()` instead of `c2pa_free()` for cleanup. @@ -293,7 +293,7 @@ The reason is that ownership runs in the opposite direction. A `Reader` or `Buil `Stream` tracks its own state with `_closed` and `_initialized` flags rather than `LifecycleState`, but it supports the same three cleanup paths: context manager, explicit `.close()`, and `__del__` fallback. -## Implementing a subclass of `ManagedResource` +## Implementing a subclass of ManagedResource To wrap a new native resource, inherit from `ManagedResource` and follow these rules: diff --git a/docs/project-contributions.md b/docs/project-contributions.md index 1e91b22c..f2f4b3d7 100644 --- a/docs/project-contributions.md +++ b/docs/project-contributions.md @@ -1,6 +1,6 @@ -# Contributing to the project +# Contributing to the project -The information in this page is primarily for those who wish to contribute to the c2pa-python library project itself, rather than those who simply wish to use it in an application. For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md). +The information in this page is primarily for those who wish to contribute to the c2pa-python library project itself, rather than those who simply wish to use it in an application. For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md). ## Setup @@ -28,7 +28,7 @@ pip install -r requirements.txt pip install -r requirements-dev.txt ``` -Download [c2pa-rs library artifacts](https://github.com/contentauth/c2pa-rs/tags) for the current version you want, (for example, as shown below for v0.73.1): +Download [c2pa-rs library artifacts](https://github.com/contentauth/c2pa-rs/tags) for the current version you want (for example, as shown below for v0.73.1): ```bash python scripts/download_artifacts.py c2pa-v0.73.1 @@ -58,7 +58,7 @@ pip install -U pytest python3 -m build --wheel ``` -To test local wheels locally, enter this command: +To test the local wheel build, enter this command: ```bash make test-local-wheel-build @@ -94,26 +94,28 @@ make verify-wheel-build ## Testing -The project uses [PyTest](https://docs.pytest.org/) and [unittest](https://docs.python.org/3/library/unittest.html) for testing. +The project uses [pytest](https://docs.pytest.org/) and [unittest](https://docs.python.org/3/library/unittest.html) for testing. Run tests by following these steps: 1. Activate the virtual environment: `source .venv/bin/activate` 2. (optional) Install dependencies: `pip install -r requirements.txt` -4. Run the tests: +3. Run the tests: ```bash make test ``` -5. Alternatively, install `pytest` (if not already installed) and run it: +4. Alternatively, install `pytest` (if not already installed) and run it: ```bash pip install pytest pytest ``` - **Warning**: Using `pytest` can lead to issues if you often switch between virtual environments. + + > [!WARNING] + > Using `pytest` can lead to issues if you often switch between virtual environments. ### Testing during bindings development -While developing bindings locally, we use [unittest](https://docs.python.org/3/library/unittest.html), since [PyTest](https://docs.pytest.org/) can get confused by virtual environment re-deployments (especially if you bump the version number). +While developing bindings locally, we use [unittest](https://docs.python.org/3/library/unittest.html), since [pytest](https://docs.pytest.org/) can get confused by virtual environment re-deployments (especially if you bump the version number). To run tests while developing bindings, enter this command: @@ -150,5 +152,3 @@ View the output by loading `api-docs/build/html/index.html` in a web browser. This uses `sphinx-autoapi` to parse `src/c2pa` directly, avoiding imports of native libs. - Entry script: `scripts/generate_api_docs.py` - Config: `api-docs/conf.py`; index: `api-docs/index.rst` - -Sphinx config is in `api-docs/conf.py`, which uses `index.rst`. diff --git a/docs/release-notes.md b/docs/release-notes.md index 31ff90a8..f8f09717 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,7 +29,7 @@ See [Release tag 0.6.0](https://github.com/contentauth/c2pa-python/releases/tag/ ### Breaking changes -The signature of the `c2pa.sign_ps256()` method changed. Previously, the argument was a file path but now its the PEM certificate string. +The signature of the `c2pa.sign_ps256()` method changed. Previously, the argument was a file path, but now it's the PEM certificate string. ## Version 0.5.2 @@ -55,23 +55,23 @@ New features in this release: - You can archive a `Builder` using `builder.to_archive` and reconstruct it with `builder.from_archive`. - Signers can be constructed with `c2pa_api.create_signer`. - The signer now requires a signing function to keep private keys private. -- Example signing functions are provided in c2pa_api.py +- Example signing functions are provided in `c2pa_api.py`. ## Version 0.4.0 This release: - Changes the name of the import from `c2pa-python` to `c2pa`. -- Supports pre-built platform wheels for macOS, Windows and [manylinux](https://github.com/pypa/manylinux). +- Supports pre-built platform wheels for macOS, Windows, and [manylinux](https://github.com/pypa/manylinux). ## Version 0.3.0 This release includes some breaking changes to align with future APIs: -- `C2paSignerInfo` moves the `alg` to the first parameter from the 3rd. +- `C2paSignerInfo` moves the `alg` to the first parameter from the third. - `c2pa.verify_from_file_json` is now `c2pa.read_file`. - `c2pa.ingredient_from_file_json` is now `c2pa.read_ingredient_file`. - `c2pa.add_manifest_to_file_json` is now `c2pa.sign_file`. -- There are many more specific errors types now, and Error messages always start with the name of the error i.e (str(err.value).startswith("ManifestNotFound")). -- The ingredient thumbnail identifier may be jumbf uri reference if a valid thumb already exists in the active manifest. -- Extracted file paths for read_file now use a folder structure and different naming conventions. +- There are many more specific error types now, and error messages always start with the name of the error, i.e., `str(err.value).startswith("ManifestNotFound")`. +- The ingredient thumbnail identifier may be a JUMBF URI reference if a valid thumb already exists in the active manifest. +- Extracted file paths for `read_file` now use a folder structure and different naming conventions. diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index de424fc6..0dd8553a 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,6 +1,6 @@ # 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). +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: @@ -10,7 +10,7 @@ This process is best described as *filtering* or *rebuilding* a working store: 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." +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. @@ -162,7 +162,7 @@ with open("signed_asset.jpg", "rb") as source: ### 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()`. +Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. Do this 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: @@ -187,8 +187,6 @@ flowchart TD style NI fill:#efe,stroke:#090 ``` - - ```py ctx = Context.from_dict({ "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, @@ -235,7 +233,6 @@ builder.add_action({ ### Action JSON fields - | Field | Required | Description | | --- | --- | --- | | `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | @@ -243,19 +240,18 @@ builder.add_action({ | `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 +#### 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` +#### 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. @@ -382,7 +378,7 @@ with Builder(manifest_json, context=ctx) as builder: builder.sign("image/jpeg", source, dest) ``` -#### Linking with `instance_id` +#### Linking with instance_id When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. @@ -466,7 +462,7 @@ with open("signed_asset.jpg", "rb") as signed: # matched is the ingredient linked to this action ``` -#### When to use `label` vs `instance_id` +#### When to use label vs instance_id | Property | `label` | `instance_id` | | --- | --- | --- | @@ -476,13 +472,12 @@ with open("signed_asset.jpg", "rb") as signed: | **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 and archives](working-stores.md). +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 you to save, transfer, or resume the work later. For more background on working stores and archives, see [Working stores and archives](working-stores.md). There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working store archives) and ingredient archives. @@ -534,8 +529,6 @@ flowchart TD style CTX fill:#e8f4fd,stroke:#4a90d9 ``` - - ```py ctx = Context.from_dict({ "builder": { @@ -692,13 +685,13 @@ Action linking also changes between the two approaches. Legacy catalog code link #### Choosing between approaches -The legacy read-filter-rebuild APIs fit when the catalog already exists as a single `.c2pa` builder archive that bundles all ingredients together and the consumer wants a subset—picked via `Reader` + manual JSON filtering. The dedicated ingredient archive APIs fit when ingredients are produced and consumed independently: each ingredient gets its own archive, so no Reader-based filtering is needed. Both produce the same signed output. +The legacy read-filter-rebuild APIs fit when the catalog already exists as a single `.c2pa` builder archive that bundles all ingredients together and the consumer wants a subset, picked via `Reader` + manual JSON filtering. The dedicated ingredient archive APIs fit when ingredients are produced and consumed independently: each ingredient gets its own archive, so no Reader-based filtering is needed. Both produce the same signed output. ### Identifying ingredients in archives When building an ingredient archive, you can set `instance_id` on the ingredient to give it a stable, caller-controlled identifier. This field survives archiving and signing unchanged, so it can be used to look up a specific ingredient from a catalog archive. The `description` and `informational_URI` fields also survive and can carry additional metadata about the ingredient's origin. -`instance_id` is only for identification and catalog lookups. It cannot be used as a linking key in `ingredientIds` when linking ingredient archives to actions — use `label` for that (see [Linking an archived ingredient to an action](#linking-an-archived-ingredient-to-an-action)). +`instance_id` is only for identification and catalog lookups. It cannot be used as a linking key in `ingredientIds` when linking ingredient archives to actions: use `label` for that (see [Linking an archived ingredient to an action](#linking-an-archived-ingredient-to-an-action)). ```py # Set instance_id when adding the ingredient to the archive builder. @@ -836,8 +829,6 @@ flowchart TD style CTX fill:#e8f4fd,stroke:#4a90d9 ``` - - **Step 1:** Build a working store and archive it: ```py @@ -1250,7 +1241,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: Some actions reference ingredients (via `parameters.ingredients[].url` after signing). If keeping an action that references an ingredient, **the corresponding ingredient and its binary resources must also be kept**. If an ingredient is dropped, any actions that reference it must also be dropped (or updated). -#### `c2pa.opened` action +#### c2pa.opened action The `c2pa.opened` action is special because it must be the first action and it references the asset that was opened (the `parentOf` ingredient). When filtering: @@ -1258,7 +1249,7 @@ The `c2pa.opened` action is special because it must be the first action and it r - **Keep the ingredient it references**: the `parentOf` ingredient linked via its `parameters.ingredients[].url`. - Removing the ingredient that `c2pa.opened` points to will make the manifest invalid. -#### `c2pa.placed` action +#### c2pa.placed action The `c2pa.placed` action references a `componentOf` ingredient that was composited into the asset. When filtering: diff --git a/docs/usage.md b/docs/usage.md index 46f7399a..c7754e72 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -52,7 +52,7 @@ manifest_json = json.dumps({ ## Settings, Context, and ContextProvider -The `Settings` and `Context` classes provide per-instance configuration for `Reader` and `Builder` operations, replacing the global `load_settings()` function, which is now deprecated. +The `Settings` and `Context` classes provide per-instance configuration for `Reader` and `Builder` operations, replacing the global `load_settings()` function, which is now deprecated. See [Context and settings](context-settings.md) for details. @@ -179,7 +179,7 @@ except Exception as 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 is 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). +> 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 is insecure. Instead, use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key, as shown in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). Pass a `Context` to the `Builder` to apply custom settings during signing. The signer is still passed explicitly to `builder.sign()`. @@ -237,8 +237,8 @@ except Exception as 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 IS insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; for example as shown in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). +> [!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 is insecure. Instead, use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key, as shown in the [C2PA Python Example](https://github.com/contentauth/c2pa-python-example). ```py try: diff --git a/docs/working-stores.md b/docs/working-stores.md index 247d80fe..88364e1c 100644 --- a/docs/working-stores.md +++ b/docs/working-stores.md @@ -145,7 +145,7 @@ else: ``` - `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. +- `manifests`: A dictionary of all manifests in the store, keyed by their URI label. Re-signed assets, or assets 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: @@ -164,11 +164,11 @@ 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. +`Reader.detailed_json()` returns a more comprehensive JSON representation with a different structure than `json()`. It is useful when you need additional details about the manifest internals. ## Using working stores -A **working store** is represented by a `Builder` object. It contains "live" manifest data as you add information to it. +A `Builder` object represents a **working store**. It contains "live" manifest data as you add information to it. ### Creating a working store @@ -281,11 +281,12 @@ signer_info = C2paSignerInfo( 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). +> [!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 -The `Builder` must be created with a `Context` that includes a signer. Then call `sign()` without passing a signer argument: +Create the `Builder` with a `Context` that includes a signer, then call `sign()` without passing a signer argument: ```py try: @@ -358,11 +359,11 @@ except Exception as e: _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. +C2PA manifest data is not just JSON. A manifest store also contains binary resources (thumbnails, ingredient data, and other embedded files) that the JSON metadata references by JUMBF URIs. When you call `reader.json()`, 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. +When you add a resource to a working store (`Builder`), you assign it an identifier string. During signing, the SDK creates the manifest store and automatically converts this identifier to a proper JUMBF URI. **Resource identifier workflow:** @@ -443,32 +444,32 @@ Ingredients represent source materials used to create an asset, preserving the p 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. -Ingredients themselves can be turned into ingredient archives (`.c2pa`). An ingredient archive is a `Builder` archive containing _exactly one_ ingredient. Ingredient archives can be added directly as an ingredient to another working store using the `application/c2pa` MIME type — no un-archiving step is needed. +You can turn ingredients themselves into ingredient archives (`.c2pa`). An ingredient archive is a `Builder` archive containing _exactly one_ ingredient. You can add ingredient archives directly as an ingredient to another working store using the `application/c2pa` MIME type, with no un-archiving step needed. ### Ingredient vs. ingredient archive A **(plain) ingredient** is a source asset that the builder reads at `add_ingredient` time. The builder sees the asset's bytes and stores the required ingredient data (including any caller-set `instance_id`) inside the new manifest. -An **ingredient archive** (in c2pa archive format) is a `.c2pa` file that already contains a fully-formed ingredient. It can be produced with `write_ingredient_archive` (dedicated ingredient archive APIs) or with `to_archive()` on a builder holding one ingredient (legacy). When passed to `add_ingredient`, the builder treats the archive's contents as opaque provenance: the archive's internal fields are not exposed as live JSON the signing builder can introspect or use for linking to actions. Only the JSON the caller supplies in the current `add_ingredient` call is visible to the builder in that round. +An **ingredient archive** (in c2pa archive format) is a `.c2pa` file that already contains a fully-formed ingredient. You can produce it with `write_ingredient_archive` (dedicated ingredient archive APIs) or with `to_archive()` on a builder holding one ingredient (legacy). When passed to `add_ingredient`, the builder treats the archive's contents as opaque provenance: the signing builder cannot introspect the archive's internal fields as live JSON or use them for linking to actions. Only the JSON the caller supplies in the current `add_ingredient` call is visible to the builder in that round. -Once an ingredient is archived, the original ingredient asset is no longer needed: the `.c2pa` ingredient archive stands in for it and carries the ingredient's provenance. +After you archive an ingredient, you no longer need the original ingredient asset: the `.c2pa` ingredient archive stands in for it and carries the ingredient's provenance. > [!NOTE] -> The relationship is one-directional. For legacy support you can _read_ an ingredient out of a builder archive, but you should not try to restore a `Builder` from an ingredient archive — consume it as an ingredient with `add_ingredient_from_archive` (or the legacy `add_ingredient(json, "application/c2pa", archive)` path) instead. +> The relationship is one-directional. For legacy support you can _read_ an ingredient out of a builder archive, but you should not try to restore a `Builder` from an ingredient archive: consume it as an ingredient with `add_ingredient_from_archive` (or the legacy `add_ingredient(json, "application/c2pa", archive)` path) instead. For the dedicated ingredient archive APIs, see [Single-ingredient archive APIs](#single-ingredient-archive-apis). -This difference governs how each can be linked to an action via `ingredientIds`. The table below covers all three cases: plain ingredients, ingredient archives loaded via the dedicated APIs (recommended), and ingredient archives loaded via the legacy `add_ingredient` path: +This difference governs how you can link each to an action via `ingredientIds`. The table below covers all three cases: plain ingredients, ingredient archives loaded via the dedicated APIs (recommended), and ingredient archives loaded via the legacy `add_ingredient` path: | Aspect | Ingredient | Ingredient archive (dedicated APIs: `write_ingredient_archive` + `add_ingredient_from_archive`) | Ingredient archive (legacy load via `add_ingredient(json, "application/c2pa", archive)`) | | --- | --- | --- | --- | -| Source format passed to `add_ingredient` | Asset MIME type (`image/jpeg`, `video/mp4`, ...) | N/A — loaded via `add_ingredient_from_archive(stream)` | `"application/c2pa"` | +| Source format passed to `add_ingredient` | Asset MIME type (`image/jpeg`, `video/mp4`, ...) | N/A: loaded via `add_ingredient_from_archive(stream)` | `"application/c2pa"` | | What it is | "Live" asset | A serialized single-ingredient archive (opaque provenance) | A serialized manifest store (opaque provenance) | | Linking via `label` | Primary linking key, set on the signing builder's `add_ingredient` JSON | Pass `label` value as archive key to `write_ingredient_archive`; flows through as `ingredientIds` value | Only linking key that works, set on the signing builder's `add_ingredient` JSON | | Linking via `instance_id` | Alternative to using `label` | Pass `instance_id` value as archive key to `write_ingredient_archive`; flows through as `ingredientIds` value | Does not link, signing-time error | -| Linking via a `label` baked in at archive-creation time | N/A (not an archive) | N/A — archive key is set explicitly at `write_ingredient_archive` call time | Does not carry through, must be re-asserted on the signing builder's `add_ingredient` JSON | +| Linking via a `label` baked in at archive-creation time | N/A (not an archive) | N/A: archive key is set explicitly at `write_ingredient_archive` call time | Does not carry through, must be re-asserted on the signing builder's `add_ingredient` JSON | -#### When to use `label` vs `instance_id` +#### When to use label vs instance_id | Property | `label` | `instance_id` | | --- | --- | --- | @@ -478,7 +479,7 @@ This difference governs how each can be linked to an action via `ingredientIds`. | Survives signing | SDK may reassign the actual assertion label in the signed manifest | 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 a stable identifier that persists unchanged across rebuilds is needed. The `label` used at build time may be reassigned by the SDK during signing and will not appear unchanged in `reader.json()` output. +Use `label` when defining manifests in JSON. Use `instance_id` when you need a stable identifier that persists unchanged across rebuilds. The SDK may reassign the `label` used at build time during signing, so it will not appear unchanged in `reader.json()` output. ### Adding ingredients to a working store @@ -505,7 +506,7 @@ with open("new_asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: ### Linking an ingredient archive to an action -To link an ingredient archive to an action via `ingredientIds`, you must use a `label` set in the `add_ingredient()` call on the signing builder. Labels baked into the archive ingredient are not carried through, and `instance_id` does not work as a linking key for ingredient archives regardless of where it is set. +To link an ingredient archive to an action via `ingredientIds`, you must use a `label` set in the `add_ingredient()` call on the signing builder. Signing does not carry through labels baked into the archive ingredient, and `instance_id` does not work as a linking key for ingredient archives regardless of where it is set. ```py import io, json @@ -707,11 +708,11 @@ with open("manifest.c2pa", "wb") as f: 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. +A `Builder` containing **only one ingredient and only the ingredient data** (no other ingredient, no other actions) is an ingredient archive. You can add ingredient archives directly as an ingredient to other working stores too. ### Restoring a working store from archive -To restore a `Builder` from a working store, use `with_archive()`. The restored `Builder` will have the settings used when the `Builder` was created with a `Context`. The archive replaces only the manifest definition; the `Context` and `Settings` are preserved. +To restore a `Builder` from a working store, use `with_archive()`. The restored `Builder` keeps the settings used when you created the `Builder` with a `Context`. The archive replaces only the manifest definition and preserves the `Context` and `Settings`. ```py # Create context with custom settings and signer @@ -739,7 +740,7 @@ with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: ### Restoring with context preservation -Create a `Builder` with a custom `Context` first, then load the archive into it. The context settings (thumbnails, claim generator, signer) are preserved, and only the manifest definition is replaced by the archive. +Create a `Builder` with a custom `Context` first, then load the archive into it. The archive replaces only the manifest definition and preserves the context settings (thumbnails, claim generator, signer). ```py from c2pa import Builder, Context @@ -794,7 +795,7 @@ print("Working store saved to artwork_manifest.c2pa") **Phase 2: Sign the asset** -Restore the working store with a `Context` so that settings (e.g. thumbnails on/off) and the signer are applied: +Restore the working store with a `Context` that applies settings (e.g., thumbnails on/off) and the signer: ```py ctx = Context.from_dict({ @@ -821,13 +822,13 @@ The `Builder` class exposes two dedicated APIs for moving a single ingredient be - `builder.write_ingredient_archive(ingredient_id, stream)` writes one already-registered ingredient out as a single-ingredient JUMBF archive. The destination stream must be writable and seekable. - `builder.add_ingredient_from_archive(stream)` loads one such archive into a builder. The source stream must be readable and seekable, **positioned at byte 0** before the call. After calling `write_ingredient_archive`, always call `stream.seek(0)` before passing the stream to `add_ingredient_from_archive`. -#### How `add_ingredient` and `write_ingredient_archive` interact +#### How add_ingredient and write_ingredient_archive interact `add_ingredient(json, format, stream)` is the registration step. It hashes the source asset, builds the ingredient assertion, and stores the ingredient in the builder under an id read from the JSON. The id is the `label` field if present, otherwise `instance_id`. `write_ingredient_archive(ingredient_id, stream)` is a lookup step rather than a factory. It finds an ingredient that was already registered under `ingredient_id` and serializes that one ingredient as a JUMBF archive. Calling it without a prior `add_ingredient` for that id raises `C2paError`. -The exported archive is not a lossless slice of the parent. It contains one cloned ingredient and a fresh claim instance id. Any other ingredients on the parent builder are omitted. +The exported archive is not a lossless slice of the parent. It contains one cloned ingredient and a fresh claim instance id. The export omits any other ingredients on the parent builder. `add_ingredient_from_archive(stream)` adds the ingredient back to a consuming builder, keyed by the same id the producer used. @@ -877,7 +878,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: #### Id resolution -The id passed to `write_ingredient_archive` is matched against each registered ingredient's `label` and its `instance_id`. The first ingredient whose `label` or `instance_id` equals the id is selected (OR-match, no precedence). If both are set on the same ingredient, pass whichever value is to be used as the linking key. See [Lookup keys and action linking](#lookup-keys-and-action-linking) for the full table of linking outcomes. +`write_ingredient_archive` matches the id passed to it against each registered ingredient's `label` and its `instance_id`. It selects the first ingredient whose `label` or `instance_id` equals the id (OR-match, no precedence). If both are set on the same ingredient, pass whichever value you want to use as the linking key. See [Lookup keys and action linking](#lookup-keys-and-action-linking) for the full table of linking outcomes. #### Errors when handling ingredient archives with write_ingredient_archive @@ -910,7 +911,7 @@ builder.write_ingredient_archive("ingredient-id", archive) For a multi-archive use case (one catalog, many ingredients picked at build time), see [The ingredients catalog pattern](./selective-manifests.md#the-ingredients-catalog-pattern). -#### Migration guide: from `to_archive` / `with_archive` to single-ingredient APIs +#### Migration guide: from to_archive and with_archive to single-ingredient APIs The legacy approach wrapped one ingredient in a full builder archive, then restored it with `with_archive`: @@ -953,21 +954,21 @@ Key differences: no JSON parsing, no `add_resource` loops, each archive holds ex Action linking also changes between the two APIs. The legacy load path (`add_ingredient(json, "application/c2pa", archive)`) accepts only `label` as the linking key on the signing builder's `add_ingredient` JSON. See [Linking an ingredient archive to an action](#linking-an-ingredient-archive-to-an-action). The dedicated ingredient archive APIs (`write_ingredient_archive` + `add_ingredient_from_archive`) accept the archive key, which can be either `label` or `instance_id`. See [Lookup keys and action linking](#lookup-keys-and-action-linking). When migrating code that linked by label, pass that same label as the archive key to keep `ingredientIds` unchanged. -## How `instance_id` survives archiving and signing +## How instance_id survives archiving and signing ### What is an instance_id? -`instance_id` is a string field on an ingredient. It is optional in C2PA ingredient assertion starting versions 2, which the SDK currently writes by default. Version 1 required it. +`instance_id` is a string field on an ingredient. It is optional in C2PA ingredient assertion starting with version 2, which the SDK currently writes by default. Version 1 required it. In priority order, this value comes from: 1. The caller: if you set `instance_id` in the JSON passed to `add_ingredient`, that value is stored as-is. No normalization or transformation is applied. -2. XMP fallback: if no `instance_id` was provided and the source asset has `xmpMM:InstanceID` in its XMP metadata, the library reads that value and sets it on the ingredient. +2. XMP fallback: if you don't provide `instance_id` and the source asset has `xmpMM:InstanceID` in its XMP metadata, the library reads that value and sets it on the ingredient. 3. Auto-generated default: if neither caller nor XMP provided a value, the library generates `xmp.iid:` automatically (required for V1 assertion compatibility). ### Instance_id across operations -`instance_id` is kept through every archiving and signing operation this library performs. The table below covers the common paths: +This library keeps `instance_id` through every archiving and signing operation it performs. The table below covers the common paths: | Operation | `instance_id` kept? | | --- | --- | @@ -992,7 +993,7 @@ The linking label is a builder-only concept. It does not appear in `reader.json( If the archive key matches neither `label` nor `instance_id` of any ingredient on the producer builder, `write_ingredient_archive` raises immediately with `C2paError`. -#### Linking with `instance_id` only +#### Linking with instance_id only When no `label` is set, pass the `instance_id` value to `write_ingredient_archive`. Use that same string in `ingredientIds` on the signing builder. @@ -1032,11 +1033,11 @@ with open(source_path, "rb") as src, open(output_path, "w+b") as dst: consumer.sign(signer, "image/jpeg", src, dst) ``` -#### Linking with `label` only +#### Linking with label only When only `label` is set, pass the `label` value to `write_ingredient_archive`. Use that same string in `ingredientIds`. -This works even though `label` is not preserved as an ingredient field after signing. `add_ingredient_from_archive` carries the archive key in the archive's metadata and restores it as a builder-only linking key, so the action resolves to the ingredient at signing time. +This works even though signing does not preserve `label` as an ingredient field. `add_ingredient_from_archive` carries the archive key in the archive's metadata and restores it as a builder-only linking key, so the action resolves to the ingredient at signing time. Producer: @@ -1071,9 +1072,9 @@ with open(source_path, "rb") as src, open(output_path, "w+b") as dst: consumer.sign(signer, "image/jpeg", src, dst) ``` -#### Linking when both `label` and `instance_id` are set +#### Linking when both label and instance_id are set -If both `label` and `instance_id` are set on an ingredient, pass whichever value is to be used as the linking key to `write_ingredient_archive`. That string, and only that string, is what `ingredientIds` must reference on the signing builder. +If both `label` and `instance_id` are set on an ingredient, pass whichever value you want to use as the linking key to `write_ingredient_archive`. That string, and only that string, is what `ingredientIds` must reference on the signing builder. Producer (passing `label` as the key): @@ -1114,7 +1115,7 @@ with open(source_path, "rb") as src, open(output_path, "w+b") as dst: ### Catalog lookups with the read-filter-rebuild APIs -With the legacy `to_archive` + `Reader` pattern, `instance_id` survives into the Reader output and can be used to find a specific ingredient by scanning `reader.json()`: +With the legacy `to_archive` + `Reader` pattern, `instance_id` survives into the Reader output, and you can use it to find a specific ingredient by scanning `reader.json()`: ```py import json @@ -1135,11 +1136,11 @@ Using the dedicated archive API, this loop is unnecessary: each archive holds ex ## Embedded versus external manifests -By default, manifest stores are **embedded** directly into the asset file. You can also use **external** or **remote** manifest stores. +By default, the SDK **embeds** manifest stores directly into the asset file. You can also use **external** or **remote** manifest stores. ### Default: embedded manifest stores -By default, the manifest store is embedded in the output asset. +This is the default behavior: ```py ctx = Context.from_dict({"builder": {"thumbnail": {"enabled": True}}, "signer": signer}) @@ -1153,7 +1154,7 @@ with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: reader = Reader("signed.jpg", context=ctx) ``` -### External manifest stores (no embed) +### External manifest stores without embedding Call `set_no_embed()` on the `Builder` instance to prevent embedding the manifest store in the asset. For example: