1"use strict";(globalThis.webpackChunkopensource_contentauth_org=globalThis.webpackChunkopensource_contentauth_org||[]).push([[8820],{6869:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>o,contentTitle:()=>d,default:()=>h,frontMatter:()=>a,metadata:()=>t,toc:()=>c});const t=JSON.parse('{"id":"sdk-repos/c2pa-python/docs/selective-manifests","title":"Selective manifest construction","description":"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).","source":"@site/docs/sdk-repos/c2pa-python/docs/selective-manifests.md","sourceDirName":"sdk-repos/c2pa-python/docs","slug":"/sdk-repos/c2pa-python/docs/selective-manifests","permalink":"/docs/sdk-repos/c2pa-python/docs/selective-manifests","draft":false,"unlisted":false,"editUrl":"https://github.com/contentauth/c2pa-python/edit/main/docs/selective-manifests.md","tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Manifests, working stores, and archives","permalink":"/docs/sdk-repos/c2pa-python/docs/working-stores"},"next":{"title":"Python example code","permalink":"/docs/sdk-repos/c2pa-python/docs/examples"}}');var r=i(4848),s=i(8453);const a={},d="Selective manifest construction",o={},c=[{value:"Core concepts",id:"core-concepts",level:2},{value:"Reading an existing manifest",id:"reading-an-existing-manifest",level:2},{value:"Extracting binary resources",id:"extracting-binary-resources",level:3},{value:"Filtering into a new Builder",id:"filtering-into-a-new-builder",level:2},{value:"Transferring binary resources",id:"transferring-binary-resources",level:3},{value:"Keep only specific ingredients",id:"keep-only-specific-ingredients",level:3},{value:"Keep only specific assertions",id:"keep-only-specific-assertions",level:3},{value:"Start fresh and preserve provenance",id:"start-fresh-and-preserve-provenance",level:3},{value:"Adding actions to a working store",id:"adding-actions-to-a-working-store",level:2},{value:"Action JSON fields",id:"action-json-fields",level:3},{value:"Linking actions to ingredients",id:"linking-actions-to-ingredients",level:3},{value:"How ingredientIds resolution works",id:"how-ingredientids-resolution-works",level:4},{value:"Linking with label",id:"linking-with-label",level:4},{value:"Linking multiple ingredients",id:"linking-multiple-ingredients",level:5},{value:"Linking with instance_id",id:"linking-with-instance_id",level:4},{value:"Reading linked ingredients",id:"reading-linked-ingredients",level:4},{value:"When to use label vs instance_id",id:"when-to-use-label-vs-instance_id",level:4},{value:"Working with archives",id:"working-with-archives",level:2},{value:"Builder archives vs. ingredient archives",id:"builder-archives-vs-ingredient-archives",level:3},{value:"Producing an ingredient archive",id:"producing-an-ingredient-archive",level:3},{value:"The ingredients catalog pattern",id:"the-ingredients-catalog-pattern",level:3},{value:"Dedicated archives API: one ingredient per archive",id:"dedicated-archives-api-one-ingredient-per-archive",level:3},{value:"Legacy catalog: read-filter-rebuild APIs",id:"legacy-catalog-read-filter-rebuild-apis",level:3},{value:"Migration guide: catalog pattern",id:"migration-guide-catalog-pattern",level:4},{value:"Choosing between approaches",id:"choosing-between-approaches",level:4},{value:"Identifying ingredients in archives",id:"identifying-ingredients-in-archives",level:3},{value:"Overriding ingredient properties",id:"overriding-ingredient-properties",level:3},{value:"Using custom vendor parameters in actions",id:"using-custom-vendor-parameters-in-actions",level:3},{value:"Extracting ingredients from a working store",id:"extracting-ingredients-from-a-working-store",level:3},{value:"Reading ingredient details from an ingredient archive",id:"reading-ingredient-details-from-an-ingredient-archive",level:3},{value:"Linking an archived ingredient to an action",id:"linking-an-archived-ingredient-to-an-action",level:4},{value:"Troubleshooting ingredients to actions linking errors",id:"troubleshooting-ingredients-to-actions-linking-errors",level:4},{value:"Merging multiple working stores",id:"merging-multiple-working-stores",level:3},{value:"Retrieving actions from a working store",id:"retrieving-actions-from-a-working-store",level:2},{value:"Reading actions",id:"reading-actions",level:3},{value:"Reading actions from an archive",id:"reading-actions-from-an-archive",level:3},{value:"Understanding the manifest tree",id:"understanding-the-manifest-tree",level:3},{value:"Filtering actions",id:"filtering-actions",level:2},{value:"Basic action filtering",id:"basic-action-filtering",level:3},{value:"Filtering actions that reference ingredients",id:"filtering-actions-that-reference-ingredients",level:3},{value:"c2pa.opened action",id:"c2paopened-action",level:4},{value:"c2pa.placed action",id:"c2paplaced-action",level:4},{value:"Example",id:"example",level:4},{value:"Controlling manifest embedding",id:"controlling-manifest-embedding",level:2},{value:"Not embedding a manifest store into an asset",id:"not-embedding-a-manifest-store-into-an-asset",level:3},{value:"Checking manifest location on a Reader",id:"checking-manifest-location-on-a-reader",level:3}];function l(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",h
15:"h5",header:"header",li:"li",mermaid:"mermaid",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"selective-manifest-construction",children:"Selective manifest construction"})}),"\n",(0,r.jsxs)(n.p,{children:["You can use ",(0,r.jsx)(n.code,{children:"Builder"})," and ",(0,r.jsx)(n.code,{children:"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)."]}),"\n",(0,r.jsxs)(n.p,{children:["This process is best described as ",(0,r.jsx)(n.em,{children:"filtering"})," or ",(0,r.jsx)(n.em,{children:"rebuilding"})," a working store:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Read an existing manifest."}),"\n",(0,r.jsx)(n.li,{children:"Choose which elements to retain."}),"\n",(0,r.jsx)(n.li,{children:"Build a new manifest containing only those elements."}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"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)."}),"\n",(0,r.jsxs)(n.p,{children:["Since both ",(0,r.jsx)(n.code,{children:"Reader"})," and ",(0,r.jsx)(n.code,{children:"Builder"})," are ",(0,r.jsx)(n.strong,{children:"read-only"})," by design (neither has a ",(0,r.jsx)(n.code,{children:"remove()"})," method), to exclude content you must ",(0,r.jsx)(n.strong,{children:"read what exists, filter to keep what you need, and create a new"})," ",(0,r.jsx)(n.code,{children:"Builder"})," ",(0,r.jsx)(n.strong,{children:"with only that information"}),". This produces a new ",(0,r.jsx)(n.code,{children:"Builder"}),' instance: a "rebuild."']}),"\n",(0,r.jsx)(n.admonition,{type:"info",children:(0,r.jsxs)(n.p,{children:["This process always creates a new ",(0,r.jsx)(n.code,{children:"Builder"}),". The original signed asset and its manifest are never modified, neither is the starting working store. The ",(0,r.jsx)(n.code,{children:"Reader"})," extracts data without side effects, and the ",(0,r.jsx)(n.code,{children:"Builder"})," constructs a new manifest based on extracted data."]})}),"\n",(0,r.jsx)(n.h2,{id:"core-concepts",children:"Core concepts"}),"\n",(0,r.jsx)(n.mermaid,{value:"flowchart LR\n A[Signed Asset] --\x3e|Reader| B[JSON + Resources]\n B --\x3e|Filter| C[Filtered Data]\n C --\x3e|new Builder| D[New Builder]\n D --\x3e|sign| E[New Asset]"}),"\n",(0,r.jsx)(n.p,{children:"The fundamental workflow is:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Read"})," the existing manifest with ",(0,r.jsx)(n.code,{children:"Reader"})," to get JSON and binary resources"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Identify and filter"})," the parts to keep (parse the JSON, select and gather elements)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["Create a new ",(0,r.jsx)(n.code,{children:"Builder"})]})," with only the selected parts based on the applied filtering rules"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Sign"})," the new ",(0,r.jsx)(n.code,{children:"Builder"})," into the output asset"]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"reading-an-existing-manifest",children:"Reading an existing manifest"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"Reader"})," with a ",(0,r.jsx)(n.code,{children:"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 ",(0,r.jsxs)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/context-settings#with-reader",children:["Configuring ",(0,r.jsx)(n.code,{children:"Reader"})]})," and ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/context-settings#trust",children:"Trust configuration"})," for details."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "trust": {\n "user_anchors": "-----BEGIN CERTIFICATE-----\\nMIICEzCCA...\\n-----END CERTIFICATE-----",\n }
1,\n "verify": {\n "verify_trust": True\n }\n})\n\nwith open("signed_asset.jpg", "rb") as source:\n with Reader("image/jpeg", source, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active_label = manifest_store["active_manifest"]\n manifest = manifest_store["manifests"][active_label]\n'})}),"\n",(0,r.jsx)(n.h3,{id:"extracting-binary-resources",children:"Extracting binary resources"}),"\n",(0,r.jsxs)(n.p,{children:["The JSON returned by ",(0,r.jsx)(n.code,{children:"reader.json()"})," contains only string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. Extract the actual binary content by using ",(0,r.jsx)(n.code,{children:"resource_to_stream()"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'# Extract a thumbnail to an in-memory stream\nthumb_stream = io.BytesIO()\nreader.resource_to_stream(thumbnail_id, thumb_stream)\n\n# Or extract to a file\nwith open("thumbnail.jpg", "wb") as f:\n reader.resource_to_stream(thumbnail_id, f)\n'})}),"\n",(0,r.jsx)(n.h2,{id:"filtering-into-a-new-builder",children:"Filtering into a new Builder"}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["All examples on this page use ",(0,r.jsx)(n.code,{children:"Context"})," with ",(0,r.jsx)(n.code,{children:"Reader"})," and ",(0,r.jsx)(n.code,{children:"Builder"}),". For ",(0,r.jsx)(n.code,{children:"Reader"}),", the context provides trust configuration and verification settings: ",(0,r.jsx)(n.code,{children:"Reader(format, source, context=ctx)"}),". For ",(0,r.jsx)(n.code,{children:"Builder"}),", the context provides custom settings (thumbnails, claim generator, intent): ",(0,r.jsx)(n.code,{children:"Builder(manifest_json, context=ctx)"}),". When a signer is configured in the context, ",(0,r.jsx)(n.code,{children:"builder.sign()"})," is called without a signer instance. See ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/context-settings",children:"Context and settings"})," for details."]})}),"\n",(0,r.jsxs)(n.p,{children:["Each example below creates a ",(0,r.jsxs)(n.strong,{children:["new ",(0,r.jsx)(n.code,{children:"Builder"})]})," from filtered data. The original asset and its manifest store are never modified."]}),"\n",(0,r.jsxs)(n.p,{children:["When transferring ingredients from a ",(0,r.jsx)(n.code,{children:"Reader"})," to a new ",(0,r.jsx)(n.code,{children:"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 ",(0,r.jsx)(n.code,{children:"builder.add_resource()"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"transferring-binary-resources",children:"Transferring binary resources"}),"\n",(0,r.jsxs)(n.p,{children:["Since ingredients reference binary data (thumbnails, manifest stores), you need to copy those resources from the ",(0,r.jsx)(n.code,{children:"Reader"})," to the new ",(0,r.jsx)(n.code,{children:"Builder"}),". This helper function encapsulates the pattern:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'def transfer_ingredient_resources(reader, builder, ingredients):\n """Copy binary resources for a list of ingredients from reader to builder."""\n for ingredient in ingredients:\n for key in ("thumbnail", "manifest_data"):\n if key in ingredient:\n uri = ingredient[key]["identifier"]\n buf = io.BytesIO()\n reader.resource_to_stream(uri, buf)\n buf.seek(0)\n builder.add_resource(uri, buf)\n'})}),"\n",(0,r.jsx)(n.p,{children:"This function is used throughout the examples below."}),"\n",(0,r.jsx)(n.h3,{id:"keep-only-specific-ingredients",children:"Keep only specific ingredients"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\nwith open("signed_asset.jpg", "rb") as source:\n with Reader("image/jpeg", source, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active = manifest_store["manifests"][manifest_store["active_manifest"]]\n\n # Filter: keep only ingredients with a specific relationship\n kept = [\n ing for ing in active["ingredients"]\n if ing["relationship"] == "parentOf"\n ]\n\n # Create a new Builder with only the kept ingredients\n with Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "ingredients": kept,\n }, context=ctx) as new_builder:\n transfer_ingredient_resources(reader, new_builder, kept)\n\n source.seek(0)\n with open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n new_builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h3,{id:"keep-only-specific-assertions",children:"Keep only specific assertions"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\nwith open("signed_asset.jpg", "rb") as source:\n with Reader("image/jpeg", source, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active = manifest_store["manifests"][manifest_store["active_manifest"]]\n\n # Keep training-mining assertions, filter out everything else\n kept = [\n a for a in active["assertions"]\n if a["label"] == "cawg.training-mining"\n ]\n\n with Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": kept,\n }, context=ctx) as new_builder:\n source.seek(0)\n with open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n new_builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h3,{id:"start-fresh-and-preserve-provenance",children:"Start fresh and preserve provenance"}),"\n",(0,r.jsxs)(n.p,{children:["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 ",(0,r.jsx)(n.code,{children:"Builder"})," with a new manifest definition and adding the original signed asset as an ingredient using ",(0,r.jsx)(n.code,{children:"add_ingredient()"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["The function ",(0,r.jsx)(n.code,{children:"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:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"The new manifest has its own, independent set of assertions."}),"\n",(0,r.jsx)(n.li,{children:"The original's full manifest is preserved inside the ingredient, so validators can inspect the full provenance history."}),"\n",(0,r.jsx)(n.li,{children:"The provenance chain is unbroken: anyone reading the new asset can follow the ingredient link back to the original."}),"\n"]}),"\n",(0,r.jsx)(n.mermaid,{value:'flowchart TD\n subgraph Original["Original Signed Asset"]\n OA["Assertions: A, B, C"]\n OI["Ingredients: X, Y"]\n end\n subgraph NewBuilder["New Builder"]\n NA["Assertions: (empty or new)"]\n NI["Ingredient: original.jpg (contains full original manifest as binary data)"]\n end\n Original --\x3e|"add_ingredient()"| NI\n NI -.->|"validators can trace back"| Original\n\n style NA fill:#efe,stroke:#090\n style NI fill:#efe,stroke:#090'}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\nwith Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [],\n}, context=ctx) as new_builder:\n # Add the original as an ingredient to preserve provenance chain.\n # add_ingredient() stores the original\'s manifest as binary data inside\n # the ingredient, but does NOT copy the original\'s assertions.\n with open("original_signed.jpg", "rb") as original:\n new_builder.add_ingredient(\n {"title": "original.jpg", "relationship": "parentOf"},\n "image/jpeg",\n original,\n )\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a Signer explicitly.\n new_builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h2,{id:"adding-actions-to-a-working-store",children:"Adding actions to a working store"}),"\n",(0,r.jsxs)(n.p,{children:["Actions record what was done to an asset (e.g., color adju
1stments, cropping, placing content). Use ",(0,r.jsx)(n.code,{children:"builder.add_action()"})," to add them to a working store."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'builder.add_action({\n "action": "c2pa.color_adjustments",\n "parameters": {"name": "brightnesscontrast"},\n})\n\nbuilder.add_action({\n "action": "c2pa.filtered",\n "parameters": {"name": "A filter"},\n "description": "Filtering applied",\n})\n'})}),"\n",(0,r.jsx)(n.h3,{id:"action-json-fields",children:"Action JSON fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Required"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"action"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsxs)(n.td,{children:["Action identifier, e.g. ",(0,r.jsx)(n.code,{children:'"c2pa.created"'}),", ",(0,r.jsx)(n.code,{children:'"c2pa.opened"'}),", ",(0,r.jsx)(n.code,{children:'"c2pa.placed"'}),", ",(0,r.jsx)(n.code,{children:'"c2pa.color_adjustments"'}),", ",(0,r.jsx)(n.code,{children:'"c2pa.filtered"'})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"parameters"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsxs)(n.td,{children:["Free-form object with action-specific data (including ",(0,r.jsx)(n.code,{children:"ingredientIds"})," for linking ingredients, for instance)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"description"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:"Human-readable description of what happened"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"digitalSourceType"})}),(0,r.jsx)(n.td,{children:"Sometimes, depending on action"}),(0,r.jsxs)(n.td,{children:["URI describing the digital source type (typically for ",(0,r.jsx)(n.code,{children:"c2pa.created"}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"linking-actions-to-ingredients",children:"Linking actions to ingredients"}),"\n",(0,r.jsxs)(n.p,{children:["When an action involves a specific ingredient, the ingredient is linked to the action using ",(0,r.jsx)(n.code,{children:"ingredientIds"})," (in the action's ",(0,r.jsx)(n.code,{children:"parameters"}),"), referencing a matching key in the ingredient."]}),"\n",(0,r.jsx)(n.h4,{id:"how-ingredientids-resolution-works",children:"How ingredientIds resolution works"}),"\n",(0,r.jsxs)(n.p,{children:["The SDK matches each value in ",(0,r.jsx)(n.code,{children:"ingredientIds"})," against ingredients using this priority:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"label"})," on the ingredient (primary): if set and non-empty, this is used as the linking key."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"instance_id"})," on the ingredient (fallback): used when ",(0,r.jsx)(n.code,{children:"label"})," is absent or empty."]}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"linking-with-label",children:"Linking with label"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"label"})," field on an ingredient is the ",(0,r.jsx)(n.strong,{children:"primary"})," linking key. Set a ",(0,r.jsx)(n.code,{children:"label"})," on the ingredient and reference it in the action's ",(0,r.jsx)(n.code,{children:"ingredientIds"}),". The label can be any string: it acts as a linking key between the ingredient and the action."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\nmanifest_json = {\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [\n {\n "label": "c2pa.actions.v2",\n "data": {\n "actions": [\n {\n "action": "c2pa.created",\n "digitalSourceType": "http://cv.iptc.org/newscodes/
1digitalsourcetype/digitalCreation",\n },\n {\n "action": "c2pa.placed",\n "parameters": {\n "ingredientIds": ["c2pa.ingredient.v3"]\n },\n },\n ]\n },\n }\n ],\n}\n\nwith Builder(manifest_json, context=ctx) as builder:\n # The label on the ingredient matches the value in ingredientIds\n with open("photo.jpg", "rb") as photo:\n builder.add_ingredient(\n {\n "title": "photo.jpg",\n "format": "image/jpeg",\n "relationship": "componentOf",\n "label": "c2pa.ingredient.v3",\n },\n "image/jpeg",\n photo,\n )\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h5,{id:"linking-multiple-ingredients",children:"Linking multiple ingredients"}),"\n",(0,r.jsx)(n.p,{children:"When linking multiple ingredients, each ingredient needs a unique label."}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsx)(n.p,{children:"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."})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\nmanifest_json = {\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [\n {\n "label": "c2pa.actions.v2",\n "data": {\n "actions": [\n {\n "action": "c2pa.opened",\n "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",\n "parameters": {\n "ingredientIds": ["c2pa.ingredient.v3_1"]\n },\n },\n {\n "action": "c2pa.placed",\n "parameters": {\n "ingredientIds": ["c2pa.ingredient.v3_2"]\n },\n },\n ]\n },\n }\n ],\n}\n\nwith Builder(manifest_json, context=ctx) as builder:\n # parentOf ingredient linked to c2pa.opened\n with open("original.jpg", "rb") as original:\n builder.add_ingredient(\n {\n "title": "original.jpg",\n "format": "image/jpeg",\n "relationship": "parentOf",\n "label": "c2pa.ingredient.v3_1",\n },\n "image/jpeg",\n original,\n )\n\n # componentOf ingredient linked to c2pa.placed\n with open("overlay.jpg", "rb") as overlay:\n builder.add_ingredient(\n {\n "title": "overlay.jpg",\n "format": "image/jpeg",\n "relationship": "componentOf",\n "label": "c2pa.ingredient.v3_2",\n },\n "image/jpeg",\n overlay,\n )\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h4,{id:"linking-with-instance_id",children:"Linking with instance_id"}),"\n",(0,r.jsxs)(n.p,{children:["When no ",(0,r.jsx)(n.code,{children:"label"})," is set on an ingredient, the SDK matches ",(0,r.jsx)(n.code,{children:"ingredientIds"})," against ",(0,r.jsx)(n.code,{children:"instance_id"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\n# instance_id is used as the linking identifier and must be unique\ninstance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f"\n\nmanifest_json = {\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [\n {\n "label": "c2pa.actions",\n "data": {\n "actions": [\n {\n "action": "c2pa.opened",\n "parameters": {\n "ingredientIds": [instance_id]\n },\n }\n ]\n },\n }\n ],\n}\n\nwith Builder(manifest_json, context=ctx) as builder:\n # No label set: instance_id is used as the linking key\n with open("source_photo.jpg", "rb") as photo:\n builder.add_ingredient(\n {\n "title": "source_photo.jpg",\n "relationship": "parentOf",\n "instance_id": instance_id,\n },\n "image/jpeg",\n photo,\n )\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"instance_id"})," can be read back from the ingredient JSON after signing."]})}),"\n",(0,r.jsx)(n.h4,{id:"reading-linked-ingredients",children:"Reading linked ingredients"}),"\n",(0,r.jsxs)(n.p,{children:["After signing, ",(0,r.jsx)(n.code,{children:"ingredientIds"})," is gone. The action's ",(0,r.jsx)(n.code,{children:"parameters.ingredients[]"})," contains hashed JUMBF URIs pointing to ingredient assertions. To match an action to its ingredient, extract the label from the URL:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({"verify": {"verify_trust": True}})\n\nwith open("signed_asset.jpg", "rb") as signed:\n with Reader("image/jpeg", signed, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active_label = manifest_store["active_manifest"]\n manifest = manifest_store["manifests"][active_label]\n\n # Build a map: label -> ingredient\n label_to_ingredient = {\n ing["label"]: ing for ing in manifest["ingredients"]\n }\n\n # Match each action to its ingredients by extracting labels from URLs\n for assertion in manifest["assertions"]:\n if assertion["label"] != "c2pa.actions.v2":\n continue\n for action in assertion["data"]["actions"]:\n for ref in action.get("parameters", {}).get("ingredients", []):\n label = ref["url"].rsplit("/", 1)[-1]\n matched = label_to_ingredient.get(label)\n # matched is the ingredient linked to this action\n'})}),"\n",(0,r.jsx)(n.h4,{id:"when-to-use-label-vs-instance_id",children:"When to use label vs instance_id"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"label"})}),(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"instance_id"})})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Who controls it"})}),(0,r.jsx)(n.td,{children:"Caller (any string)"}),(0,r.jsx)(n.td,{children:"Caller (any string, or from XMP metadata)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Priority for linking"})}),(0,r.jsx)(n.td,{children:"Primary: checked first"}),(0,r.jsx)(n.td,{children:"Fallback: used when label is absent/empty"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"When to use"})}),(0,r.jsx)(n.td,{children:"JSON-defined manifests where the caller controls the ingredient definition"}),(0,r.jsx)(n.td,{children:"Programmatic workflows where a stable identifier persisting unchanged across rebuilds is needed"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Survives signing"})}),(0,r.jsx)(n.td,{children:"SDK may reassign the actual assertion label"}),(0,r.jsx)(n.td,{children:"Unchanged"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Stable across rebuilds"})}),(0,r.jsx)(n.td,{children:"The caller controls the build-time value; the post-signing label may change"}),(0,r.jsx)(n.td,{children:"Yes, always the same set value"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Use ",(0,r.jsx)(n.code,{children:"label"})]})," when defining manifests in JSON.\n",(0,r.jsxs)(n.strong,{children:["Use ",(0,r.jsx)(n.code,{children:"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."]}),"\n",(0,r.jsx)(n.h2,{id:"working-with-archives",children:"Working with archives"}),"\n",(0,r.jsxs)(n.p,{children:["A ",(0,r.jsx)(n.code,{children:"Builder"})," represents a ",(0,r.jsx)(n.strong,{children:"working store"}),": a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a ",(0,r.jsx)(n.code,{children:".c2pa"})," binary format, allowing you to save, transfer, or resume the work later. For more background on working stores and archives, see ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/working-stores",children:"Working stores and archives"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working store archives) and ingredient archives."}),"\n",(0,r.jsx)(n.h3,{id:"builder-archives-vs-ingredient-archives",children:"Builder archives vs. ingredient archives"}),"\n",(0,r.jsxs)(n.p,{children:["A ",(0,r.jsx)(n.strong,{children:"builder archive"})," (also called a working store archive) is a serialized snapshot of a ",(0,r.jsx)(n.code,{children:"Builder"}),". It contains the manifest definition, all resources, and any ingredients that were added. It is created by ",(0,r.jsx)(n.code,{children:"builder.to_archive()"})," and restored with ",(0,r.jsx)(n.code,{children:"Builder.from_archive()"})," to create a new builder instance from an archive, or ",(0,r.jsx)(n.code,{children:"builder.with_archive()"})," to load a working store from a builder archive into an existing builder instance."]}),"\n",(0,r.jsxs)(n.p,{children:["An ",(0,r.jsx)(n.strong,{children:"ingredient archive"})," contains the manifest store from an asset that was added as an ingredient."]}),"\n",(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.h3,{id:"producing-an-ingredient-archive",children:"Producing an ingredient archive"}),"\n",(0,r.jsxs)(n.p,{children:["The SDK supports two approaches for producing an ingredient archive. They share the same ",(0,r.jsx)(n.code,{children:".c2pa"})," binary format and are interchangeable from the consumer side."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Approach"}),(0,r.jsx)(n.th,{children:"Entry point"}),(0,r.jsx)(n.th,{children:"Status"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Dedicated ingredient archive APIs"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"add_ingredient"})," then ",(0,r.jsx)(n.code,{children:"write_ingredient_archive(id, stream)"})]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Recommended"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Read-filter-rebuild pattern"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"Builder"})," + ",(0,r.jsx)(n.code,{children:"add_ingredient"})," + ",(0,r.jsx)(n.code,{children:"to_archive"})]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Older pattern"})})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["For the full contract, see ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/working-stores#single-ingredient-archive-apis",children:"Single-ingredient archive APIs"})," in the working stores guide."]}),"\n",(0,r.jsx)(n.h3,{id:"the-ingredients-catalog-pattern",children:"The ingredients catalog pattern"}),"\n",(0,r.jsxs)(n.p,{children:["An ",(0,r.jsx)(n.strong,{children:"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."]}),"\n",(0,r.jsx)(n.mermaid,{value:'flowchart TD\n subgraph Catalog["Ingredients Catalog (archived)"]\n A1["Archive: photos.c2pa (ingredients from photo shoot)"]\n A2["Archive: graphics.c2pa (ingredients from design assets)"]\n A3["Archive: audio.c2pa (ingredients from audio tracks)"]\n end\n CTX["Context (to propagate settings and configuration)"]\n subgraph Build["Final Builder"]\n direction TB\n SEL["Pick and choose ingredients from any archive in the catalog"]\n FB["New Builder with selected ingredients only"]\n end\n A1 --\x3e|"select photo_1, photo_3"| SEL\n A2 --\x3e|"select logo"| SEL\n A3 -. "skip (not needed)" .-> X((not used))\n CTX -.->|"settings"| FB\n SEL --\x3e FB\n FB --\x3e|sign| OUT[Signed Output Asset]\n\n style A3 fill:#eee,stroke:#999\n style X fill:#f99,stroke:#c00\n style
1CTX fill:#e8f4fd,stroke:#4a90d9'}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {\n "thumbnail": {"enabled": False},\n "claim_generator_info": {"name": "an-application", "version": "0.1.0"}\n },\n "signer": signer,\n})\n\narchive_stream.seek(0)\nwith Reader("application/c2pa", archive_stream, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active = manifest_store["manifests"][manifest_store["active_manifest"]]\n\n selected = [\n ing for ing in active["ingredients"]\n if ing["title"] in {"photo_1.jpg", "logo.png"}\n ]\n\n with Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "ingredients": selected,\n }, context=ctx) as new_builder:\n transfer_ingredient_resources(reader, new_builder, selected)\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n new_builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h3,{id:"dedicated-archives-api-one-ingredient-per-archive",children:"Dedicated archives API: one ingredient per archive"}),"\n",(0,r.jsxs)(n.p,{children:["The producer registers each ingredient on a builder and writes one archive per ingredient, keyed by ",(0,r.jsx)(n.code,{children:"instance_id"})," as unique identifier. The consumer assembles a final Builder instance by loading only the archives it needs via ",(0,r.jsx)(n.code,{children:"add_ingredient_from_archive"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["The first argument to ",(0,r.jsx)(n.code,{children:"write_ingredient_archive"})," is the ",(0,r.jsx)(n.em,{children:"archive key"}),": it locates the ingredient on the producer (matched against either ",(0,r.jsx)(n.code,{children:"label"})," or ",(0,r.jsx)(n.code,{children:"instance_id"}),") and becomes the ",(0,r.jsx)(n.code,{children:"ingredientIds"})," value to use on the signing builder. See ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/working-stores#lookup-keys-and-action-linking",children:"Lookup keys and action linking"})," for the full rules."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:'"relationship": "componentOf"'})," is shown explicitly below, but ",(0,r.jsx)(n.code,{children:"componentOf"})," is the default the SDK applies when ",(0,r.jsx)(n.code,{children:"relationship"})," is omitted."]})}),"\n",(0,r.jsx)(n.p,{children:"Producer side, build the catalog:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'import io\nfrom c2pa import Builder\n\ncatalog_builder = Builder.from_json(manifest_json)\nwith open("photo-A.jpg", "rb") as f:\n catalog_builder.add_ingredient(\n {"title": "photo-A.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-A"},\n "image/jpeg", f\n )\nwith open("photo-B.jpg", "rb") as f:\n catalog_builder.add_ingredient(\n {"title": "photo-B.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-B"},\n "image/jpeg", f\n )\nwith open("photo-C.jpg", "rb") as f:\n catalog_builder.add_ingredient(\n {"title": "photo-C.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-C"},\n "image/jpeg", f\n )\n\n# One archive per ingredient, keyed by the instance_id used at registration.\narchive_a, archive_b, archive_c = io.BytesIO(), io.BytesIO(), io.BytesIO()\ncatalog_builder.write_ingredient_archive("catalog:ingredient-A", archive_a)\ncatalog_builder.write_ingredient_archive("catalog:ingredient-B", archive_b)\ncatalog_builder.write_ingredient_archive("catalog:ingredient-C", archive_c)\n'})}),"\n",(0,r.jsx)(n.p,{children:"Consumer side, pick one archive and load it:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'final_builder = Builder.from_json(manifest_json)\narchive_b.seek(0)\nfinal_builder.add_ingredient_from_archive(archive_b)\n\nwith open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst:\n final_builder.sign(signer, "image/jpeg", src, dst)\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The signed output contains exactly the picked ingredient (",(0,r.jsx)(n.code,{children:"photo-B.jpg"})," here). ",(0,r.jsx)(n.code,{children:"archive_a"})," stays unused."]}),"\n",(0,r.jsxs)(n.p,{children:["A single action can link several ingredients loaded this way. With the three archives from the producer above, a ",(0,r.jsx)(n.code,{children:"c2pa.placed"})," action that lists all three ids in ",(0,r.jsx)(n.code,{children:"ingredientIds"})," resolves to three distinct ingredient URLs after signing:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'signing_manifest = {\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [{\n "label": "c2pa.actions.v2",\n "data": {\n "actions": [{\n "action": "c2pa.placed",\n "parameters": {\n "ingredientIds": ["catalog:ingredient-A", "catalog:ingredient-
1B", "catalog:ingredient-C"]\n }\n }]\n }\n }]\n}\n\nsigning_builder = Builder.from_json(signing_manifest)\nfor archive in (archive_a, archive_b, archive_c):\n archive.seek(0)\n signing_builder.add_ingredient_from_archive(archive)\n\nwith open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst:\n signing_builder.sign(signer, "image/jpeg", src, dst)\n'})}),"\n",(0,r.jsx)(n.h3,{id:"legacy-catalog-read-filter-rebuild-apis",children:"Legacy catalog: read-filter-rebuild APIs"}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Legacy approach."})," This pattern requires manual JSON parsing and ",(0,r.jsx)(n.code,{children:"add_resource"})," loops to transfer binary data related to ingredients. See ",(0,r.jsx)(n.a,{href:"#migration-guide-catalog-pattern",children:"Migration guide"})," to use the ",(0,r.jsx)(n.a,{href:"#dedicated-archives-api-one-ingredient-per-archive",children:"dedicated ingredient archive APIs"})," instead."]})}),"\n",(0,r.jsxs)(n.p,{children:["Use this approach when the catalog already exists as a single ",(0,r.jsx)(n.code,{children:".c2pa"})," builder archive containing many ingredients and you need to pick a subset by reading, filtering, and rebuilding."]}),"\n",(0,r.jsx)(n.h4,{id:"migration-guide-catalog-pattern",children:"Migration guide: catalog pattern"}),"\n",(0,r.jsxs)(n.p,{children:["Switch to the dedicated ingredient archive APIs: set ",(0,r.jsx)(n.code,{children:"instance_id"})," per ingredient, call ",(0,r.jsx)(n.code,{children:"write_ingredient_archive"})," once per ingredient on the producer, and ",(0,r.jsx)(n.code,{children:"add_ingredient_from_archive"})," on the consumer. No JSON parsing or ",(0,r.jsx)(n.code,{children:"add_resource"})," loops required."]}),"\n",(0,r.jsx)(n.p,{children:"Producer side:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'catalog_builder = Builder.from_json(manifest_json)\nwith open("photo-A.jpg", "rb") as f:\n catalog_builder.add_ingredient(\n {"title": "photo-A.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-A"},\n "image/jpeg", f\n )\nwith open("photo-B.jpg", "rb") as f:\n catalog_builder.add_ingredient(\n {"title": "photo-B.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-B"},\n "image/jpeg", f\n )\n\narchive_a, archive_b = io.BytesIO(), io.BytesIO()\ncatalog_builder.write_ingredient_archive("catalog:ingredient-A", archive_a)\ncatalog_builder.write_ingredient_archive("catalog:ingredient-B", archive_b)\n'})}),"\n",(0,r.jsx)(n.p,{children:"Consumer side:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'final_builder = Builder.from_json(manifest_json)\narchive_b.seek(0)\nfinal_builder.add_ingredient_from_archive(archive_b)\nwith open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst:\n final_builder.sign(signer, "image/jpeg", src, dst)\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Action linking also changes between the two approaches. Legacy catalog code linked ingredients via ",(0,r.jsx)(n.code,{children:"label"})," set on the signing builder's ",(0,r.jsx)(n.code,{children:"add_ingredient"})," JSON; ",(0,r.jsx)(n.code,{children:"instance_id"})," was not accepted. The dedicated archive API accepts the archive key passed to ",(0,r.jsx)(n.code,{children:"write_ingredient_archive"}),", which can be either ",(0,r.jsx)(n.code,{children:"label"})," or ",(0,r.jsx)(n.code,{children:"instance_id"}),". See ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/working-stores#lookup-keys-and-action-linking",children:"Lookup keys and action linking"}),"."]}),"\n",(0,r.jsx)(n.h4,{id:"choosing-between-approaches",children:"Choosing between approaches"}),"\n",(0,r.jsxs)(n.p,{children:["The legacy read-filter-rebuild APIs fit when the catalog already exists as a single ",(0,r.jsx)(n.code,{children:".c2pa"})," builder archive that bundles all ingredients together and the consumer wants a subset, picked via ",(0,r.jsx)(n.code,{children:"Reader"})," + manual JSON filtering. The dedicated ingredient archive APIs fit when ingredients are produced and consumed
1independently: each ingredient gets its own archive, so no Reader-based filtering is needed. Both produce the same signed output."]}),"\n",(0,r.jsx)(n.h3,{id:"identifying-ingredients-in-archives",children:"Identifying ingredients in archives"}),"\n",(0,r.jsxs)(n.p,{children:["When building an ingredient archive, you can set ",(0,r.jsx)(n.code,{children:"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 ",(0,r.jsx)(n.code,{children:"description"})," and ",(0,r.jsx)(n.code,{children:"informational_URI"})," fields also survive and can carry additional metadata about the ingredient's origin."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"instance_id"})," is only for identification and catalog lookups. It cannot be used as a linking key in ",(0,r.jsx)(n.code,{children:"ingredientIds"})," when linking ingredient archives to actions: use ",(0,r.jsx)(n.code,{children:"label"})," for that (see ",(0,r.jsx)(n.a,{href:"#linking-an-archived-ingredient-to-an-action",children:"Linking an archived ingredient to an action"}),")."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'# Set instance_id when adding the ingredient to the archive builder.\nbuilder = Builder.from_json(manifest_json)\nwith open("photo-A.jpg", "rb") as f:\n builder.add_ingredient(\n {\n "title": "photo-A.jpg",\n "relationship": "componentOf",\n "instance_id": "catalog:photo-A",\n },\n "image/jpeg",\n f,\n )\n\narchive = io.BytesIO()\nbuilder.to_archive(archive)\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Later, when reading the archive, select ingredients by their ",(0,r.jsx)(n.code,{children:"instance_id"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'archive.seek(0)\nreader = Reader("application/c2pa", archive)\nmanifest_data = json.loads(reader.json())\nactive = manifest_data["active_manifest"]\ningredients = manifest_data["manifests"][active]["ingredients"]\n\nfor ing in ingredients:\n if ing.get("instance_id") == "catalog:photo-A":\n # Do something with the found ingredient...\n pass\n'})}),"\n",(0,r.jsx)(n.h3,{id:"overriding-ingredient-properties",children:"Overriding ingredient properties"}),"\n",(0,r.jsxs)(n.p,{children:["When adding an ingredient from an archive or from a file, the JSON passed to ",(0,r.jsx)(n.code,{children:"add_ingredient()"})," can override properties like ",(0,r.jsx)(n.code,{children:"title"})," and ",(0,r.jsx)(n.code,{children:"relationship"}),". This is useful when reusing archived ingredients in a different context:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'with open("signed_asset.jpg", "rb") as signed:\n builder.add_ingredient(\n {\n "title": "my-custom-title.jpg",\n "relationship": "parentOf",\n "instance_id": "my-tracking-id:asset-example-id",\n },\n "image/jpeg",\n signed,\n )\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"title"}),", ",(0,r.jsx)(n.code,{children:"relationship"}),", and ",(0,r.jsx)(n.code,{children:"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, ",(0,r.jsx)(n.code,{children:".c2pa"})," archives, or unsigned files."]}),"\n",(0,r.jsx)(n.h3,{id:"using-custom-vendor-parameters-in-actions",children:"Using custom vendor parameters in actions"}),"\n",(0,r.jsxs)(n.p,{children:["The C2PA specification allows ",(0,r.jsx)(n.strong,{children:"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."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'manifest_json = {\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [\n {\n "label": "c2pa.actions.v2",\n "data": {\n "actions": [\n {\n "action": "c2pa.created",\n "digitalSourceType": "http://cv.iptc.org/newscodes/
1digitalsourcetype/compositeCapture",\n "parameters": {\n "com.mycompany.tool": "my-editor",\n "com.mycompany.session_id": "session-abc-123",\n },\n },\n {\n "action": "c2pa.placed",\n "description": "Placed an image",\n "parameters": {\n "com.mycompany.layer_id": "layer-42",\n "ingredientIds": ["c2pa.ingredient.v3"],\n },\n },\n ]\n },\n }\n ],\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"After signing, these custom parameters appear alongside the standard fields:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "action": "c2pa.placed",\n "parameters": {\n "com.mycompany.layer_id": "layer-42",\n "ingredients": [{"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"}]\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Custom vendor parameters can be used to filter actions. For example, to find all actions related to a specific layer:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'layer_actions = [\n action for action in actions\n if action.get("parameters", {}).get("com.mycompany.layer_id") == "layer-42"\n]\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Vendor parameters must use reverse domain notation with period-separated components (for example, ",(0,r.jsx)(n.code,{children:"com.mycompany.tool"}),", ",(0,r.jsx)(n.code,{children:"net.example.session_id"}),"). Some namespaces (for example, ",(0,r.jsx)(n.code,{children:"c2pa"})," or ",(0,r.jsx)(n.code,{children:"cawg"}),") may be reserved."]})}),"\n",(0,r.jsx)(n.h3,{id:"extracting-ingredients-from-a-working-store",children:"Extracting ingredients from a working store"}),"\n",(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.mermaid,{value:'flowchart TD\n subgraph Step1["Step 1: Build a working store with ingredients"]\n IA["add_ingredient(A.jpg)"] --\x3e B1[Builder]\n IB["add_ingredient(B.jpg)"] --\x3e B1\n B1 --\x3e|"to_archive()"| AR["archive.c2pa"]\n end\n subgraph Step2["Step 2: Extract ingredients from archive"]\n AR --\x3e|"Reader(application/c2pa)"| RD[JSON + resources]\n RD --\x3e|"pick ingredients"| SEL[Selected ingredients]\n end\n CTX["Context (optional)"]\n subgraph Step3["Step 3: Reuse in a new Builder"]\n SEL --\x3e|"new Builder + add_resource()"| B2[New Builder]\n CTX -.->|"settings"| B2\n B2 --\x3e|sign| OUT[Signed Output]\n end\n\n style CTX fill:#e8f4fd,stroke:#4a90d9'}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Step 1:"})," Build a working store and archive it:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n})\n\nwith Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n}, context=ctx) as builder:\n # Add ingredients to the working store\n with open("A.jpg", "rb") as ing_a:\n builder.add_ingredient(\n {"title": "A.jpg", "relationship": "componentOf"},\n "image/jpeg",\n ing_a,\n )\n\n with open("B.jpg", "rb") as ing_b:\n builder.add_ingredient(\n {"title": "B.jpg", "relationship": "componentOf"},\n "image/jpeg",\n ing_b,\n )\n\n # Save the working store as an archive\n archive_stream = io.BytesIO()\n builder.to_archive(archive_stream)\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["When restoring from an archive, ",(0,r.jsx)(n.code,{children:"with_archive()"})," preserves context settings while ",(0,r.jsx)(n.code,{children:"from_archive()"})," does not. See ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/working-stores#working-with-archives",children:"Working with archives"})," for the full comparison."]})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Step 2:"})," Read the archive and extract ingredients:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'archive_stream.seek(0)\nwith Reader("application/c2pa", archive_stream, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active = manifest_store["manifests"][manifest_store["active_manifest"]]\n ingredients = active["ingredients"]\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Step 3:"})," Create a new Builder with the extracted ingredients:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:' sign_ctx = Context.from_dict({\n "builder": {\n "thumbnail": {"enabled": False},\n "claim_generator_info": {"name": "an-application", "version": "0.1.0"}\n },\n "signer": signer,\n })\n\n selected = [ing for ing in ingredients if ing["title"] == "A.jpg"]\n\n with Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "ingredients": selected,\n }, context=sign_ctx) as new_builder:\n transfer_ingredient_resources(reader, new_builder, selected)\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n new_builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h3,{id:"reading-ingredient-details-from-an-ingredient-archive",children:"Reading ingredient details from an ingredient archive"}),"\n",(0,r.jsxs)(n.p,{children:["An ingredient archive is a serialized ",(0,r.jsx)(n.code,{children:"Builder"})," containing exactly one ingredient (see ",(0,r.jsx)(n.a,{href:"#builder-archives-vs-ingredient-archives",children:"Builder archives vs. ingredient archives"}),"). Reading it with ",(0,r.jsx)(n.code,{children:"Reader"})," allows the caller to inspect the ingredient before deciding whether to use it: its thumbnail, whether it carries provenance (e.g. an active manifest), validation status, relationship, etc."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'# Open the ingredient archive.\nwith open("ingredient_archive.c2pa", "rb") as archive_file:\n reader = Reader("application/c2pa", archive_file, context=ctx)\n parsed = json.loads(reader.json())\n active = parsed["active_manifest"]\n manifest = parsed["manifests"][active]\n\n # An ingredient archive has exactly one ingredient.\n ingredient = manifest["ingredients"][0]\n\n # Relationship e.g. "parentOf", "componentOf", "inputTo".\n relationship = ingredient["relationship"]\n\n # Instance ID (optional, can be set by caller).\n instance_id = ingredient.get("instance_id")\n\n # Active manifest:\n # When present, the ingredient had content credentials itself.\n if "active_manifest" in ingredient:\n ing_manifest_label = ingredient["active_manifest"]\n ing_manifest = parsed["manifests"][ing_manifest_label]\n # ing_manifest contains the ingredient\'s own assertions, actions, etc.\n\n # Validation status.\n # The top-level "validation_status" array covers the entire manifest store,\n # including this ingredient\'s manifest.\n if "validation_status" in parsed:\n for status in parsed["validation_status"]:\n print(f"{status[\'code\']}: {status[\'explanation\']}
1")\n\n # Thumbnail\n if "thumbnail" in ingredient:\n thumb_id = ingredient["thumbnail"]["identifier"]\n with open("thumbnail.jpg", "wb") as thumb_file:\n reader.resource_to_stream(thumb_id, thumb_file)\n\n reader.close()\n'})}),"\n",(0,r.jsx)(n.h4,{id:"linking-an-archived-ingredient-to-an-action",children:"Linking an archived ingredient to an action"}),"\n",(0,r.jsxs)(n.p,{children:["After reading the ingredient details from an ingredient archive, the ingredient can be added to a new ",(0,r.jsx)(n.code,{children:"Builder"})," and linked to an action. You must assign a ",(0,r.jsx)(n.code,{children:"label"})," in the ",(0,r.jsx)(n.code,{children:"add_ingredient()"})," call on the signing builder and use that label as the linking key in ",(0,r.jsx)(n.code,{children:"ingredientIds"}),". Labels baked into the archive ingredient are not carried through, and ",(0,r.jsx)(n.code,{children:"instance_id"})," does not work as a linking key for ingredient archives."]}),"\n",(0,r.jsx)(n.p,{children:"Labels are only used as build-time linking keys. The SDK may reassign the actual label in the signed manifest."}),"\n",(0,r.jsxs)(n.p,{children:["Assign a ",(0,r.jsx)(n.code,{children:"label"})," in the ",(0,r.jsx)(n.code,{children:"add_ingredient()"})," call and reference that same label in ",(0,r.jsx)(n.code,{children:"ingredientIds"})," to link an ingredient to an action."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\n# Read the ingredient archive.\nwith open("ingredient_archive.c2pa", "rb") as archive_file:\n reader = Reader("application/c2pa", archive_file, context=ctx)\n parsed = json.loads(reader.json())\n active = parsed["active_manifest"]\n ingredient = parsed["manifests"][active]["ingredients"][0]\n\n # Use a label as the linking key.\n # Any label can be used, as long as it uniquely identifies the link.\n manifest_json = {\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "assertions": [\n {\n "label": "c2pa.actions.v2",\n "data": {\n "actions": [\n {\n "action": "c2pa.opened",\n "parameters": {\n "ingredientIds": ["archived-ingredient"]\n },\n }\n ]\n },\n }\n ],\n }\n\n with Builder(manifest_json, context=ctx) as builder:\n # The label on the ingredient must match the entry in ingredientIds on the action.\n archive_file.seek(0)\n builder.add_ingredient(\n {\n "title": ingredient["title"],\n "relationship": "parentOf",\n "label": "archived-ingredient",\n },\n "application/c2pa",\n archive_file,\n )\n\n with open("source.jpg", "rb") as source, open("output.jpg", "w+b") as dest:\n builder.sign("image/jpeg", source, dest)\n\n reader.close()\n'})}),"\n",(0,r.jsx)(n.h4,{id:"troubleshooting-ingredients-to-actions-linking-errors",children:"Troubleshooting ingredients to actions linking errors"}),"\n",(0,r.jsx)(n.p,{children:"A signing-time error when linking ingredients to actions failed is:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"Builder.sign failure: Other: assertion-specific error:\nAction ingredientId not found: <some id>\n"})}),"\n",(0,r.jsx)(n.p,{children:"Causes and potential fixes to investigate:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Symptom"}),(0,r.jsx)(n.th,{children:"Cause"}),(0,r.jsx)(n.th,{children:"Fix"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"Action ingredientId not found: xmp:iid:..."})," (or any ",(0,r.jsx)(n.code,{children:"instance_id"})," value)"]}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"instance_id"})," was used as the linking key for an ingredient archive loaded via the legacy path."]}),(0,r.jsxs)(n.td,{children:["Assign a ",(0,r.jsx)(n.code,{children:"label"})," on the signing builder's ",(0,r.jsx)(n.code,{children:"add_ingredient"})," JSON and use that label in ",(0,r.jsx)(n.code,{children:"ingredientIds"}),"."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"Action ingredientId not found: <label>"})," where the label was set only when building the archive"]}),(0,r.jsx)(n.td,{children:"Labels baked into an archive ingredient do not carry through as linking keys for the legacy load path."}),(0,r.jsxs)(n.td,{children:["Re-assert the same ",(0,r.jsx)(n.code,{children:"label"})," in the signing builder's ",(0,r.jsx)(n.code,{children:"add_ingredient"})," JSON."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"Action ingredientId not found: <label>"})," where the label is on ",(0,r.jsx)(n.code,{children:"add_ingredient"})," but the action references a different string"]}),(0,r.jsxs)(n.td,{children:["Typo or mismatch between ",(0,r.jsx)(n.code,{children:"ingredientIds[i]"})," and the ",(0,r.jsx)(n.code,{children:"label"})," field on the ingredient."]}),(0,r.jsx)(n.td,{children:"Make the two strings identical."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Sign succeeds but the action's ",(0,r.jsx)(n.code,{children:"parameters.ingredients"})," array is empty in the signed output"]}),(0,r.jsx)(n.td,{children:"The action was kept during a filter/rebuild but the corresponding ingredient was not, or was not linked."}),(0,r.jsx)(n.td,{children:"Keep the ingredient and its binary resources alongside the action. Verify that the linking of ingredients and actions uses the correct JSON attributes."})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["For the linking rules, see ",(0,r.jsx)(n.a,{href:"#linking-an-archived-ingredient-to-an-action",children:"Linking an archived ingredient to an action"})," above."]}),"\n",(0,r.jsx)(n.h3,{id:"merging-multiple-working-stores",children:"Merging multiple working stores"}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"Builder"})," construction and signing in the merge workflow also support ",(0,r.jsx)(n.code,{children:"Context"}),". The caller can pass ",(0,r.jsx)(n.code,{children:"context=ctx"})," to ",(0,r.jsx)(n.code,{children:"Builder()"})," and call ",(0,r.jsx)(n.code,{children:"sign()"})," without a signer argument when the context has one. See ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/context-settings",children:"Context and settings"})," for details."]})}),"\n",(0,r.jsxs)(n.p,{children:["In some cases it is necessary to merge ingredients from multiple working stores (builder archives) into a single ",(0,r.jsx)(n.code,{children:"Builder"}),". This should be a ",(0,r.jsx)(n.strong,{children:"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."]}),"\n",(0,r.jsxs)(n.p,{children:["When each source contributes a single ingredient, the dedicated single-ingredient API
1sidesteps this collision case: each archive holds exactly one ingredient, and ",(0,r.jsx)(n.code,{children:"add_ingredient_from_archive"})," registers it on the consuming builder (see ",(0,r.jsx)(n.a,{href:"/docs/sdk-repos/c2pa-python/docs/working-stores#single-ingredient-archive-apis",children:"Single-ingredient archive APIs"}),"). The two-pass approach below remains the right tool when sources hold multiple ingredients each and a full merge is required."]}),"\n",(0,r.jsxs)(n.p,{children:["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 ",(0,r.jsx)(n.code,{children:"resource_to_stream()"}),", renamed ID for ",(0,r.jsx)(n.code,{children:"add_resource()"})," when collisions occurred)."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\n\nused_ids: set[str] = set()\nsuffix_counter = 0\nall_ingredients = []\narchive_ingredient_counts = []\n\n# Pass 1: Collect ingredients, renaming IDs on collision\nfor archive_stream in archives:\n archive_stream.seek(0)\n with Reader("application/c2pa", archive_stream, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active = manifest_store["manifests"][manifest_store["active_manifest"]]\n ingredients = active["ingredients"]\n\n for ingredient in ingredients:\n for key in ("thumbnail", "manifest_data"):\n if key not in ingredient:\n continue\n uri = ingredient[key]["identifier"]\n if uri in used_ids:\n suffix_counter += 1\n ingredient[key]["identifier"] = f"{uri}__{suffix_counter}"\n used_ids.add(ingredient[key]["identifier"])\n all_ingredients.append(ingredient)\n\n archive_ingredient_counts.append(len(ingredients))\n\nwith Builder({\n "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],\n "ingredients": all_ingredients,\n}, context=ctx) as builder:\n # Pass 2: Transfer resources (match by ingredient index)\n offset = 0\n for archive_stream, count in zip(archives, archive_ingredient_counts):\n archive_stream.seek(0)\n with Reader("application/c2pa", archive_stream, context=ctx) as reader:\n manifest_store = json.loads(reader.json())\n active = manifest_store["manifests"][manifest_store["active_manifest"]]\n originals = active["ingredients"]\n\n for original, merged in zip(originals, all_ingredients[offset:offset + count]):\n for key in ("thumbnail", "manifest_data"):\n if key not in original:\n continue\n buf = io.BytesIO()\n reader.resource_to_stream(original[key]["identifier"], buf)\n buf.seek(0)\n builder.add_resource(merged[key]["identifier"], buf)\n\n offset += count\n\n with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest:\n # In this example, the Signer is on the context.\n # A Signer can also be passed as first argument to\n # configure a dedicated Signer explicitly.\n builder.sign("image/jpeg", source, dest)\n'})}),"\n",(0,r.jsx)(n.h2,{id:"retrieving-actions-from-a-working-store",children:"Retrieving actions from a working store"}),"\n",(0,r.jsxs)(n.p,{children:["Actions are stored in the ",(0,r.jsx)(n.code,{children:"c2pa.actions.v2"})," assertion. Use ",(0,r.jsx)(n.code,{children:"Reader"})," to extract them from a signed asset or an archived ",(0,r.jsx)(n.code,{children:"Builder"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"reading-actions",children:"Reading actions"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'import json\nfrom c2pa import Context, Reader\n\nctx = Context()\nwith open("source.jpg", "rb") as f:\n reader = Reader("image/jpeg", f, context=ctx)\n parsed = json.loads(reader.json())\n\nactive = parsed["active_manifest"]\nassertions = parsed["manifests"][active].get("assertions", [])\n\nfor assertion in assertions:\n if assertion["label"] == "c2pa.actions.v2":\n for action in assertion["data"]["actions"]:\n print("Action:", action["action"])\n if "description" in action:\n print(" Description:", action["description"])\n'})}),"\n",(0,r.jsx)(n.h3,{id:"reading-actions-from-an-archive",children:"Reading actions from an archive"}),"\n",(0,r.jsxs)(n.p,{children:["Use the same approach with format ",(0,r.jsx)(n.code,{children:'"application/c2pa"'})," and an archive stream:"]}
1),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'with open("builder_archive.c2pa", "rb") as archive_file:\n reader = Reader("application/c2pa", archive_file)\n # Then parse and iterate assertions as in the example above\n'})}),"\n",(0,r.jsx)(n.h3,{id:"understanding-the-manifest-tree",children:"Understanding the manifest tree"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"reader.json()"})," returns a manifest store, a dictionary of manifests keyed by label (a URN like ",(0,r.jsx)(n.code,{children:"contentauth:urn:uuid:..."}),"). Conceptually it forms a tree: each manifest has assertions and ingredients; ingredients with ",(0,r.jsx)(n.code,{children:"manifest_data"})," carry their own manifest store, which can have its own ingredients and assertions recursively. The ",(0,r.jsx)(n.code,{children:"active_manifest"})," key indicates the root."]}),"\n",(0,r.jsx)(n.mermaid,{value:'flowchart TD\n subgraph Store["Manifest Store"]\n M1["Active Manifest\\n- assertions (including c2pa.actions.v2)\\n- ingredients"]\n M2["Ingredient A\'s manifest\\n- its own c2pa.actions.v2\\n- its own ingredients"]\n M3["Ingredient B\'s manifest\\n- its own c2pa.actions.v2"]\n end\n M1 --\x3e|"ingredient A has manifest_data"| M2\n M1 --\x3e|"ingredient B has manifest_data"| M3\n M1 -.-|"ingredient C has no manifest_data"| M5["Ingredient C\\n(unsigned asset, no provenance)"]\n M2 --\x3e|"may have its own ingredients..."| M4["...deeper in the tree"]\n\n style M5 fill:#eee,stroke:#999,stroke-dasharray: 5 5'}),"\n",(0,r.jsxs)(n.p,{children:["Not every ingredient has provenance. An unsigned asset added as an ingredient has ",(0,r.jsx)(n.code,{children:"title"}),", ",(0,r.jsx)(n.code,{children:"format"}),", and ",(0,r.jsx)(n.code,{children:"relationship"}),", but no ",(0,r.jsx)(n.code,{children:"manifest_data"})," and no entry in the ",(0,r.jsx)(n.code,{children:'"manifests"'})," dictionary. Walking the tree reveals the full provenance chain: what each actor did at each step, including actions performed and ingredients used."]}),"\n",(0,r.jsx)(n.p,{children:"To walk the tree and find actions at each level:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'import json\nfrom c2pa import Reader\n\nparsed = json.loads(reader.json())\nactive = parsed["active_manifest"]\nactive_manifest = parsed["manifests"][active]\n\n# Read the active manifest\'s actions\nfor assertion in active_manifest.get("assertions", []):\n if assertion["label"] == "c2pa.actions.v2":\n print("Active manifest actions:")\n for action in assertion["data"]["actions"]:\n print(" ", action["action"])\n\n# Walk into each ingredient\'s manifest\nfor ingredient in active_manifest.get("ingredients", []):\n print("Ingredient:", ingredient["title"])\n\n if "active_manifest" in ingredient:\n ing_manifest_label = ingredient["active_manifest"]\n ing_manifest = parsed["manifests"].get(ing_manifest_label)\n if ing_manifest:\n for assertion in ing_manifest.get("assertions", []):\n if assertion["label"] == "c2pa.actions.v2":\n print(" Ingredient\'s actions:")\n for action in assertion["data"]["actions"]:\n print(" ", action["action"])\n else:\n # This ingredient has no manifest of its own (unsigned asset).\n print(" (no content credentials)")\n'})}),"\n",(0,r.jsx)(n.h2,{id:"filtering-actions",children:"Filtering actions"}),"\n",(0,r.jsxs)(n.p,{children:["To remove actions, use the same read-filter-rebuild pattern: ",(0,r.jsx)(n.strong,{children:"read, pick the ones to keep, create a new Builder"}),"."]}),"\n",(0,r.jsx)(n.mermaid,{value:'flowchart TD\n SA["Signed Asset with 3 actions: opened, placed, filtered"] --\x3e|Reader| JSON[Parse JSON]\n JSON --\x3e|"Keep only opened + placed"| FILT[Filtered actions]\n FILT --\x3e|"New Builder with 2 actions"| NB[New Builder]\n NB --\x3e|sign| OUT["New asset with 2 actions only: opened, placed"]'}),"\n",(0,r.jsx)(n.h3,{id:"basic-action-filtering",children:"Basic action filtering"}),"\n",(0,r.jsxs)(n.p,{children:["When filtering, remember that the first action must remain ",(0,r.jsx)(n.code,{children:"c2pa.created"})," or ",(0,r.jsx)(n.code,{children:"c2pa.opened"})," for the manifest to be valid. If the first action is removed, a new one must be added."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'import json\nfrom c2pa import Builder, Context, Reader\n\nctx = Context()\nwith open("source.jpg", "rb") as f:\n reader = Reader("image/jpeg", f, context=ctx)\n parsed = json.loads(reader.json())\n\nactive = parsed["active_manifest"]\nmanifest = parsed["manifests"][active]\n\n# Filter actions: keep c2pa.created/c2pa.opened (mandatory) and c2pa.placed, drop the rest\nkept_actions = []\nfor assertion in manifest.get("assertions", []):\n if assertion["label"] == "c2pa.actions.v2":\n for action in assertion["data"]["actions"]:\n action_type = action["action"]\n if action_type in ("c2pa.created", "c2pa.opened", "c2pa.placed"):\n kept_actions.append(action)\n # Skip c2pa.filtered, c2pa.color_adju
1stments, etc.\n\n# Build a new manifest with only the kept actions\nnew_manifest = {"claim_generator_info": [{"name": "an-application", "version": "1.0"}]}\nif kept_actions:\n new_manifest["assertions"] = [{"label": "c2pa.actions", "data": {"actions": kept_actions}}]\n\nbuilder = Builder.from_json(new_manifest)\nwith open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst:\n builder.sign(signer, "image/jpeg", src, dst)\n'})}),"\n",(0,r.jsx)(n.h3,{id:"filtering-actions-that-reference-ingredients",children:"Filtering actions that reference ingredients"}),"\n",(0,r.jsxs)(n.p,{children:["Some actions reference ingredients (via ",(0,r.jsx)(n.code,{children:"parameters.ingredients[].url"})," after signing). If keeping an action that references an ingredient, ",(0,r.jsx)(n.strong,{children:"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)."]}),"\n",(0,r.jsx)(n.h4,{id:"c2paopened-action",children:"c2pa.opened action"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"c2pa.opened"})," action is special because it must be the first action and it references the asset that was opened (the ",(0,r.jsx)(n.code,{children:"parentOf"})," ingredient). When filtering:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsxs)(n.strong,{children:["Always keep ",(0,r.jsx)(n.code,{children:"c2pa.opened"})," or ",(0,r.jsx)(n.code,{children:"c2pa.created"})]}),": it is required for a valid manifest."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Keep the ingredient it references"}),": the ",(0,r.jsx)(n.code,{children:"parentOf"})," ingredient linked via its ",(0,r.jsx)(n.code,{children:"parameters.ingredients[].url"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:["Removing the ingredient that ",(0,r.jsx)(n.code,{children:"c2pa.opened"})," points to will make the manifest invalid."]}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"c2paplaced-action",children:"c2pa.placed action"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"c2pa.placed"})," action references a ",(0,r.jsx)(n.code,{children:"componentOf"})," ingredient that was composited into the asset. When filtering:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["If keeping ",(0,r.jsx)(n.code,{children:"c2pa.placed"}),", keep the ingredient it references."]}),"\n",(0,r.jsxs)(n.li,{children:["If the ingredient is dropped, also drop the ",(0,r.jsx)(n.code,{children:"c2pa.placed"})," action."]}),"\n",(0,r.jsxs)(n.li,{children:["If ",(0,r.jsx)(n.code,{children:"c2pa.placed"})," is not required, it can safely be removed (along with the ingredient it references, if it is the only reference)."]}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"example",children:"Example"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'import io\nimport json\nfrom c2pa import Builder, Context, Reader\n\nctx = Context()\nwith open("source.jpg", "rb") as f:\n reader = Reader("image/jpeg", f, context=ctx)\n parsed = json.loads(reader.json())\n active = parsed["active_manifest"]\n manifest = parsed["manifests"][active]\n\n # Filter actions and track which ingredients are needed\n kept_actions = []\n needed_ingredient_labels = set()\n\n for assertion in manifest.get("assertions", []):\n if assertion["label"] == "c2pa.actions.v2":\n for action in assertion["data"]["actions"]:\n action_type = action["action"]\n keep = action_type in ("c2pa.opened", "c2pa.created", "c2pa.placed")\n if keep:\n kept_actions.append(action)\n # Track which ingredients this action needs\n for ing_ref in action.get("parameters", {}).get("ingredients", []):\n url = ing_ref["url"]\n label = url.rsplit("/", 1)[-1]\n needed_ingredient_labels.add(label)\n\n # Keep only the ingredients that are referenced by kept actions\n kept_ingredients = [\n ing for ing in manifest.get("ingredients", [])\n if ing.get("label") in needed_ingredient_labels\n ]\n\n # Build the new manifest with filtered actions and matching ingredients\n new_manifest = {"claim_generator_info": [{"name": "an-application", "version": "1.0"}]}\n new_manifest["ingredients"] = kept_ingredients\n if kept_actions:\n new_manifest["assertions"] = [{"label": "c2pa.actions", "data": {"actions": kept_actions}}]\n\n builder = Builder.from_json(new_manifest)\n\n # Transfer binary resources for kept ingredients\n for ingredient in kept_ingredients:\n for key in ("thumbnail", "manifest_data"):\n if key in ingredient:\n resource_id = ingredient[key]["identifier"]\n resource_buf = io.BytesIO()\n reader.resource_to_stream(resource_id, resource_buf)\n resource_buf.seek(0)\n builder.add_resource(resource_id, resource_buf)\n\nwith open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst:\n builder.sign(signer, "image/jpeg", src, dst)\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["When copying ingredient JSON objects from a reader, they keep their ",(0,r.jsx)(n.code,{children:"label"})," field. Since the action URLs reference ingredients by label, the links resolve correctly as long as ingredients are not renamed or reindexed. If ingredients are re-added via ",(0,r.jsx)(n.code,{children:"add_ingredient()"})," (which generates new labels), the action URLs will also need to be updated."]})}),"\n",(0,r.jsx)(n.h2,{id:"controlling-manifest-embedding",children:"Controlling manifest embedding"}),"\n",(0,r.jsxs)(n.p,{children:["By default, ",(0,r.jsx)(n.code,{children:"sign()"})," embeds the manifest directly inside the output asset file."]}),"\n",(0,r.jsx)(n.h3,{id:"not-embedding-a-manifest-store-into-an-asset",children:"Not embedding a manifest store into an asset"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"set_no_embed()"}
1)," so the signed asset contains no embedded manifest store. The manifest store bytes are returned from ",(0,r.jsx)(n.code,{children:"sign()"})," and can be stored separately (e.g. as a sidecar file)."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'ctx = Context.from_dict({\n "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},\n "signer": signer,\n})\nbuilder = Builder(manifest_json, context=ctx)\nbuilder.set_no_embed()\nbuilder.set_remote_url("<<URI/URL to remote storage of manifest bytes>>")\n\nwith open("source.jpg", "rb") as source, open("output.jpg", "w+b") as dest:\n manifest_bytes = builder.sign("image/jpeg", source, dest)\n # manifest_bytes contains the full manifest store.\n # Upload manifest_bytes to the remote URL.\n # The output asset has no embedded manifest.\n'})}),"\n",(0,r.jsx)(n.h3,{id:"checking-manifest-location-on-a-reader",children:"Checking manifest location on a Reader"}),"\n",(0,r.jsxs)(n.p,{children:["After opening an asset with ",(0,r.jsx)(n.code,{children:"Reader"}),", use ",(0,r.jsx)(n.code,{children:"is_embedded()"})," to check whether the manifest is embedded in the asset or stored remotely. If the manifest is remote, ",(0,r.jsx)(n.code,{children:"get_remote_url()"})," returns the URL it was fetched from (the URL set via ",(0,r.jsx)(n.code,{children:"set_remote_url()"})," at signing time)."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-py",children:'reader = Reader("output.jpg", context=ctx)\n\nif reader.is_embedded():\n print("Manifest is embedded in the asset.")\nelse:\n print("Manifest is not embedded.")\n url = reader.get_remote_url()\n if url is not None:\n print(f"Remote manifest URL: {url}")\n'})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},8453:(e,n,i)=>{i.d(n,{R:()=>a,x:()=>d});var t=i(6540);const r={},s=t.createContext(r);function a(e){const n=t.useContext(s);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.