From 7f43fbbf5b1bfbd8c8ff030e96b0c514eebfad4d Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:55:10 -0700 Subject: [PATCH 1/2] fix: Add deprecation warnings --- docs/context-settings.md | 4 +- docs/intents.md | 14 ++-- docs/native-resources-management.md | 12 ++-- docs/selective-manifests.md | 24 +++---- docs/working-stores.md | 36 +++++----- examples/sign_info.py | 15 ++-- src/c2pa/c2pa.py | 105 ++++++++++++++++++++-------- 7 files changed, 131 insertions(+), 79 deletions(-) diff --git a/docs/context-settings.md b/docs/context-settings.md index 35692a82..6391b231 100644 --- a/docs/context-settings.md +++ b/docs/context-settings.md @@ -235,7 +235,7 @@ Archives (`.c2pa` files) store only the manifest definition. They do **not** sto - **`Builder.from_archive(stream)`** creates a context-free builder. All settings revert to SDK defaults regardless of what context the original builder had. - **`Builder({}, ctx).with_archive(stream)`** creates a builder with a context first, then loads the archived manifest definition into it. The context settings are preserved. -Use `with_archive()` when your workflow depends on specific settings (thumbnails, claim generator, intent, and so on). Use `from_archive()` only for quick prototyping where SDK defaults are acceptable. +Use `with_archive()`. `from_archive()` is deprecated and will be removed when c2pa-python moves to c2pa-rs 0.92.0. ```py ctx = Context.from_dict({ @@ -754,7 +754,7 @@ assert isinstance(ctx, ContextProvider) # True ## Migrating from load_settings -The `load_settings()` function is deprecated. Replace it with `Settings` and `Context` APIs: +The `load_settings()` function is deprecated and will be removed when c2pa-python moves to c2pa-rs 0.92.0. Replace it with `Settings` and `Context` APIs: | Aspect | `load_settings` (legacy) | `Context` | |--------|--------------------------|-----------| diff --git a/docs/intents.md b/docs/intents.md index bd970185..ffee4179 100644 --- a/docs/intents.md +++ b/docs/intents.md @@ -11,7 +11,7 @@ With intents, the caller declares *what is being done* and `Builder` handles the For example, without intents you have to manually wire up actions and make sure ingredients are properly linked to actions. This is especially important for `parentOf` ingredient relationships with the `c2pa.opened` action. ```py -with Builder({ +with Context() as ctx, Builder({ "assertions": [ { "label": "c2pa.actions", @@ -25,7 +25,7 @@ with Builder({ }, } ], -}) as builder: +}, context=ctx) as builder: with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: builder.sign(signer, "image/jpeg", source, dest) ``` @@ -33,7 +33,7 @@ with Builder({ But with intents, `Builder` generates the actions automatically; for example: ```py -with Builder({}) as builder: +with Context() as ctx, Builder({}, context=ctx) as builder: builder.set_intent( C2paBuilderIntent.CREATE, C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, @@ -92,7 +92,7 @@ for path in image_paths: Call `set_intent` directly on a `Builder` instance for one-off operations or when the intent is determined at runtime. For example: ```py -with Builder({}) as builder: +with Context() as ctx, Builder({}, context=ctx) as builder: builder.set_intent( C2paBuilderIntent.CREATE, C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, @@ -300,7 +300,7 @@ with Builder({}, context=ctx) as builder: Using `set_intent`: ```py -with Builder({}) as builder: +with Context() as ctx, Builder({}, context=ctx) as builder: builder.set_intent( C2paBuilderIntent.CREATE, C2paDigitalSourceType.DIGITAL_CREATION, @@ -379,7 +379,7 @@ with Builder({}, context=ctx) as builder: Using `set_intent`: ```py -with Builder({}) as builder: +with Context() as ctx, Builder({}, context=ctx) as builder: builder.set_intent(C2paBuilderIntent.EDIT) with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: builder.sign(signer, "image/jpeg", source, dest) @@ -473,7 +473,7 @@ with Builder({}, context=ctx) as builder: Using `set_intent`: ```py -with Builder({}) as builder: +with Context() as ctx, Builder({}, context=ctx) as builder: builder.set_intent(C2paBuilderIntent.UPDATE) with open("signed_asset.jpg", "rb") as source, open("updated_asset.jpg", "wb") as dest: diff --git a/docs/native-resources-management.md b/docs/native-resources-management.md index d3723948..95d51919 100644 --- a/docs/native-resources-management.md +++ b/docs/native-resources-management.md @@ -26,10 +26,12 @@ Python's garbage collector works by reference counting: each object counts how m `ManagedResource` gives every object three ways to release its native pointer: a `with` statement, an explicit `close()`, or, as a fallback, the destructor. +The examples on this page use a `Context` named `ctx`, created with `ctx = Context()`. A `Context` is a `ManagedResource` too, and is released the same ways. + ### `with` statement ```python -with Reader("image.jpg") as reader: +with Reader("image.jpg", context=ctx) as reader: print(reader.json()) # reader is automatically closed here. ``` @@ -39,7 +41,7 @@ On exit, `__exit__` calls `close()`, freeing the native pointer even if the bloc ### Explicit close ```python -reader = Reader("image.jpg") +reader = Reader("image.jpg", context=ctx) try: print(reader.json()) finally: @@ -57,7 +59,7 @@ Without `with` or `.close()`, `__del__` attempts the free when Python garbage-co Multiple resources can share one `with` statement or nest in separate `with` blocks. They are cleaned up in reverse order: right to left (when sharing one statement), or inner to outer (when statements are nested). ```python -with open("photo.jpg", "rb") as file, Reader("image/jpeg", file) as reader: +with open("photo.jpg", "rb") as file, Reader("image/jpeg", file, context=ctx) as reader: manifest = reader.json() # reader is closed first, then file ``` @@ -95,7 +97,7 @@ A native call checks the object is usable, then hands the pointer to native code A thread pool with a timeout can hit this too: ```python -with Reader("image.jpg") as reader: +with Reader("image.jpg", context=ctx) as reader: future = thread_pool.submit(reader.json) try: future.result(timeout=2.0) @@ -214,7 +216,7 @@ Two shapes: `Builder.sign()` is a consume-and-close call: ```python -builder = Builder(manifest_json) +builder = Builder(manifest_json, context=ctx) builder.sign(signer, "image/jpeg", source, dest) # builder is now CLOSED: sign() consumed its handle ``` diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 0dd8553a..5830c8ed 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -483,7 +483,7 @@ There are two distinct types of archives, sharing the same binary format but bei ### Builder archives vs. ingredient archives -A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder.from_archive()` to create a new builder instance from an archive, or `builder.with_archive()` to load a working store from a builder archive into an existing builder instance. +A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `builder.with_archive()`, which loads a working store from a builder archive into an existing builder instance. `Builder.from_archive()` also creates a new builder instance from an archive, but it is deprecated. An **ingredient archive** contains the manifest store from an asset that was added as an ingredient. @@ -576,7 +576,7 @@ Producer side, build the catalog: import io from c2pa import Builder -catalog_builder = Builder.from_json(manifest_json) +catalog_builder = Builder.from_json(manifest_json, context=ctx) with open("photo-A.jpg", "rb") as f: catalog_builder.add_ingredient( {"title": "photo-A.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-A"}, @@ -603,7 +603,7 @@ catalog_builder.write_ingredient_archive("catalog:ingredient-C", archive_c) Consumer side, pick one archive and load it: ```py -final_builder = Builder.from_json(manifest_json) +final_builder = Builder.from_json(manifest_json, context=ctx) archive_b.seek(0) final_builder.add_ingredient_from_archive(archive_b) @@ -631,7 +631,7 @@ signing_manifest = { }] } -signing_builder = Builder.from_json(signing_manifest) +signing_builder = Builder.from_json(signing_manifest, context=ctx) for archive in (archive_a, archive_b, archive_c): archive.seek(0) signing_builder.add_ingredient_from_archive(archive) @@ -654,7 +654,7 @@ Switch to the dedicated ingredient archive APIs: set `instance_id` per ingredien Producer side: ```py -catalog_builder = Builder.from_json(manifest_json) +catalog_builder = Builder.from_json(manifest_json, context=ctx) with open("photo-A.jpg", "rb") as f: catalog_builder.add_ingredient( {"title": "photo-A.jpg", "relationship": "componentOf", "instance_id": "catalog:ingredient-A"}, @@ -674,7 +674,7 @@ catalog_builder.write_ingredient_archive("catalog:ingredient-B", archive_b) Consumer side: ```py -final_builder = Builder.from_json(manifest_json) +final_builder = Builder.from_json(manifest_json, context=ctx) archive_b.seek(0) final_builder.add_ingredient_from_archive(archive_b) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: @@ -695,7 +695,7 @@ When building an ingredient archive, you can set `instance_id` on the ingredient ```py # Set instance_id when adding the ingredient to the archive builder. -builder = Builder.from_json(manifest_json) +builder = Builder.from_json(manifest_json, context=ctx) with open("photo-A.jpg", "rb") as f: builder.add_ingredient( { @@ -715,7 +715,7 @@ Later, when reading the archive, select ingredients by their `instance_id`: ```py archive.seek(0) -reader = Reader("application/c2pa", archive) +reader = Reader("application/c2pa", archive, context=ctx) manifest_data = json.loads(reader.json()) active = manifest_data["active_manifest"] ingredients = manifest_data["manifests"][active]["ingredients"] @@ -860,7 +860,7 @@ with Builder({ ``` > [!NOTE] -> When restoring from an archive, `with_archive()` preserves context settings while `from_archive()` does not. See [Working with archives](working-stores.md#working-with-archives) for the full comparison. +> When restoring from an archive, `with_archive()` preserves context settings. The deprecated `from_archive()` creates a builder with SDK default settings. See [Working with archives](working-stores.md#working-with-archives) for the full comparison. **Step 2:** Read the archive and extract ingredients: @@ -1129,7 +1129,7 @@ Use the same approach with format `"application/c2pa"` and an archive stream: ```py with open("builder_archive.c2pa", "rb") as archive_file: - reader = Reader("application/c2pa", archive_file) + reader = Reader("application/c2pa", archive_file, context=ctx) # Then parse and iterate assertions as in the example above ``` @@ -1232,7 +1232,7 @@ new_manifest = {"claim_generator_info": [{"name": "an-application", "version": " if kept_actions: new_manifest["assertions"] = [{"label": "c2pa.actions", "data": {"actions": kept_actions}}] -builder = Builder.from_json(new_manifest) +builder = Builder.from_json(new_manifest, context=ctx) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: builder.sign(signer, "image/jpeg", src, dst) ``` @@ -1300,7 +1300,7 @@ with open("source.jpg", "rb") as f: if kept_actions: new_manifest["assertions"] = [{"label": "c2pa.actions", "data": {"actions": kept_actions}}] - builder = Builder.from_json(new_manifest) + builder = Builder.from_json(new_manifest, context=ctx) # Transfer binary resources for kept ingredients for ingredient in kept_ingredients: diff --git a/docs/working-stores.md b/docs/working-stores.md index 88364e1c..a2a7a8c2 100644 --- a/docs/working-stores.md +++ b/docs/working-stores.md @@ -104,7 +104,7 @@ For full details on `Context` and `Settings`, see [Context and settings](context ### Checking if the manifest store is embedded ```py -reader = Reader("signed_image.jpg") +reader = Reader("signed_image.jpg", context=ctx) if reader.is_embedded(): print("Manifest store is embedded in the asset") @@ -515,7 +515,7 @@ import io, json archive_builder = Builder.from_json({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [], -}) +}, context=ctx) with open("photo.jpg", "rb") as f: archive_builder.add_ingredient( {"title": "photo.jpg", "relationship": "componentOf"}, @@ -842,7 +842,7 @@ manifest = { "claim_generator_info": [{"name": "c2pa-test", "version": "0.1.0"}], "assertions": [], } -builder = Builder.from_json(manifest) +builder = Builder.from_json(manifest, context=ctx) # Register three ingredients. The `label` becomes each ingredient's id. with open("first.jpg", "rb") as f: @@ -865,7 +865,7 @@ The archive contains exactly one ingredient. import io from c2pa import Builder -consumer = Builder.from_json(manifest) +consumer = Builder.from_json(manifest, context=ctx) # `archive` is a stream produced by write_ingredient_archive on another builder. archive.seek(0) @@ -888,7 +888,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: - The id does not match any registered ingredient's `label` or `instance_id`. ```py -builder = Builder.from_json(manifest) +builder = Builder.from_json(manifest, context=ctx) with open("photo.jpg", "rb") as f: builder.add_ingredient({"title": "photo.jpg", "relationship": "componentOf", "label": "real-id"}, "image/jpeg", f) @@ -900,7 +900,7 @@ builder.write_ingredient_archive("wrong-id", archive) To correct, pass the same string used in the ingredient's `label` or `instance_id` field: ```py -builder = Builder.from_json(manifest) +builder = Builder.from_json(manifest, context=ctx) with open("photo.jpg", "rb") as f: builder.add_ingredient({"title": "photo.jpg", "relationship": "componentOf", "label": "ingredient-id"}, "image/jpeg", f) @@ -917,7 +917,7 @@ The legacy approach wrapped one ingredient in a full builder archive, then resto ```py # Legacy: one ingredient archived as a full builder, restored with with_archive -builder = Builder.from_json(manifest) +builder = Builder.from_json(manifest, context=ctx) with open("photo.jpg", "rb") as f: builder.add_ingredient({"title": "photo.jpg", "relationship": "componentOf"}, "image/jpeg", f) archive = io.BytesIO() @@ -925,7 +925,7 @@ builder.to_archive(archive) # Consumer: archive.seek(0) -restored = Builder({}) +restored = Builder({}, context=ctx) restored.with_archive(archive) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: restored.sign(signer, "image/jpeg", src, dst) @@ -935,7 +935,7 @@ With the dedicated ingredient archive APIs, the producer writes a single-ingredi ```py # Current API: one archive per ingredient via write_ingredient_archive -builder = Builder.from_json(manifest) +builder = Builder.from_json(manifest, context=ctx) with open("photo.jpg", "rb") as f: builder.add_ingredient({"title": "photo.jpg", "relationship": "componentOf", "instance_id": "my-photo"}, "image/jpeg", f) @@ -943,7 +943,7 @@ archive = io.BytesIO() builder.write_ingredient_archive("my-photo", archive) # Consumer: -consumer = Builder.from_json(manifest) +consumer = Builder.from_json(manifest, context=ctx) archive.seek(0) consumer.add_ingredient_from_archive(archive) with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: @@ -1003,7 +1003,7 @@ Producer: import io from c2pa import Builder -producer = Builder.from_json(manifest_str) +producer = Builder.from_json(manifest_str, context=ctx) with open(source_path, "rb") as f: producer.add_ingredient( {"title": "photo.jpg", "relationship": "componentOf", "instance_id": "catalog:photo-A"}, @@ -1026,7 +1026,7 @@ signing_manifest = { }] } -consumer = Builder.from_json(signing_manifest) +consumer = Builder.from_json(signing_manifest, context=ctx) archive.seek(0) consumer.add_ingredient_from_archive(archive) with open(source_path, "rb") as src, open(output_path, "w+b") as dst: @@ -1042,7 +1042,7 @@ This works even though signing does not preserve `label` as an ingredient field. Producer: ```py -producer = Builder.from_json(manifest_str) +producer = Builder.from_json(manifest_str, context=ctx) with open(source_path, "rb") as f: producer.add_ingredient( {"title": "photo.jpg", "relationship": "componentOf", "label": "my-photo"}, @@ -1065,7 +1065,7 @@ signing_manifest = { }] } -consumer = Builder.from_json(signing_manifest) +consumer = Builder.from_json(signing_manifest, context=ctx) archive.seek(0) consumer.add_ingredient_from_archive(archive) with open(source_path, "rb") as src, open(output_path, "w+b") as dst: @@ -1079,7 +1079,7 @@ If both `label` and `instance_id` are set on an ingredient, pass whichever value Producer (passing `label` as the key): ```py -producer = Builder.from_json(manifest_str) +producer = Builder.from_json(manifest_str, context=ctx) with open(source_path, "rb") as f: producer.add_ingredient( {"title": "photo.jpg", "relationship": "componentOf", "label": "my-photo", "instance_id": "iid:abc123"}, @@ -1106,7 +1106,7 @@ signing_manifest = { }] } -consumer = Builder.from_json(signing_manifest) +consumer = Builder.from_json(signing_manifest, context=ctx) archive.seek(0) consumer.add_ingredient_from_archive(archive) with open(source_path, "rb") as src, open(output_path, "w+b") as dst: @@ -1121,7 +1121,7 @@ With the legacy `to_archive` + `Reader` pattern, `instance_id` survives into the import json from c2pa import Reader -reader = Reader("archive.c2pa") +reader = Reader("archive.c2pa", context=ctx) parsed = json.loads(reader.json()) active = parsed["active_manifest"] ingredients = parsed["manifests"][active].get("ingredients", []) @@ -1194,7 +1194,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: ### Checking manifest store location ```py -reader = Reader("asset.jpg") +reader = Reader("asset.jpg", context=ctx) if reader.is_embedded(): print("Manifest store is embedded in the asset") diff --git a/examples/sign_info.py b/examples/sign_info.py index 6b256647..cef9b280 100644 --- a/examples/sign_info.py +++ b/examples/sign_info.py @@ -25,8 +25,11 @@ fixtures_dir = os.path.join(os.path.dirname(__file__), "../tests/fixtures/") output_dir = os.path.join(os.path.dirname(__file__), "../output/") -# Note: Builder, Reader, and Signer support being used as context managers -# (with 'with' statements) for proper resource management. +# Note: Builder, Reader, Signer, and Context support being used as context +# managers (with 'with' statements) for proper resource management. + +# Use default Context and Settings. +context = c2pa.Context() # Ensure the output directory exists if not os.path.exists(output_dir): @@ -39,7 +42,7 @@ # Read existing C2PA metadata from the file print("\nReading existing C2PA metadata:") with open(fixtures_dir + "C.jpg", "rb") as file: - with c2pa.Reader("image/jpeg", file) as reader: + with c2pa.Reader("image/jpeg", file, context=context) as reader: print(reader.json()) # Create a signer from certificate and key files @@ -89,7 +92,7 @@ # Sign the image print("\nSigning the image...") with c2pa.Signer.from_info(signer_info) as signer: - with c2pa.Builder(manifest_definition) as builder: + with c2pa.Builder(manifest_definition, context) as builder: with open(fixtures_dir + "C.jpg", "rb") as source: # File needs to be opened in write+read mode to be signed # and verified properly. @@ -99,11 +102,13 @@ # Read the signed image to verify print("\nReading signed image metadata:") with open(output_dir + "C_signed.jpg", "rb") as file: - with c2pa.Reader("image/jpeg", file) as reader: + with c2pa.Reader("image/jpeg", file, context=context) as reader: # The validation state will depend on loaded trust settings. # Without loaded trust settings, # the manifest validation_state will be "Invalid". print(reader.json()) +context.close() + print("\nExample completed successfully!") diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index eca9e952..96b7dd7f 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -42,14 +42,17 @@ 'c2pa_version', # Error retriever and parser 'c2pa_error', - # Legacy APIs, deprecated + # Legacy APIs, deprecated. 'c2pa_load_settings', + 'c2pa_reader_from_stream', + 'c2pa_reader_from_manifest_data_and_stream', + 'c2pa_builder_from_json', + 'c2pa_builder_from_archive', + 'c2pa_format_embeddable', # Stream 'c2pa_create_stream', 'c2pa_release_stream', # Reader bindings - 'c2pa_reader_from_stream', - 'c2pa_reader_from_manifest_data_and_stream', 'c2pa_reader_json', 'c2pa_reader_detailed_json', 'c2pa_reader_crjson', @@ -62,8 +65,6 @@ 'c2pa_reader_remote_url', 'c2pa_reader_supported_mime_types', # Builder bindings - 'c2pa_builder_from_json', - 'c2pa_builder_from_archive', 'c2pa_builder_set_no_embed', 'c2pa_builder_set_remote_url', 'c2pa_builder_set_intent', @@ -79,13 +80,11 @@ 'c2pa_builder_with_definition', 'c2pa_builder_with_archive', 'c2pa_builder_supported_mime_types', - 'c2pa_format_embeddable', # Signer bindings 'c2pa_signer_create', 'c2pa_signer_from_info', 'c2pa_signer_reserve_size', 'c2pa_ed25519_sign', - 'c2pa_signature_free', # Settings bindings 'c2pa_settings_new', 'c2pa_settings_set_value', @@ -97,9 +96,7 @@ 'c2pa_context_builder_set_signer', 'c2pa_context_new', # Free bindings - 'c2pa_string_free', 'c2pa_free_string_array', - 'c2pa_manifest_bytes_free', 'c2pa_free', ] @@ -1120,7 +1117,7 @@ def _read_native_error() -> Optional[str]: try: message = ctypes.string_at(error).decode('utf-8') finally: - _lib.c2pa_string_free(error) + _lib.c2pa_free(error) _write_no_error_marker() if not message or _is_no_error_marker(message): @@ -1311,7 +1308,6 @@ def _setup_function(func, argtypes, restype=None): # Set up function prototypes not attached to an API object _setup_function(_lib.c2pa_version, [], ctypes.c_void_p) _setup_function(_lib.c2pa_error, [], ctypes.c_void_p) -_setup_function(_lib.c2pa_string_free, [ctypes.c_void_p], None) _setup_function( _lib.c2pa_load_settings, [ ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int) @@ -1407,10 +1403,6 @@ def _setup_function(func, argtypes, restype=None): ctypes.POINTER(C2paSigner), ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte))], ctypes.c_int64) -_setup_function( - _lib.c2pa_manifest_bytes_free, [ - ctypes.POINTER( - ctypes.c_ubyte)], None) _setup_function( _lib.c2pa_format_embeddable, [ ctypes.c_char_p, ctypes.POINTER( @@ -1437,10 +1429,6 @@ def _setup_function(func, argtypes, restype=None): ctypes.POINTER( ctypes.c_ubyte), ctypes.c_size_t, ctypes.c_char_p], ctypes.POINTER( ctypes.c_ubyte)) -_setup_function( - _lib.c2pa_signature_free, [ - ctypes.POINTER( - ctypes.c_ubyte)], None) _setup_function( _lib.c2pa_builder_supported_mime_types, [ctypes.POINTER(ctypes.c_size_t)], @@ -1532,7 +1520,7 @@ def _learn_no_error_marker_text(): try: text = ctypes.string_at(raw).decode('utf-8') finally: - _lib.c2pa_string_free(raw) + _lib.c2pa_free(raw) if not text: logger.warning( "c2pa: error-slot marker not set, some errors may be stale") @@ -1754,14 +1742,14 @@ def _convert_to_py_string(value) -> str: finally: # Only free if we have a valid pointer try: - _lib.c2pa_string_free(value) + _lib.c2pa_free(value) except Exception: # Ignore clean up issues pass except (ctypes.ArgumentError, TypeError, ValueError, OSError): # Invalid pointer type or value, gracefully handled by native lib. try: - _lib.c2pa_string_free(value) + _lib.c2pa_free(value) except Exception: pass return "" @@ -1967,6 +1955,20 @@ def version() -> str: return _convert_to_py_string(result) +def _warn_deprecated_api(message: str, stacklevel: int) -> None: + """Deprecation warning flag. + + Args: + message: Deprecation details. + stacklevel: warnings.warn. + """ + warnings.warn( + f"{message} This API will be removed in a future version", + DeprecationWarning, + stacklevel=stacklevel + 1, + ) + + @overload def load_settings(settings: str, format: str = "json") -> None: ... @@ -1986,6 +1988,8 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: Context will propagate configurations through object instances, no thread-local configurations. Avoid mixing Context APIs and legacy load_settings usage. + c2pa-rs 0.92.0 removes c2pa_load_settings, so this function + will be removed when c2pa-python moves to that release. Args: settings: The settings string or dict to load @@ -1995,10 +1999,9 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: Raises: C2paError: If there was an error loading the settings """ - warnings.warn( + _warn_deprecated_api( "load_settings() is deprecated. Use Settings" " and Context for per-instance configuration.", - DeprecationWarning, stacklevel=2, ) @@ -3020,6 +3023,7 @@ def try_create( return cls( format_or_path, stream, manifest_data, context=context, + _warn_stacklevel=3, ) except C2paError.ManifestNotFound: return None @@ -3055,6 +3059,8 @@ def __init__( stream: Optional[Any] = None, manifest_data: Optional[Any] = None, context: Optional['ContextProvider'] = None, + *, + _warn_stacklevel: int = 2, ): """Create a new Reader. @@ -3080,7 +3086,11 @@ def __init__( string requests detection from the bytes. stream: Optional stream to read from (Python stream-like object) manifest_data: Optional manifest data in bytes - context: Optional context implementing ContextProvider with settings + context: Context implementing ContextProvider with settings. + Omitting it is deprecated: c2pa-rs 0.92.0 removes the + native functions a Reader without a context calls. + _warn_stacklevel: Internal. Stack level for the deprecation + warning, raised by factory methods that call this one. Raises: C2paError: If there was an error creating the reader, including @@ -3109,6 +3119,12 @@ def __init__( ) return + _warn_deprecated_api( + "Creating a Reader without a context is deprecated." + " Pass context=Context(), or a Context built from Settings.", + stacklevel=_warn_stacklevel, + ) + format_bytes = self._resolve_format_bytes(format_or_path, stream) if stream is None: @@ -4004,7 +4020,7 @@ def from_json( Raises: C2paError: If there was an error creating the builder """ - return cls(manifest_json, context=context) + return cls(manifest_json, context=context, _warn_stacklevel=3) @classmethod def from_archive( @@ -4016,6 +4032,11 @@ def from_archive( This creates builder without a context. To use a context, create a Builder with a context first, then call with_archive() on it. + .. deprecated:: + Use ``Builder({}, context=Context()).with_archive(stream)``. + c2pa-rs 0.92.0 removes c2pa_builder_from_archive, so this method + will be removed when c2pa-python moves to that release. + Args: stream: The stream containing the archive (any Python stream-like object) @@ -4027,6 +4048,12 @@ def from_archive( C2paError: If there was an error creating the builder from archive """ + _warn_deprecated_api( + "Builder.from_archive() is deprecated. Use" + " Builder({}, context=Context()).with_archive(stream).", + stacklevel=2, + ) + stream_obj = Stream(stream) try: @@ -4063,12 +4090,18 @@ def __init__( self, manifest_json: Any, context: Optional['ContextProvider'] = None, + *, + _warn_stacklevel: int = 2, ): """Initialize a new Builder instance. Args: manifest_json: The manifest JSON definition (string or dict) - context: Optional Context (ContextProvider) for settings + context: Context (ContextProvider) for settings. Omitting it is + deprecated: c2pa-rs 0.92.0 removes c2pa_builder_from_json, + which a Builder without a context calls. + _warn_stacklevel: Internal. Stack level for the deprecation + warning, raised by factory methods that call this one. Raises: C2paError: If there was an error creating the builder @@ -4090,6 +4123,11 @@ def __init__( if context is not None: self._init_from_context(context, json_str) else: + _warn_deprecated_api( + "Creating a Builder without a context is deprecated." + " Pass context=Context(), or a Context built from Settings.", + stacklevel=_warn_stacklevel, + ) self._create_and_activate( lambda: _lib.c2pa_builder_from_json(json_str), Builder._ERROR_MESSAGES['builder_error']) @@ -4497,7 +4535,7 @@ def _sign_internal( # Always free the C-allocated memory, # even if we failed to copy manifest bytes try: - _lib.c2pa_manifest_bytes_free(manifest_bytes_ptr) + _lib.c2pa_free(manifest_bytes_ptr) except Exception: logger.error( "Failed to release native manifest bytes memory" @@ -4700,6 +4738,10 @@ def sign_file( def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]: """Convert a binary C2PA manifest into an embeddable version. + .. deprecated:: + c2pa-rs 0.92.0 removes c2pa_format_embeddable, so this function + will be removed when c2pa-python moves to that release. + Args: format: The MIME type or extension of the target format manifest_bytes: The raw manifest bytes @@ -4710,6 +4752,9 @@ def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]: Raises: C2paError: If there was an error converting the manifest """ + _warn_deprecated_api( + "format_embeddable() is deprecated.", stacklevel=2) + _check_cstr_arg("format", format) format_str = format.encode('utf-8') manifest_array = (ctypes.c_ubyte * len(manifest_bytes)).from_buffer_copy( @@ -4741,7 +4786,7 @@ def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]: f"Failed to convert embeddable manifest bytes: {e}" ) from e finally: - _lib.c2pa_manifest_bytes_free(result_bytes_ptr) + _lib.c2pa_free(result_bytes_ptr) return size, result_bytes @@ -4868,7 +4913,7 @@ def ed25519_sign(data: bytes, private_key: str) -> bytes: # Ed25519 signatures are always 64 bytes signature = bytes(signature_ptr[:64]) finally: - _lib.c2pa_signature_free(signature_ptr) + _lib.c2pa_free(signature_ptr) return signature From 0508adb2368f5ed5ee259251a79615c3de6bda32 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:01:31 -0700 Subject: [PATCH 2/2] fix: Review feedback --- docs/context-settings.md | 4 +- docs/intents.md | 25 ++- docs/native-resources-management.md | 12 +- docs/usage.md | 2 +- examples/sign_info.py | 15 +- src/c2pa/c2pa.py | 238 ++++++++++------------------ tests/perf/README.md | 2 +- tests/perf/scenarios.py | 4 +- tests/test_unit_tests.py | 197 +++++++++++------------ 9 files changed, 199 insertions(+), 300 deletions(-) diff --git a/docs/context-settings.md b/docs/context-settings.md index 6391b231..6010adba 100644 --- a/docs/context-settings.md +++ b/docs/context-settings.md @@ -18,7 +18,7 @@ The `Context` class encapsulates configuration for: - **Simplifies testing**: Create isolated configurations for tests without worrying about cleanup or interference. > [!NOTE] -> The deprecated `load_settings()` function still works for backward compatibility, but you are encouraged to migrate your code to use `Context`. See [Migrating from load_settings](#migrating-from-load_settings). +> Without a `Context`, `Reader` and `Builder` use one with SDK default settings. Pass a `Context` for custom settings or a context signer. The deprecated `load_settings()` function no longer affects `Reader` or `Builder`. See [Migrating from load_settings](#migrating-from-load_settings). ## Quick start @@ -768,7 +768,7 @@ The `load_settings()` function is deprecated and will be removed when c2pa-pytho from c2pa import load_settings, Reader load_settings({"builder": {"thumbnail": {"enabled": False}}}) -reader = Reader("image.jpg") # uses global settings +reader = Reader("image.jpg") # no longer sees thread local settings ``` **Current approach:** diff --git a/docs/intents.md b/docs/intents.md index ffee4179..79edd7a6 100644 --- a/docs/intents.md +++ b/docs/intents.md @@ -11,7 +11,7 @@ With intents, the caller declares *what is being done* and `Builder` handles the For example, without intents you have to manually wire up actions and make sure ingredients are properly linked to actions. This is especially important for `parentOf` ingredient relationships with the `c2pa.opened` action. ```py -with Context() as ctx, Builder({ +with Builder({ "assertions": [ { "label": "c2pa.actions", @@ -25,7 +25,7 @@ with Context() as ctx, Builder({ }, } ], -}, context=ctx) as builder: +}) as builder: with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: builder.sign(signer, "image/jpeg", source, dest) ``` @@ -33,7 +33,7 @@ with Context() as ctx, Builder({ But with intents, `Builder` generates the actions automatically; for example: ```py -with Context() as ctx, Builder({}, context=ctx) as builder: +with Builder({}) as builder: builder.set_intent( C2paBuilderIntent.CREATE, C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, @@ -51,7 +51,7 @@ You can set the intent on a `Builder` instance by: - [Using Context](#using-context) - [Using `set_intent` on the `Builder`](#using-set_intent-on-the-builder) -Don't set the intent using the deprecated `load_settings()` function. For existing code, see [Context and settings - Migrating from load_settings](context-settings.md#migrating-from-load_settings). +The deprecated `load_settings()` function no longer sets the intent for a `Builder`. To migrate existing code, see [Context and settings - Migrating from load_settings](context-settings.md#migrating-from-load_settings). ### Using Context @@ -92,7 +92,7 @@ for path in image_paths: Call `set_intent` directly on a `Builder` instance for one-off operations or when the intent is determined at runtime. For example: ```py -with Context() as ctx, Builder({}, context=ctx) as builder: +with Builder({}) as builder: builder.set_intent( C2paBuilderIntent.CREATE, C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, @@ -114,11 +114,7 @@ flowchart TD Check --> |No| CheckCtx{Was a Context with builder.intent provided?} CheckCtx --> |Yes| UseCtx["Use Context intent"] - CheckCtx --> |No| CheckGlobal{Was load_settings called - with builder.intent?} - CheckGlobal --> |Yes| UseGlobal["Use global intent - (deprecated)"] - CheckGlobal --> |No| NoIntent["No intent set. + CheckCtx --> |No| NoIntent["No intent set. Caller must define actions manually in manifest JSON."] ``` @@ -173,8 +169,7 @@ The `Builder` intent controls what the `Builder` does with the source stream (so ```mermaid flowchart TD Intent["Intent - (via Context, set_intent, - or load_settings)"] --> Q{Intent type?} + (via Context or set_intent)"] --> Q{Intent type?} Q --> |CREATE| CreateFlow["No parent allowed Source stream is new content"] Q --> |EDIT or UPDATE| EditFlow{Was a parentOf ingredient @@ -300,7 +295,7 @@ with Builder({}, context=ctx) as builder: Using `set_intent`: ```py -with Context() as ctx, Builder({}, context=ctx) as builder: +with Builder({}) as builder: builder.set_intent( C2paBuilderIntent.CREATE, C2paDigitalSourceType.DIGITAL_CREATION, @@ -379,7 +374,7 @@ with Builder({}, context=ctx) as builder: Using `set_intent`: ```py -with Context() as ctx, Builder({}, context=ctx) as builder: +with Builder({}) as builder: builder.set_intent(C2paBuilderIntent.EDIT) with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: builder.sign(signer, "image/jpeg", source, dest) @@ -473,7 +468,7 @@ with Builder({}, context=ctx) as builder: Using `set_intent`: ```py -with Context() as ctx, Builder({}, context=ctx) as builder: +with Builder({}) as builder: builder.set_intent(C2paBuilderIntent.UPDATE) with open("signed_asset.jpg", "rb") as source, open("updated_asset.jpg", "wb") as dest: diff --git a/docs/native-resources-management.md b/docs/native-resources-management.md index 95d51919..d3723948 100644 --- a/docs/native-resources-management.md +++ b/docs/native-resources-management.md @@ -26,12 +26,10 @@ Python's garbage collector works by reference counting: each object counts how m `ManagedResource` gives every object three ways to release its native pointer: a `with` statement, an explicit `close()`, or, as a fallback, the destructor. -The examples on this page use a `Context` named `ctx`, created with `ctx = Context()`. A `Context` is a `ManagedResource` too, and is released the same ways. - ### `with` statement ```python -with Reader("image.jpg", context=ctx) as reader: +with Reader("image.jpg") as reader: print(reader.json()) # reader is automatically closed here. ``` @@ -41,7 +39,7 @@ On exit, `__exit__` calls `close()`, freeing the native pointer even if the bloc ### Explicit close ```python -reader = Reader("image.jpg", context=ctx) +reader = Reader("image.jpg") try: print(reader.json()) finally: @@ -59,7 +57,7 @@ Without `with` or `.close()`, `__del__` attempts the free when Python garbage-co Multiple resources can share one `with` statement or nest in separate `with` blocks. They are cleaned up in reverse order: right to left (when sharing one statement), or inner to outer (when statements are nested). ```python -with open("photo.jpg", "rb") as file, Reader("image/jpeg", file, context=ctx) as reader: +with open("photo.jpg", "rb") as file, Reader("image/jpeg", file) as reader: manifest = reader.json() # reader is closed first, then file ``` @@ -97,7 +95,7 @@ A native call checks the object is usable, then hands the pointer to native code A thread pool with a timeout can hit this too: ```python -with Reader("image.jpg", context=ctx) as reader: +with Reader("image.jpg") as reader: future = thread_pool.submit(reader.json) try: future.result(timeout=2.0) @@ -216,7 +214,7 @@ Two shapes: `Builder.sign()` is a consume-and-close call: ```python -builder = Builder(manifest_json, context=ctx) +builder = Builder(manifest_json) builder.sign(signer, "image/jpeg", source, dest) # builder is now CLOSED: sign() consumed its handle ``` diff --git a/docs/usage.md b/docs/usage.md index c7754e72..524b6765 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. Without a `Context`, `Reader` and `Builder` use one with SDK default settings. See [Context and settings](context-settings.md) for details. diff --git a/examples/sign_info.py b/examples/sign_info.py index cef9b280..6b256647 100644 --- a/examples/sign_info.py +++ b/examples/sign_info.py @@ -25,11 +25,8 @@ fixtures_dir = os.path.join(os.path.dirname(__file__), "../tests/fixtures/") output_dir = os.path.join(os.path.dirname(__file__), "../output/") -# Note: Builder, Reader, Signer, and Context support being used as context -# managers (with 'with' statements) for proper resource management. - -# Use default Context and Settings. -context = c2pa.Context() +# Note: Builder, Reader, and Signer support being used as context managers +# (with 'with' statements) for proper resource management. # Ensure the output directory exists if not os.path.exists(output_dir): @@ -42,7 +39,7 @@ # Read existing C2PA metadata from the file print("\nReading existing C2PA metadata:") with open(fixtures_dir + "C.jpg", "rb") as file: - with c2pa.Reader("image/jpeg", file, context=context) as reader: + with c2pa.Reader("image/jpeg", file) as reader: print(reader.json()) # Create a signer from certificate and key files @@ -92,7 +89,7 @@ # Sign the image print("\nSigning the image...") with c2pa.Signer.from_info(signer_info) as signer: - with c2pa.Builder(manifest_definition, context) as builder: + with c2pa.Builder(manifest_definition) as builder: with open(fixtures_dir + "C.jpg", "rb") as source: # File needs to be opened in write+read mode to be signed # and verified properly. @@ -102,13 +99,11 @@ # Read the signed image to verify print("\nReading signed image metadata:") with open(output_dir + "C_signed.jpg", "rb") as file: - with c2pa.Reader("image/jpeg", file, context=context) as reader: + with c2pa.Reader("image/jpeg", file) as reader: # The validation state will depend on loaded trust settings. # Without loaded trust settings, # the manifest validation_state will be "Invalid". print(reader.json()) -context.close() - print("\nExample completed successfully!") diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 96b7dd7f..a9090cc1 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -44,9 +44,6 @@ 'c2pa_error', # Legacy APIs, deprecated. 'c2pa_load_settings', - 'c2pa_reader_from_stream', - 'c2pa_reader_from_manifest_data_and_stream', - 'c2pa_builder_from_json', 'c2pa_builder_from_archive', 'c2pa_format_embeddable', # Stream @@ -1318,13 +1315,6 @@ def _setup_function(func, argtypes, restype=None): ) # Set up Reader function prototypes -_setup_function(_lib.c2pa_reader_from_stream, - [ctypes.c_char_p, ctypes.POINTER(C2paStream)], - ctypes.POINTER(C2paReader)) -_setup_function( - _lib.c2pa_reader_from_manifest_data_and_stream, [ - ctypes.c_char_p, ctypes.POINTER(C2paStream), ctypes.POINTER( - ctypes.c_ubyte), ctypes.c_size_t], ctypes.POINTER(C2paReader)) _setup_function( _lib.c2pa_reader_json, [ ctypes.POINTER(C2paReader)], ctypes.c_void_p) @@ -1353,9 +1343,6 @@ def _setup_function(func, argtypes, restype=None): ) # Set up Builder function prototypes -_setup_function( - _lib.c2pa_builder_from_json, [ - ctypes.c_char_p], ctypes.POINTER(C2paBuilder)) _setup_function(_lib.c2pa_builder_from_archive, [ctypes.POINTER(C2paStream)], ctypes.POINTER(C2paBuilder)) @@ -1990,6 +1977,9 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: and legacy load_settings usage. c2pa-rs 0.92.0 removes c2pa_load_settings, so this function will be removed when c2pa-python moves to that release. + Reader and Builder no longer read these settings: without a + Context they use one with default settings. Only the deprecated + Builder.from_archive() still reads them. Args: settings: The settings string or dict to load @@ -2000,8 +1990,9 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: C2paError: If there was an error loading the settings """ _warn_deprecated_api( - "load_settings() is deprecated. Use Settings" - " and Context for per-instance configuration.", + "load_settings() is deprecated. Reader and Builder no longer read" + " these settings. Use Settings and Context for per-instance" + " configuration.", stacklevel=2, ) @@ -2047,6 +2038,46 @@ def _context_guard(context): yield +_default_context = None +_default_context_lock = threading.Lock() + + +def _is_reusable_default(ctx) -> bool: + # Checks only for a closed or foreign-process context. is_valid also + # turns False during an in-flight mutating call, and a busy default + # must report that error to its caller. + return (ctx is not None + and ctx._lifecycle_state != LifecycleState.CLOSED + and not is_foreign_process(ctx)) + + +def _get_default_context() -> 'Context': + """Return the process-wide Context that Reader and Builder use when + no context is passed. It has default settings and no signer. + """ + global _default_context + ctx = _default_context + if _is_reusable_default(ctx): + return ctx + with _default_context_lock: + if not _is_reusable_default(_default_context): + _default_context = Context() + return _default_context + + +def _reset_default_context_in_child(): + # Native locks inherited across fork() may be held by threads that do + # not exist in the child, so the child builds its own default. The + # parent's Context keeps the parent's PID, so it is never freed here. + global _default_context, _default_context_lock + _default_context = None + _default_context_lock = threading.Lock() + + +if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=_reset_default_context_in_child) + + class ContextProvider(ABC): """Abstract base class for types that provide a C2PA context. @@ -3010,7 +3041,8 @@ def try_create( requests auto-detection (best effort). stream: Optional stream to read from (Python stream-like object) manifest_data: Optional manifest data in bytes - context: Optional ContextProvider for settings + context: Optional ContextProvider for settings. Without one, + a shared Context with default settings is used. Returns: Reader instance if the asset contains C2PA data, @@ -3023,7 +3055,6 @@ def try_create( return cls( format_or_path, stream, manifest_data, context=context, - _warn_stacklevel=3, ) except C2paError.ManifestNotFound: return None @@ -3059,8 +3090,6 @@ def __init__( stream: Optional[Any] = None, manifest_data: Optional[Any] = None, context: Optional['ContextProvider'] = None, - *, - _warn_stacklevel: int = 2, ): """Create a new Reader. @@ -3086,11 +3115,9 @@ def __init__( string requests detection from the bytes. stream: Optional stream to read from (Python stream-like object) manifest_data: Optional manifest data in bytes - context: Context implementing ContextProvider with settings. - Omitting it is deprecated: c2pa-rs 0.92.0 removes the - native functions a Reader without a context calls. - _warn_stacklevel: Internal. Stack level for the deprecation - warning, raised by factory methods that call this one. + context: Optional context implementing ContextProvider with + settings. Without one, a shared Context with default + settings is used. Raises: C2paError: If there was an error creating the reader, including @@ -3101,8 +3128,6 @@ def __init__( super().__init__() self._init_attrs() - self._context = context - # Only stream, no format: Reader(fh) must auto-detect on the stream. if stream is None and Stream.is_read_stream(format_or_path): stream = format_or_path @@ -3111,36 +3136,15 @@ def __init__( # still required to read from. if format_or_path is None and stream is None: raise C2paError("Reader requires a path or a stream") + if context is None: + context = _get_default_context() + self._context = context - if context is not None: - self._init_from_context( - context, format_or_path, stream, - manifest_data, - ) - return - - _warn_deprecated_api( - "Creating a Reader without a context is deprecated." - " Pass context=Context(), or a Context built from Settings.", - stacklevel=_warn_stacklevel, + self._init_from_context( + context, format_or_path, stream, + manifest_data, ) - format_bytes = self._resolve_format_bytes(format_or_path, stream) - - if stream is None: - # Create a stream from the file path in format_or_path. - # A None format lets the native lib guess from the bytes. - self._init_from_file(str(format_or_path), format_bytes) - - elif isinstance(stream, str): - # stream is a file path, format_or_path is the format - self._init_from_file( - stream, format_bytes, manifest_data) - - else: - # format_or_path is a format string, stream is a stream object - self._init_from_stream(stream, format_bytes, manifest_data) - @staticmethod def _resolve_format_bytes(format_or_path, stream) -> Optional[bytes]: """Resolve the encoded format for a (format_or_path, stream) pair. @@ -3156,78 +3160,6 @@ def _resolve_format_bytes(format_or_path, stream) -> Optional[bytes]: return _encode_format( None if format_or_path is None else str(format_or_path), "Reader") - def _create_reader(self, format_bytes, stream_obj, - manifest_data=None): - """Create a native reader from a Stream - and activate this Reader around it. - - Args: - format_bytes: Encoded format/MIME type, or None for auto-detection - stream_obj: A Stream instance - manifest_data: Optional manifest bytes - """ - format_arg = _format_ffi_arg(format_bytes) - if manifest_data is None: - def create(): - return _lib.c2pa_reader_from_stream( - format_arg, stream_obj._stream) - else: - if not isinstance(manifest_data, bytes): - raise TypeError(Reader._ERROR_MESSAGES['manifest_error']) - manifest_array = ( - ctypes.c_ubyte * - len(manifest_data)).from_buffer_copy(manifest_data) - - def create(): - return _lib.c2pa_reader_from_manifest_data_and_stream( - format_arg, - stream_obj._stream, - manifest_array, - len(manifest_data), - ) - - self._create_and_activate( - create, Reader._ERROR_MESSAGES['reader_error']) - - def _init_from_file(self, path, format_bytes, - manifest_data=None): - """Open a file and create a reader from it. - - Args: - path: File path to open - format_bytes: UTF-8 encoded format/MIME type - manifest_data: Optional manifest bytes - """ - try: - self._backing_file = open(path, 'rb') - self._own_stream = Stream(self._backing_file) - self._create_reader(format_bytes, self._own_stream, manifest_data) - except C2paError: - self._close_streams() - raise - except Exception as e: - self._close_streams() - raise C2paError.Io( - Reader._ERROR_MESSAGES['io_error'].format(str(e))) - - def _init_from_stream(self, stream, format_bytes, - manifest_data=None): - """Create a reader from a caller-supplied stream object. - The native reader reads through this stream as long as it's alive, - so the wrapper is stored on the instance and released by _release(). - - Args: - stream: A stream-like object owned by the caller - format_bytes: UTF-8 encoded format/MIME type - manifest_data: Optional manifest bytes - """ - try: - self._own_stream = Stream(stream) - self._create_reader(format_bytes, self._own_stream, manifest_data) - except Exception: - self._close_streams() - raise - def _init_from_context(self, context, format_or_path, stream, manifest_data=None): """Initialize Reader from a Context object implementing @@ -3242,15 +3174,20 @@ def _init_from_context(self, context, format_or_path, # Determine format and open stream format_bytes = self._resolve_format_bytes(format_or_path, stream) format_arg = _format_ffi_arg(format_bytes) - if stream is None: - # A None format lets the native lib guess from the bytes. - self._backing_file = open(str(format_or_path), 'rb') - self._own_stream = Stream(self._backing_file) - elif isinstance(stream, str): - self._backing_file = open(stream, 'rb') - self._own_stream = Stream(self._backing_file) - else: - self._own_stream = Stream(stream) + try: + if stream is None: + # A None format lets the native lib guess from the bytes. + self._backing_file = open(str(format_or_path), 'rb') + self._own_stream = Stream(self._backing_file) + elif isinstance(stream, str): + self._backing_file = open(stream, 'rb') + self._own_stream = Stream(self._backing_file) + else: + self._own_stream = Stream(stream) + except OSError as e: + self._close_streams() + raise C2paError.Io( + Reader._ERROR_MESSAGES['io_error'].format(str(e))) try: # The Context is caller-supplied and may be shared, so its handle @@ -4012,7 +3949,8 @@ def from_json( Args: manifest_json: The JSON manifest definition - context: Optional ContextProvider for settings + context: Optional ContextProvider for settings. Without one, + a shared Context with default settings is used. Returns: A new Builder instance @@ -4020,7 +3958,7 @@ def from_json( Raises: C2paError: If there was an error creating the builder """ - return cls(manifest_json, context=context, _warn_stacklevel=3) + return cls(manifest_json, context=context) @classmethod def from_archive( @@ -4090,18 +4028,14 @@ def __init__( self, manifest_json: Any, context: Optional['ContextProvider'] = None, - *, - _warn_stacklevel: int = 2, ): """Initialize a new Builder instance. Args: manifest_json: The manifest JSON definition (string or dict) - context: Context (ContextProvider) for settings. Omitting it is - deprecated: c2pa-rs 0.92.0 removes c2pa_builder_from_json, - which a Builder without a context calls. - _warn_stacklevel: Internal. Stack level for the deprecation - warning, raised by factory methods that call this one. + context: Optional Context (ContextProvider) for settings. + Without one, a shared Context with default settings and no + signer is used. Raises: C2paError: If there was an error creating the builder @@ -4111,26 +4045,18 @@ def __init__( super().__init__() self._init_attrs() + if context is None: + context = _get_default_context() + self._context = context self._has_context_signer = ( - context is not None - and hasattr(context, 'has_signer') + hasattr(context, 'has_signer') and context.has_signer ) json_str = _to_utf8_bytes(manifest_json, "manifest JSON") - if context is not None: - self._init_from_context(context, json_str) - else: - _warn_deprecated_api( - "Creating a Builder without a context is deprecated." - " Pass context=Context(), or a Context built from Settings.", - stacklevel=_warn_stacklevel, - ) - self._create_and_activate( - lambda: _lib.c2pa_builder_from_json(json_str), - Builder._ERROR_MESSAGES['builder_error']) + self._init_from_context(context, json_str) def _init_from_context(self, context, json_str): """Initialize Builder from a ContextProvider. diff --git a/tests/perf/README.md b/tests/perf/README.md index a9a87a93..49704db6 100644 --- a/tests/perf/README.md +++ b/tests/perf/README.md @@ -33,7 +33,7 @@ Each scenario loops multiple times so leaks accumulate and become visible in the make memory-use-bench MEMRAY_ITERATIONS=1000 ``` -Most scenarios use the Context API: they build a `Context` once and reuse it across iterations, so its settings are parsed a single time. The jpeg and png cases also keep a `_legacy` variant that builds the `Reader`/`Builder` without a `Context`, which re-reads the thread-local settings on each construction. Running a pair (for example `builder_sign_jpeg_legacy` and `builder_sign_jpeg_with_context`) compares the two paths. +Most scenarios use the Context API: they build a `Context` once and reuse it across iterations, so its settings are parsed a single time. The jpeg and png cases also keep a `_legacy` variant that builds the `Reader`/`Builder` without a `Context`, so it uses the shared default `Context`. Running a pair (for example `builder_sign_jpeg_legacy` and `builder_sign_jpeg_with_context`) compares the two paths. The `builder_sign_{jpeg,png}_parallel_*` scenarios build one `Context` and share it across 10 threads that sign concurrently, each with its own streams and `Builder`. The name encodes two axes. `split` divides the iteration budget across the threads, so total work matches a single-threaded scenario; `full` runs the full loop on each of the 10 threads, so total work is 10x (use these with `SCENARIO=` rather than the whole suite). `pool` runs the threads through a `ThreadPoolExecutor`; `barrier` starts all 10 at once with a `threading.Barrier`. diff --git a/tests/perf/scenarios.py b/tests/perf/scenarios.py index 9510f60f..a026997a 100644 --- a/tests/perf/scenarios.py +++ b/tests/perf/scenarios.py @@ -140,8 +140,8 @@ def _read_file(path: Path, mime: str, iterations: int) -> None: # Context-API helpers: the Context is built once before the loop and reused on # every iteration, so its settings are parsed a single time. Most scenarios use # these. The `_legacy` jpeg/png scenarios build the Reader/Builder without a -# Context, which re-reads thread-local settings on each construction; running a -# legacy scenario against its `_with_context` pair isolates the settings cost. +# Context, so they use the shared default Context; running a legacy scenario +# against its `_with_context` pair compares the two. def _sign_file_context(path: Path, mime: str, iterations: int) -> None: signer = _make_signer() diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index dd3c3ec3..69ba7875 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -54,6 +54,13 @@ ALTERNATIVE_INGREDIENT_TEST_FILE = os.path.join(FIXTURES_DIR, "cloud.jpg") +def _context_from_settings(settings): + """Build a Context from a settings dict or JSON string.""" + if isinstance(settings, dict): + return Context.from_dict(settings) + return Context.from_json(settings) + + def _patch_free(test, fn): """Route ManagedResource._free_native_ptr to `fn` until `test` ends.""" patcher = patch.object( @@ -423,10 +430,10 @@ def read_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(self.testPath, "rb") as file: - reader = Reader("image/jpeg", file) + reader = Reader("image/jpeg", file, context=settings_context) validation_state = reader.get_validation_state() result['validation_state'] = validation_state except Exception as e: @@ -2233,14 +2240,14 @@ def sign_and_validate_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(self.testPath, "rb") as file: - builder = Builder(self.manifestDefinitionV2) + builder = Builder(self.manifestDefinitionV2, context=settings_context) output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() # Get validation state with trust config @@ -2307,7 +2314,7 @@ def sign_and_validate_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(os.path.join(self.data_dir, "ed25519.pub"), "rb") as cert_file: certs = cert_file.read() @@ -2323,11 +2330,11 @@ def sign_and_validate_with_trust_config(): signer = Signer.from_info(signer_info) with open(self.testPath, "rb") as file: - builder = Builder(self.manifestDefinitionV2) + builder = Builder(self.manifestDefinitionV2, context=settings_context) output = io.BytesIO(bytearray()) builder.sign(signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() # Get validation state with trust config @@ -2447,7 +2454,7 @@ def sign_and_validate_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(os.path.join(self.data_dir, "ps256.pub"), "rb") as cert_file: certs = cert_file.read() @@ -2463,11 +2470,11 @@ def sign_and_validate_with_trust_config(): signer = Signer.from_info(signer_info) with open(self.testPath2, "rb") as file: - builder = Builder(self.manifestDefinitionV2) + builder = Builder(self.manifestDefinitionV2, context=settings_context) output = io.BytesIO(bytearray()) builder.sign(signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() # Get validation state with trust config @@ -2525,17 +2532,17 @@ def sign_and_validate_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(self.testPath, "rb") as file: - builder = Builder(self.manifestDefinition) + builder = Builder(self.manifestDefinition, context=settings_context) archive = io.BytesIO(bytearray()) builder.to_archive(archive) builder = Builder.from_archive(archive) output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() # Get validation state with trust config @@ -2597,10 +2604,10 @@ def sign_and_validate_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(self.testPath, "rb") as file: - builder = Builder(self.manifestDefinitionV2) + builder = Builder(self.manifestDefinitionV2, context=settings_context) archive = io.BytesIO(bytearray()) builder.to_archive(archive) builder = Builder.from_archive(archive) @@ -2610,7 +2617,7 @@ def sign_and_validate_with_trust_config(): builder.add_ingredient(ingredient_json, "image/jpeg", f) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() # Get validation state with trust config @@ -2964,6 +2971,7 @@ def test_ingredient_from_archive_linked_to_opened_action(self): output.close() def test_ingredient_from_archive_linked_to_placed_action(self): + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') ingredient_id = "xmp:iid:archive-component-001" manifest = { "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}], @@ -2981,7 +2989,7 @@ def test_ingredient_from_archive_linked_to_placed_action(self): } ], } - archive_builder = Builder.from_json(manifest) + archive_builder = Builder.from_json(manifest, context=settings_context) with open(self.testPath, "rb") as f: archive_builder.add_ingredient( {"title": "photo.jpg", "relationship": "componentOf", "instance_id": ingredient_id}, @@ -2991,7 +2999,7 @@ def test_ingredient_from_archive_linked_to_placed_action(self): archive_builder.close() archive.seek(0) - signing_builder = Builder.from_json(manifest) + signing_builder = Builder.from_json(manifest, context=settings_context) signing_builder.add_ingredient_from_archive(archive) archive.close() @@ -3001,13 +3009,14 @@ def test_ingredient_from_archive_linked_to_placed_action(self): signing_builder.close() output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() self.assertIn("c2pa.placed", json_data) self.assertIn(ingredient_id, json_data) output.close() def test_ingredient_from_archive_linked_to_edited_action(self): + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') ingredient_id = "xmp:iid:archive-input-001" manifest = { "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}], @@ -3025,7 +3034,7 @@ def test_ingredient_from_archive_linked_to_edited_action(self): } ], } - archive_builder = Builder.from_json(manifest) + archive_builder = Builder.from_json(manifest, context=settings_context) with open(self.testPath, "rb") as f: archive_builder.add_ingredient( {"title": "photo.jpg", "relationship": "inputTo", "instance_id": ingredient_id}, @@ -3035,7 +3044,7 @@ def test_ingredient_from_archive_linked_to_edited_action(self): archive_builder.close() archive.seek(0) - signing_builder = Builder.from_json(manifest) + signing_builder = Builder.from_json(manifest, context=settings_context) signing_builder.add_ingredient_from_archive(archive) archive.close() @@ -3045,7 +3054,7 @@ def test_ingredient_from_archive_linked_to_edited_action(self): signing_builder.close() output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() self.assertIn("c2pa.edited", json_data) self.assertIn(ingredient_id, json_data) @@ -3336,10 +3345,10 @@ def sign_and_validate_with_trust_config(): # Apply the settings (including trust configuration) # Settings are thread-local, so they won't affect other tests # And that is why we also run the test in its own thread, so tests are isolated - load_settings(settings_dict) + settings_context = _context_from_settings(settings_dict) with open(self.testPath, "rb") as file: - builder = Builder(self.manifestDefinitionV2) + builder = Builder(self.manifestDefinitionV2, context=settings_context) builder.set_no_embed() with io.BytesIO() as output_buffer: manifest_data = builder.sign( @@ -3347,7 +3356,7 @@ def sign_and_validate_with_trust_config(): output_buffer.seek(0) read_buffer = io.BytesIO(output_buffer.getvalue()) - with Reader("image/jpeg", read_buffer, manifest_data) as reader: + with Reader("image/jpeg", read_buffer, manifest_data, context=settings_context) as reader: json_data = reader.json() # Get validation state with trust config @@ -3813,9 +3822,9 @@ def test_builder_sign_with_ingredients_edit_intent(self): def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self): # The following removes the manifest's thumbnail # Settings should be loaded before the builder is created - load_settings('{"builder": { "thumbnail": {"enabled": false}}}') + settings_context = _context_from_settings('{"builder": { "thumbnail": {"enabled": false}}}') - builder = Builder.from_json(self.manifestDefinition) + builder = Builder.from_json(self.manifestDefinition, context=settings_context) assert builder._handle is not None # Test adding ingredient @@ -3827,7 +3836,7 @@ def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -3855,14 +3864,12 @@ def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self): builder.close() - # Settings are thread-local, so we reset to the default "true" here - load_settings('{"builder": { "thumbnail": {"enabled": true}}}') def test_builder_sign_with_settingdict_no_thumbnail_and_ingredient(self): # The following removes the manifest's thumbnail - using dict instead of string - load_settings({"builder": {"thumbnail": {"enabled": False}}}) + settings_context = _context_from_settings({"builder": {"thumbnail": {"enabled": False}}}) - builder = Builder.from_json(self.manifestDefinition) + builder = Builder.from_json(self.manifestDefinition, context=settings_context) assert builder._handle is not None # Test adding ingredient @@ -3874,7 +3881,7 @@ def test_builder_sign_with_settingdict_no_thumbnail_and_ingredient(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -3902,8 +3909,6 @@ def test_builder_sign_with_settingdict_no_thumbnail_and_ingredient(self): builder.close() - # Settings are thread-local, so we reset to the default "true" here - using dict instead of string - load_settings({"builder": {"thumbnail": {"enabled": True}}}) def test_builder_sign_with_duplicate_ingredient(self): builder = Builder.from_json(self.manifestDefinition) @@ -4139,9 +4144,9 @@ def test_builder_set_remote_url_no_embed(self): """Test setting the remote url of a builder with no embed flag.""" # Settings need to be loaded before the builder is created - load_settings(r'{"verify": { "remote_manifest_fetch": false} }') + settings_context = _context_from_settings(r'{"verify": { "remote_manifest_fetch": false} }') - builder = Builder.from_json(self.manifestDefinition) + builder = Builder.from_json(self.manifestDefinition, context=settings_context) builder.set_no_embed() builder.set_remote_url("http://this_does_not_exist/foo.jpg") @@ -4150,12 +4155,10 @@ def test_builder_set_remote_url_no_embed(self): builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) with self.assertRaises(Error) as e: - Reader("image/jpeg", output) + Reader("image/jpeg", output, context=settings_context) self.assertIn("http://this_does_not_exist/foo.jpg", e.exception.message) - # Return back to default settings - load_settings(r'{"verify": { "remote_manifest_fetch": true} }') def test_sign_single(self): """Test signing a file using the sign_file method.""" @@ -4868,7 +4871,7 @@ def test_builder_state_with_invalid_native_pointer(self): def test_builder_add_action_to_manifest_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -4893,7 +4896,7 @@ def test_builder_add_action_to_manifest_no_auto_add(self): } ] } - builder = Builder.from_json(initial_manifest_definition) + builder = Builder.from_json(initial_manifest_definition, context=settings_context) action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}' builder.add_action(action_json) @@ -4902,7 +4905,7 @@ def test_builder_add_action_to_manifest_no_auto_add(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -4943,12 +4946,10 @@ def test_builder_add_action_to_manifest_no_auto_add(self): builder.close() - # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -4973,7 +4974,7 @@ def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): } ] } - builder = Builder.from_json(initial_manifest_definition) + builder = Builder.from_json(initial_manifest_definition, context=settings_context) # Using a dictionary instead of a JSON string action_dict = {"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}} @@ -4983,7 +4984,7 @@ def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5024,12 +5025,10 @@ def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): builder.close() - # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_add_action_to_manifest_with_auto_add(self): # For testing, force settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -5055,7 +5054,7 @@ def test_builder_add_action_to_manifest_with_auto_add(self): } ] } - builder = Builder.from_json(initial_manifest_definition) + builder = Builder.from_json(initial_manifest_definition, context=settings_context) action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}' builder.add_action(action_json) @@ -5064,7 +5063,7 @@ def test_builder_add_action_to_manifest_with_auto_add(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5113,12 +5112,10 @@ def test_builder_add_action_to_manifest_with_auto_add(self): builder.close() - # Reset settings to default - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { "claim_generator": "python_test", @@ -5130,14 +5127,14 @@ def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): "title": "Python Test Image V2", } - builder = Builder.from_json(initial_manifest_definition) + builder = Builder.from_json(initial_manifest_definition, context=settings_context) builder.add_action('{ "action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}') with open(self.testPath2, "rb") as file: output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5178,12 +5175,10 @@ def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): builder.close() - # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -5194,7 +5189,7 @@ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): "title": "Python Test Image V2", } - builder = Builder.from_json(initial_manifest_definition) + builder = Builder.from_json(initial_manifest_definition, context=settings_context) action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}' builder.add_action(action_json) @@ -5202,7 +5197,7 @@ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5252,12 +5247,10 @@ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): builder.close() - # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_sign_dicts_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { "claim_generator_info": [{ @@ -5282,7 +5275,7 @@ def test_builder_sign_dicts_no_auto_add(self): } ] } - builder = Builder.from_json(initial_manifest_definition) + builder = Builder.from_json(initial_manifest_definition, context=settings_context) # Using a dictionary instead of a JSON string action_dict = {"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}} @@ -5292,7 +5285,7 @@ def test_builder_sign_dicts_no_auto_add(self): output = io.BytesIO(bytearray()) builder.sign(self.signer, "image/jpeg", file, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5333,13 +5326,11 @@ def test_builder_sign_dicts_no_auto_add(self): builder.close() - # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}') def test_builder_opened_action_one_ingredient_no_auto_add(self): """Test Builder with c2pa.opened action and one ingredient, following Adobe provenance patterns""" # Disable auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') # Instance IDs for linking ingredients and actions # This can be any unique id so the ingredient can be uniquely identified and linked to the action @@ -5390,7 +5381,7 @@ def test_builder_opened_action_one_ingredient_no_auto_add(self): with open(self.testPath2, "rb") as test_file: file_content = test_file.read() - builder = Builder.from_json(manifestDefinition) + builder = Builder.from_json(manifestDefinition, context=settings_context) # Add C.jpg as the parent "opened" ingredient with open(self.testPath, 'rb') as f: @@ -5405,7 +5396,7 @@ def test_builder_opened_action_one_ingredient_no_auto_add(self): output_buffer.seek(0) # Read and verify the manifest - reader = Reader("image/jpeg", output_buffer) + reader = Reader("image/jpeg", output_buffer, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5417,13 +5408,11 @@ def test_builder_opened_action_one_ingredient_no_auto_add(self): builder.close() - # Make sure settings are put back to the common test defaults - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_builder_one_opened_one_placed_action_no_auto_add(self): """Test Builder with c2pa.opened action where asset is its own parent ingredient""" # Disable auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') # Instance IDs for linking ingredients and actions, # need to be unique even if the same binary file is used, so ingredients link properly to actions @@ -5493,7 +5482,7 @@ def test_builder_one_opened_one_placed_action_no_auto_add(self): with open(self.testPath2, "rb") as test_file: file_content = test_file.read() - builder = Builder.from_json(manifestDefinition) + builder = Builder.from_json(manifestDefinition, context=settings_context) # An asset can be its own parent ingredient! # We add A.jpg as its own parent ingredient @@ -5509,7 +5498,7 @@ def test_builder_one_opened_one_placed_action_no_auto_add(self): output_buffer.seek(0) # Read and verify the manifest - reader = Reader("image/jpeg", output_buffer) + reader = Reader("image/jpeg", output_buffer, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5523,13 +5512,11 @@ def test_builder_one_opened_one_placed_action_no_auto_add(self): builder.close() - # Make sure settings are put back to the common test defaults - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_builder_opened_action_multiple_ingredient_no_auto_add(self): """Test Builder with c2pa.opened and c2pa.placed actions with multiple ingredients""" # Disable auto-added actions, as what we are doing here can confuse auto-placements - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') # Instance IDs for linking ingredients and actions # With multiple ingredients, we need multiple different unique ids so they each link properly @@ -5606,7 +5593,7 @@ def test_builder_opened_action_multiple_ingredient_no_auto_add(self): with open(self.testPath2, "rb") as test_file: file_content = test_file.read() - builder = Builder.from_json(manifestDefinition) + builder = Builder.from_json(manifestDefinition, context=settings_context) # Add C.jpg as the parent ingredient (for c2pa.opened, it's the opened asset) with open(self.testPath, 'rb') as f1: @@ -5625,7 +5612,7 @@ def test_builder_opened_action_multiple_ingredient_no_auto_add(self): output_buffer.seek(0) # Read and verify the manifest - reader = Reader("image/jpeg", output_buffer) + reader = Reader("image/jpeg", output_buffer, context=settings_context) json_data = reader.json() manifest_data = json.loads(json_data) @@ -5640,13 +5627,11 @@ def test_builder_opened_action_multiple_ingredient_no_auto_add(self): builder.close() - # Make sure settings are put back to the common test defaults - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_link_archive_label_on_signing_builder_placed(self): """Label set on the signing builder's add_ingredient links an ingredient archive to a c2pa.placed action.""" - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') archive = self._create_ingredient_archive() @@ -5673,7 +5658,7 @@ def test_link_archive_label_on_signing_builder_placed(self): ], } - builder = Builder.from_json(manifest) + builder = Builder.from_json(manifest, context=settings_context) builder.add_ingredient( {"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"}, "application/c2pa", @@ -5685,7 +5670,7 @@ def test_link_archive_label_on_signing_builder_placed(self): builder.sign(self.signer, "image/jpeg", src, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) manifest_data = json.loads(reader.json()) active = manifest_data["active_manifest"] assertions = manifest_data["manifests"][active]["assertions"] @@ -5712,12 +5697,11 @@ def test_link_archive_label_on_signing_builder_placed(self): archive.close() builder.close() - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_link_archive_label_on_signing_builder_opened(self): """Label set on the signing builder's add_ingredient links an ingredient archive to a c2pa.opened action.""" - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') archive = self._create_ingredient_archive( {"title": "photo.jpg", "relationship": "parentOf"} @@ -5743,7 +5727,7 @@ def test_link_archive_label_on_signing_builder_opened(self): ], } - builder = Builder.from_json(manifest) + builder = Builder.from_json(manifest, context=settings_context) builder.add_ingredient( {"title": "photo.jpg", "relationship": "parentOf", "label": "my-ingredient"}, "application/c2pa", @@ -5755,7 +5739,7 @@ def test_link_archive_label_on_signing_builder_opened(self): builder.sign(self.signer, "image/jpeg", src, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) manifest_data = json.loads(reader.json()) active = manifest_data["active_manifest"] assertions = manifest_data["manifests"][active]["assertions"] @@ -5782,12 +5766,11 @@ def test_link_archive_label_on_signing_builder_opened(self): archive.close() builder.close() - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_link_archive_two_ingredients_labels(self): """Two ingredient archives linked to two different actions via distinct labels. Verifies no cross-linking.""" - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') archive1 = self._create_ingredient_archive( {"title": "photo-placed.jpg", "relationship": "componentOf"} @@ -5822,7 +5805,7 @@ def test_link_archive_two_ingredients_labels(self): ], } - builder = Builder.from_json(manifest) + builder = Builder.from_json(manifest, context=settings_context) builder.add_ingredient( {"title": "photo-placed.jpg", "relationship": "componentOf", "label": "ingredient-for-placed"}, "application/c2pa", @@ -5839,7 +5822,7 @@ def test_link_archive_two_ingredients_labels(self): builder.sign(self.signer, "image/jpeg", src, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) manifest_data = json.loads(reader.json()) active = manifest_data["active_manifest"] assertions = manifest_data["manifests"][active]["assertions"] @@ -5875,12 +5858,11 @@ def test_link_archive_two_ingredients_labels(self): archive2.close() builder.close() - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_link_archive_multiple_ingredients_in_one_placed_action(self): """A single c2pa.placed action references two componentOf ingredients via ingredientIds with two labels.""" - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + settings_context = _context_from_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') archive1 = self._create_ingredient_archive( {"title": "base-layer.jpg", "relationship": "componentOf"} @@ -5912,7 +5894,7 @@ def test_link_archive_multiple_ingredients_in_one_placed_action(self): ], } - builder = Builder.from_json(manifest) + builder = Builder.from_json(manifest, context=settings_context) builder.add_ingredient( {"title": "base-layer.jpg", "relationship": "componentOf", "label": "base-layer"}, "application/c2pa", @@ -5929,7 +5911,7 @@ def test_link_archive_multiple_ingredients_in_one_placed_action(self): builder.sign(self.signer, "image/jpeg", src, output) output.seek(0) - reader = Reader("image/jpeg", output) + reader = Reader("image/jpeg", output, context=settings_context) manifest_data = json.loads(reader.json()) active = manifest_data["active_manifest"] assertions = manifest_data["manifests"][active]["assertions"] @@ -5960,7 +5942,6 @@ def test_link_archive_multiple_ingredients_in_one_placed_action(self): archive2.close() builder.close() - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') def test_ingredient_fields_survive_archive(self): archive = self._create_ingredient_archive({ @@ -8376,13 +8357,13 @@ def test_construction_failure_leaves_nothing_to_free(self): finally: c2pa_module._lib.c2pa_context_new = real_new - real_json = c2pa_module._lib.c2pa_builder_from_json - c2pa_module._lib.c2pa_builder_from_json = lambda j: None + real_from_context = c2pa_module._lib.c2pa_builder_from_context + c2pa_module._lib.c2pa_builder_from_context = lambda c: None try: with self.assertRaises(Error): Builder({"claim_generator": "test"}) finally: - c2pa_module._lib.c2pa_builder_from_json = real_json + c2pa_module._lib.c2pa_builder_from_context = real_from_context def test_context_build_null_return_frees_builder(self): # Mock a pointer rejection. @@ -9345,7 +9326,11 @@ def _raw_builder_handle(self): manifest = json.dumps( {"claim_generator": "raw_ffi_test", "format": "image/jpeg"} ).encode("utf-8") - handle = c2pa_module._lib.c2pa_builder_from_json(manifest) + handle = c2pa_module._lib.c2pa_builder_from_context( + c2pa_module._get_default_context().execution_context) + self.assertTrue(handle, "the FFI did not return a builder pointer") + handle = c2pa_module._lib.c2pa_builder_with_definition( + handle, manifest) self.assertTrue(handle, "the FFI did not return a builder pointer") return handle