Skip to content

Commit f22ec1d

Browse files
authored
Merge pull request #170 from contentauth/gpeacock/embeddable_api
feat: Add new embeddable api support The older bmff_hashed_embeddable and box_hashed_embedable apis will be deprecated soon.
2 parents d09aa8d + 6f93d58 commit f22ec1d

4 files changed

Lines changed: 155 additions & 2 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
cmake_minimum_required(VERSION 3.27)
1515

1616
# This is the version of this C++ project
17-
project(c2pa-c VERSION 0.14.1)
17+
project(c2pa-c VERSION 0.15.0)
1818

1919
# Set the version of the c2pa_rs library used
20-
set(C2PA_VERSION "0.76.2")
20+
set(C2PA_VERSION "0.77.0")
2121

2222
set(CMAKE_POLICY_DEFAULT_CMP0135 NEW)
2323
set(CMAKE_C_STANDARD 17)

include/c2pa.hpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,59 @@ namespace c2pa
10221022
/// @return A formatted copy of the data.
10231023
static std::vector<unsigned char> format_embeddable(const std::string &format, std::vector<unsigned char> &data);
10241024

1025+
/// @brief Check if the given format requires a placeholder embedding step.
1026+
/// @details Returns false for BoxHash-capable formats when prefer_box_hash is enabled in
1027+
/// the context settings (no placeholder needed — hash covers the full asset).
1028+
/// Always returns true for BMFF formats (MP4, etc.) regardless of settings.
1029+
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
1030+
/// @return true if placeholder() must be called and embedded before sign_embeddable(); false otherwise.
1031+
/// @throws C2paException on error.
1032+
bool needs_placeholder(const std::string &format);
1033+
1034+
/// @brief Create a composed placeholder manifest to embed in the asset.
1035+
/// @details The signer (and its reserve size) are obtained from the Builder's Context.
1036+
/// For BMFF assets, if core.merkle_tree_chunk_size_in_kb is set in the context
1037+
/// settings, the placeholder will include pre-allocated Merkle map slots.
1038+
/// Returns empty bytes for formats that do not need a placeholder (BoxHash).
1039+
/// The placeholder size is stored internally so sign_embeddable() returns bytes
1040+
/// of exactly the same size, enabling in-place patching.
1041+
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
1042+
/// @return Composed placeholder bytes ready to embed into the asset.
1043+
/// @throws C2paException on error.
1044+
std::vector<unsigned char> placeholder(const std::string &format);
1045+
1046+
/// @brief Register the byte ranges where the placeholder was embedded (DataHash workflow).
1047+
/// @details Call this after embedding the placeholder bytes into the asset and before
1048+
/// update_hash_from_stream(). The exclusions replace the dummy ranges set by
1049+
/// placeholder() so the asset hash covers all bytes except the manifest slot.
1050+
/// Exclusions are (start, length) pairs in asset byte coordinates.
1051+
/// @param exclusions Vector of (start, length) pairs describing the embedded placeholder region.
1052+
/// @throws C2paException if no DataHash assertion exists or on other error.
1053+
void set_data_hash_exclusions(const std::vector<std::pair<uint64_t, uint64_t>> &exclusions);
1054+
1055+
/// @brief Compute and store the asset hash by reading a stream.
1056+
/// @details Automatically detects the hard binding type from the builder state:
1057+
/// - DataHash: uses exclusion ranges already registered via set_data_hash_exclusions().
1058+
/// - BmffHash: uses path-based exclusions from the BMFF assertion (UUID box, mdat).
1059+
/// - BoxHash: hashes each format-specific box individually.
1060+
/// Call set_data_hash_exclusions() before this for DataHash workflows.
1061+
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
1062+
/// @param stream The asset stream to hash. Must include the embedded placeholder bytes.
1063+
/// @throws C2paException on error.
1064+
void update_hash_from_stream(const std::string &format, std::istream &stream);
1065+
1066+
/// @brief Sign and return the final manifest bytes, ready for embedding.
1067+
/// @details Operates in two modes:
1068+
/// - Placeholder mode (after placeholder()): zero-pads the signed manifest to the
1069+
/// pre-committed placeholder size, enabling in-place patching of the asset.
1070+
/// - Direct mode (no placeholder): returns the actual signed manifest size.
1071+
/// Requires a valid hard binding assertion (set via update_hash_from_stream()).
1072+
/// The signer is obtained from the Builder's Context.
1073+
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
1074+
/// @return Signed manifest bytes ready to embed into the asset.
1075+
/// @throws C2paException on error.
1076+
std::vector<unsigned char> sign_embeddable(const std::string &format);
1077+
10251078
/// @brief Get a list of mime types that the Builder supports.
10261079
/// @return Vector of supported MIME type strings.
10271080
static std::vector<std::string> supported_mime_types();

src/c2pa_builder.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,57 @@ namespace c2pa
336336
return detail::to_byte_vector(c2pa_manifest_bytes, result);
337337
}
338338

339+
bool Builder::needs_placeholder(const std::string &format)
340+
{
341+
int result = c2pa_builder_needs_placeholder(builder, format.c_str());
342+
if (result < 0)
343+
{
344+
throw C2paException();
345+
}
346+
return result == 1;
347+
}
348+
349+
std::vector<unsigned char> Builder::placeholder(const std::string &format)
350+
{
351+
const unsigned char *c2pa_manifest_bytes = nullptr;
352+
auto result = c2pa_builder_placeholder(builder, format.c_str(), &c2pa_manifest_bytes);
353+
return detail::to_byte_vector(c2pa_manifest_bytes, result);
354+
}
355+
356+
void Builder::set_data_hash_exclusions(const std::vector<std::pair<uint64_t, uint64_t>> &exclusions)
357+
{
358+
std::vector<uint64_t> flat;
359+
flat.reserve(exclusions.size() * 2);
360+
for (const auto &[start, len] : exclusions)
361+
{
362+
flat.push_back(start);
363+
flat.push_back(len);
364+
}
365+
int result = c2pa_builder_set_data_hash_exclusions(
366+
builder, flat.data(), exclusions.size());
367+
if (result < 0)
368+
{
369+
throw C2paException();
370+
}
371+
}
372+
373+
void Builder::update_hash_from_stream(const std::string &format, std::istream &stream)
374+
{
375+
CppIStream c_stream(stream);
376+
int result = c2pa_builder_update_hash_from_stream(builder, format.c_str(), c_stream.c_stream);
377+
if (result < 0)
378+
{
379+
throw C2paException();
380+
}
381+
}
382+
383+
std::vector<unsigned char> Builder::sign_embeddable(const std::string &format)
384+
{
385+
const unsigned char *c2pa_manifest_bytes = nullptr;
386+
auto result = c2pa_builder_sign_embeddable(builder, format.c_str(), &c2pa_manifest_bytes);
387+
return detail::to_byte_vector(c2pa_manifest_bytes, result);
388+
}
389+
339390
std::vector<unsigned char> Builder::format_embeddable(const std::string &format, std::vector<unsigned char> &data)
340391
{
341392
const unsigned char *c2pa_manifest_bytes = nullptr;

tests/embeddable.test.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,55 @@ TEST_F(EmbeddableTest, FullWorkflowWithAJpg) {
120120
<< "Signed manifest size must match placeholder size for in-place patching";
121121
}
122122

123+
// e2e workflow with A.jpg using new context-signer embeddable APIs
124+
TEST_F(EmbeddableTest, FullWorkflowWithAJpgContextSigner) {
125+
// Demonstrates the new embeddable API where the signer lives on the context
126+
// rather than being passed explicitly to placeholder/sign methods.
127+
//
128+
// Workflow:
129+
// 1. needs_placeholder() — check whether a placeholder embed step is required
130+
// 2. placeholder() — get the correctly-sized placeholder bytes to embed
131+
// 3. set_data_hash_exclusions() — register where the placeholder was embedded
132+
// 4. update_hash_from_stream() — hash the asset (SDK skips exclusion ranges)
133+
// 5. sign_embeddable() — sign; output size is exactly placeholder.size()
134+
135+
auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json"));
136+
auto source_asset = c2pa_test::get_fixture_path("A.jpg");
137+
138+
// Create a context with the signer attached programmatically
139+
auto context = c2pa::Context::ContextBuilder()
140+
.with_signer(c2pa_test::create_test_signer())
141+
.create_context();
142+
143+
auto builder = c2pa::Builder(context, manifest_json);
144+
145+
// 1: Check if a placeholder is required for JPEG
146+
ASSERT_TRUE(builder.needs_placeholder("image/jpeg"))
147+
<< "JPEG always requires a placeholder";
148+
149+
// 2: Get the placeholder bytes (size is committed internally for later validation)
150+
auto placeholder_bytes = builder.placeholder("image/jpeg");
151+
ASSERT_GT(placeholder_bytes.size(), 0) << "Placeholder must not be empty";
152+
size_t placeholder_size = placeholder_bytes.size();
153+
154+
// 3: Register where we would embed the placeholder.
155+
// In production this offset is where you physically write placeholder_bytes.
156+
size_t embed_offset = 20;
157+
builder.set_data_hash_exclusions({{embed_offset, placeholder_bytes.size()}});
158+
159+
// 4: Hash the asset stream, skipping the exclusion range
160+
std::ifstream asset_stream(source_asset, std::ios::binary);
161+
ASSERT_TRUE(asset_stream.is_open());
162+
builder.update_hash_from_stream("image/jpeg", asset_stream);
163+
asset_stream.close();
164+
165+
// 5: Sign — output must match the placeholder size (enables in-place patching)
166+
auto signed_manifest = builder.sign_embeddable("image/jpeg");
167+
ASSERT_GT(signed_manifest.size(), 0);
168+
EXPECT_EQ(signed_manifest.size(), placeholder_size)
169+
<< "Signed manifest must be exactly the same size as the placeholder";
170+
}
171+
123172
// e2e workflow with C.jpg (has existing C2PA metadata)
124173
TEST_F(EmbeddableTest, FullWorkflowWithCJpg) {
125174
auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json"));

0 commit comments

Comments
 (0)