Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 382 additions & 1 deletion docs/selective-manifests.md

Large diffs are not rendered by default.

444 changes: 444 additions & 0 deletions docs/working-stores.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "c2pa-python"
version = "0.35.0"
version = "0.35.1"
requires-python = ">=3.10"
description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
readme = { file = "README.md", content-type = "text/markdown" }
Expand All @@ -17,7 +17,8 @@ classifiers = [
"Operating System :: Microsoft :: Windows"
]
maintainers = [
{name = "Gavin Peacock", email = "gpeacock@adobe.com"}
{name = "Gavin Peacock", email = "gpeacock@adobe.com"},
{name = "Tania Mathern", email = "mathern@adobe.com"}
]
urls = {homepage = "https://contentauthenticity.org", repository = "https://github.com/contentauth/c2pa-python"}
dependencies = [
Expand Down
61 changes: 61 additions & 0 deletions src/c2pa/c2pa.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
'c2pa_builder_add_ingredient_from_stream',
'c2pa_builder_add_action',
'c2pa_builder_to_archive',
'c2pa_builder_write_ingredient_archive',
'c2pa_builder_add_ingredient_from_archive',
'c2pa_builder_sign',
'c2pa_builder_sign_context',
'c2pa_builder_from_context',
Expand Down Expand Up @@ -624,6 +626,15 @@ def _setup_function(func, argtypes, restype=None):
_setup_function(_lib.c2pa_builder_to_archive,
[ctypes.POINTER(C2paBuilder), ctypes.POINTER(C2paStream)],
ctypes.c_int)
_setup_function(_lib.c2pa_builder_write_ingredient_archive,
[ctypes.POINTER(C2paBuilder),
ctypes.c_char_p,
ctypes.POINTER(C2paStream)],
ctypes.c_int)
_setup_function(_lib.c2pa_builder_add_ingredient_from_archive,
[ctypes.POINTER(C2paBuilder),
ctypes.POINTER(C2paStream)],
ctypes.c_int)
_setup_function(_lib.c2pa_builder_sign,
[ctypes.POINTER(C2paBuilder),
ctypes.c_char_p,
Expand Down Expand Up @@ -2915,6 +2926,7 @@ class Builder(ManagedResource):
'url_error': "Error setting remote URL: {}",
'resource_error': "Error adding resource: {}",
'ingredient_error': "Error adding ingredient: {}",
'archive_read_error': "Error loading ingredient from archive: {}",
'action_error': "Error adding action: {}",
'archive_error': "Error writing archive: {}",
'sign_error': "Error during signing: {}",
Expand Down Expand Up @@ -3304,6 +3316,55 @@ def to_archive(self, stream: Any) -> None:
Builder._ERROR_MESSAGES["archive_error"].format("Unknown error"),
check=lambda r: r != 0)

def write_ingredient_archive(self, ingredient_id: str, stream: Any) -> None:
"""Write a single-ingredient archive for the named ingredient.
The archive is in C2PA format.

Args:
ingredient_id: Identifier of the ingredient within this builder;
matched against the ingredient's label or instance_id
stream: Writable, seekable stream to receive the archive

Raises:
C2paError: If there was an error writing the archive
"""
self._ensure_valid_state()

ingredient_id_str = _to_utf8_bytes(ingredient_id, "ingredient_id")

with Stream(stream) as stream_obj:
result = _lib.c2pa_builder_write_ingredient_archive(
self._handle, ingredient_id_str, stream_obj._stream)

_check_ffi_operation_result(result,
Builder._ERROR_MESSAGES["archive_error"].format(
"Unknown error"
),
check=lambda r: r != 0)

def add_ingredient_from_archive(self, stream: Any) -> None:
"""Add an ingredient from a per-ingredient archive stream.
The archive must be in C2PA format, as written by
write_ingredient_archive.

Args:
stream: Readable, seekable stream containing the ingredient archive

Raises:
C2paError: If there was an error reading the archive
"""
self._ensure_valid_state()

with Stream(stream) as stream_obj:
result = _lib.c2pa_builder_add_ingredient_from_archive(
self._handle, stream_obj._stream)

_check_ffi_operation_result(result,
Builder._ERROR_MESSAGES["archive_read_error"].format(
"Unknown error"
),
check=lambda r: r != 0)

def with_archive(self, stream: Any) -> 'Builder':
"""Load an archive into this Builder instance, replacing its
manifest definition. The archive carries only the
Expand Down
170 changes: 170 additions & 0 deletions tests/perf/scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
_PLACED_ID3 = "xmp:iid:ffffffff-0006-0006-0006-ffffffffffff"
_PLACED_ID4 = "xmp:iid:11111111-0007-0007-0007-111111111111"
_PLACED_ID5 = "xmp:iid:22222222-0008-0008-0008-222222222222"
_ARCH_PARENT_ID = "xmp:iid:33333333-0009-0009-0009-333333333333"
_ARCH_COMP_ID = "xmp:iid:44444444-0010-0010-0010-444444444444"
_ARCH_COMP_ID2 = "xmp:iid:55555555-0011-0011-0011-555555555555"

MANIFEST_BASE = {
"claim_generator": "perf_test",
Expand Down Expand Up @@ -474,6 +477,167 @@ def scenario_builder_sign_jpeg_archive_roundtrip(iterations: int = 100) -> None:
builder.sign("image/jpeg", io.BytesIO(source_bytes), io.BytesIO())


# Archive scenarios: builder as working store (to_archive/with_archive) and
# per-ingredient archives (write_ingredient_archive/add_ingredient_from_archive).

def _ingredient_archive_bytes(ingredient_json: dict, mime: str, asset_bytes: bytes) -> bytes:
"""Build a per-ingredient archive once, for reuse inside scenario loops."""
builder = Builder(MANIFEST_BASE)
with io.BytesIO(asset_bytes) as ing:
builder.add_ingredient(ingredient_json, mime, ing)
archive = io.BytesIO()
builder.write_ingredient_archive(ingredient_json["instance_id"], archive)
return archive.getvalue()


def scenario_builder_to_archive_with_ingredient(iterations: int = 100) -> None:
"""Serialize a builder holding one ingredient to an archive (no signing)."""
ingredient_bytes = SIGNED_JPEG.read_bytes()
for _ in _iterate(iterations):
builder = Builder(MANIFEST_BASE)
with io.BytesIO(ingredient_bytes) as ing:
builder.add_ingredient(
{"relationship": "parentOf", "instance_id": _ARCH_PARENT_ID},
"image/jpeg", ing,
)
builder.to_archive(io.BytesIO())


def scenario_builder_sign_jpeg_archive_roundtrip_ingredient_in_archive(iterations: int = 100) -> None:
"""Add ingredient, serialize to archive, reload, sign.

Unlike scenario_builder_sign_jpeg_archive_roundtrip, the ingredient is
added before to_archive, so its resources travel through the archive.
"""
context = Context(signer=_make_signer())
source_bytes = SOURCE_JPEG.read_bytes()
ingredient_bytes = SIGNED_JPEG.read_bytes()
manifest = {
**MANIFEST_BASE,
"assertions": [{
"label": "c2pa.actions.v2",
"data": {"actions": [{
"action": "c2pa.opened",
"softwareAgent": {"name": "perf_test"},
"parameters": {"ingredientIds": [_ARCH_PARENT_ID]},
"digitalSourceType": _DST_COMPOSITE,
}]},
}],
}
for _ in _iterate(iterations):
archive = io.BytesIO()
src_builder = Builder(manifest)
with io.BytesIO(ingredient_bytes) as ing:
src_builder.add_ingredient(
{"relationship": "parentOf", "instance_id": _ARCH_PARENT_ID},
"image/jpeg", ing,
)
src_builder.to_archive(archive)
archive.seek(0)
builder = Builder(manifest, context=context).with_archive(archive)
builder.sign("image/jpeg", io.BytesIO(source_bytes), io.BytesIO())


def scenario_builder_write_ingredient_archive(iterations: int = 100) -> None:
"""Add one ingredient and write it out as a per-ingredient archive."""
ingredient_bytes = SIGNED_JPEG.read_bytes()
for _ in _iterate(iterations):
builder = Builder(MANIFEST_BASE)
with io.BytesIO(ingredient_bytes) as ing:
builder.add_ingredient(
{"relationship": "parentOf", "instance_id": _ARCH_PARENT_ID},
"image/jpeg", ing,
)
builder.write_ingredient_archive(_ARCH_PARENT_ID, io.BytesIO())


def scenario_builder_sign_jpeg_add_ingredient_from_archive(iterations: int = 100) -> None:
"""Restore one ingredient from a prebuilt archive and sign."""
context = Context(signer=_make_signer())
source_bytes = SOURCE_JPEG.read_bytes()
archive_bytes = _ingredient_archive_bytes(
{"relationship": "parentOf", "instance_id": _ARCH_PARENT_ID},
"image/jpeg", SIGNED_JPEG.read_bytes(),
)
manifest = {
**MANIFEST_BASE,
"assertions": [{
"label": "c2pa.actions.v2",
"data": {"actions": [{
"action": "c2pa.opened",
"softwareAgent": {"name": "perf_test"},
"parameters": {"ingredientIds": [_ARCH_PARENT_ID]},
"digitalSourceType": _DST_COMPOSITE,
}]},
}],
}
for _ in _iterate(iterations):
builder = Builder(manifest, context=context)
builder.add_ingredient_from_archive(io.BytesIO(archive_bytes))
builder.sign("image/jpeg", io.BytesIO(source_bytes), io.BytesIO())


def scenario_builder_ingredient_archive_roundtrip(iterations: int = 100) -> None:
"""Write a per-ingredient archive from one builder, load into another, sign."""
context = Context(signer=_make_signer())
source_bytes = SOURCE_JPEG.read_bytes()
ingredient_bytes = SIGNED_JPEG.read_bytes()
manifest = {
**MANIFEST_BASE,
"assertions": [{
"label": "c2pa.actions.v2",
"data": {"actions": [{
"action": "c2pa.opened",
"softwareAgent": {"name": "perf_test"},
"parameters": {"ingredientIds": [_ARCH_PARENT_ID]},
"digitalSourceType": _DST_COMPOSITE,
}]},
}],
}
for _ in _iterate(iterations):
archive = io.BytesIO()
src_builder = Builder(MANIFEST_BASE)
with io.BytesIO(ingredient_bytes) as ing:
src_builder.add_ingredient(
{"relationship": "parentOf", "instance_id": _ARCH_PARENT_ID},
"image/jpeg", ing,
)
src_builder.write_ingredient_archive(_ARCH_PARENT_ID, archive)
archive.seek(0)
builder = Builder(manifest, context=context)
builder.add_ingredient_from_archive(archive)
builder.sign("image/jpeg", io.BytesIO(source_bytes), io.BytesIO())


def scenario_builder_sign_jpeg_two_ingredient_archives(iterations: int = 100) -> None:
"""Restore two ingredients (JPEG + PNG) from prebuilt archives and sign."""
context = Context(signer=_make_signer())
source_bytes = SOURCE_JPEG.read_bytes()
archive1_bytes = _ingredient_archive_bytes(
{"relationship": "componentOf", "instance_id": _ARCH_COMP_ID},
"image/jpeg", SIGNED_JPEG.read_bytes(),
)
archive2_bytes = _ingredient_archive_bytes(
{"relationship": "componentOf", "instance_id": _ARCH_COMP_ID2},
"image/png", SIGNING_PNG.read_bytes(),
)
manifest = {
**MANIFEST_BASE,
"assertions": [{
"label": "c2pa.actions.v2",
"data": {"actions": [{
"action": "c2pa.placed",
"softwareAgent": {"name": "perf_test"},
"parameters": {"ingredientIds": [_ARCH_COMP_ID, _ARCH_COMP_ID2]},
"digitalSourceType": _DST_COMPOSITE,
}]},
}],
}
for _ in _iterate(iterations):
builder = Builder(manifest, context=context)
builder.add_ingredient_from_archive(io.BytesIO(archive1_bytes))
builder.add_ingredient_from_archive(io.BytesIO(archive2_bytes))
builder.sign("image/jpeg", io.BytesIO(source_bytes), io.BytesIO())
def scenario_reader_error_no_manifest(iterations: int = 100) -> None:
"""Reader on an unsigned asset: partial-init cleanup."""
source_bytes = SOURCE_JPEG.read_bytes() # A.jpg carries no manifest
Expand Down Expand Up @@ -752,6 +916,12 @@ def scenario_fork_stream_cleanup(iterations: int = 100) -> None:
"builder_sign_jpeg_two_components_same_mime": scenario_builder_sign_jpeg_two_components_same_mime,
"builder_sign_jpeg_two_components_mixed_mime": scenario_builder_sign_jpeg_two_components_mixed_mime,
"builder_sign_jpeg_archive_roundtrip": scenario_builder_sign_jpeg_archive_roundtrip,
"builder_to_archive_with_ingredient": scenario_builder_to_archive_with_ingredient,
"builder_sign_jpeg_archive_roundtrip_ingredient_in_archive": scenario_builder_sign_jpeg_archive_roundtrip_ingredient_in_archive,
"builder_write_ingredient_archive": scenario_builder_write_ingredient_archive,
"builder_sign_jpeg_add_ingredient_from_archive": scenario_builder_sign_jpeg_add_ingredient_from_archive,
"builder_ingredient_archive_roundtrip": scenario_builder_ingredient_archive_roundtrip,
"builder_sign_jpeg_two_ingredient_archives": scenario_builder_sign_jpeg_two_ingredient_archives,
"reader_error_no_manifest": scenario_reader_error_no_manifest,
"builder_error_invalid_manifest": scenario_builder_error_invalid_manifest,
"reader_string_apis": scenario_reader_string_apis,
Expand Down
Loading
Loading