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
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
cmake_minimum_required(VERSION 3.27)

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

# Set the version of the c2pa_rs library used
set(C2PA_VERSION "0.76.2")
set(C2PA_VERSION "0.77.0")

set(CMAKE_POLICY_DEFAULT_CMP0135 NEW)
set(CMAKE_C_STANDARD 17)
Expand Down
53 changes: 53 additions & 0 deletions include/c2pa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,59 @@ namespace c2pa
/// @return A formatted copy of the data.
static std::vector<unsigned char> format_embeddable(const std::string &format, std::vector<unsigned char> &data);

/// @brief Check if the given format requires a placeholder embedding step.
/// @details Returns false for BoxHash-capable formats when prefer_box_hash is enabled in
/// the context settings (no placeholder needed — hash covers the full asset).
/// Always returns true for BMFF formats (MP4, etc.) regardless of settings.
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
/// @return true if placeholder() must be called and embedded before sign_embeddable(); false otherwise.
/// @throws C2paException on error.
bool needs_placeholder(const std::string &format);

/// @brief Create a composed placeholder manifest to embed in the asset.
/// @details The signer (and its reserve size) are obtained from the Builder's Context.
/// For BMFF assets, if core.merkle_tree_chunk_size_in_kb is set in the context
/// settings, the placeholder will include pre-allocated Merkle map slots.
/// Returns empty bytes for formats that do not need a placeholder (BoxHash).
/// The placeholder size is stored internally so sign_embeddable() returns bytes
/// of exactly the same size, enabling in-place patching.
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
/// @return Composed placeholder bytes ready to embed into the asset.
/// @throws C2paException on error.
std::vector<unsigned char> placeholder(const std::string &format);

/// @brief Register the byte ranges where the placeholder was embedded (DataHash workflow).
/// @details Call this after embedding the placeholder bytes into the asset and before
/// update_hash_from_stream(). The exclusions replace the dummy ranges set by
/// placeholder() so the asset hash covers all bytes except the manifest slot.
/// Exclusions are (start, length) pairs in asset byte coordinates.
/// @param exclusions Vector of (start, length) pairs describing the embedded placeholder region.
/// @throws C2paException if no DataHash assertion exists or on other error.
void set_data_hash_exclusions(const std::vector<std::pair<uint64_t, uint64_t>> &exclusions);

/// @brief Compute and store the asset hash by reading a stream.
/// @details Automatically detects the hard binding type from the builder state:
/// - DataHash: uses exclusion ranges already registered via set_data_hash_exclusions().
/// - BmffHash: uses path-based exclusions from the BMFF assertion (UUID box, mdat).
/// - BoxHash: hashes each format-specific box individually.
/// Call set_data_hash_exclusions() before this for DataHash workflows.
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
/// @param stream The asset stream to hash. Must include the embedded placeholder bytes.
/// @throws C2paException on error.
void update_hash_from_stream(const std::string &format, std::istream &stream);

/// @brief Sign and return the final manifest bytes, ready for embedding.
/// @details Operates in two modes:
/// - Placeholder mode (after placeholder()): zero-pads the signed manifest to the
/// pre-committed placeholder size, enabling in-place patching of the asset.
/// - Direct mode (no placeholder): returns the actual signed manifest size.
/// Requires a valid hard binding assertion (set via update_hash_from_stream()).
/// The signer is obtained from the Builder's Context.
/// @param format The MIME type or extension of the asset (e.g. "image/jpeg", "video/mp4").
/// @return Signed manifest bytes ready to embed into the asset.
/// @throws C2paException on error.
std::vector<unsigned char> sign_embeddable(const std::string &format);

/// @brief Get a list of mime types that the Builder supports.
/// @return Vector of supported MIME type strings.
static std::vector<std::string> supported_mime_types();
Expand Down
51 changes: 51 additions & 0 deletions src/c2pa_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,57 @@ namespace c2pa
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

bool Builder::needs_placeholder(const std::string &format)
{
int result = c2pa_builder_needs_placeholder(builder, format.c_str());
if (result < 0)
{
throw C2paException();
}
return result == 1;
}

std::vector<unsigned char> Builder::placeholder(const std::string &format)
{
const unsigned char *c2pa_manifest_bytes = nullptr;
auto result = c2pa_builder_placeholder(builder, format.c_str(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

void Builder::set_data_hash_exclusions(const std::vector<std::pair<uint64_t, uint64_t>> &exclusions)
{
std::vector<uint64_t> flat;
flat.reserve(exclusions.size() * 2);
for (const auto &[start, len] : exclusions)
{
flat.push_back(start);
flat.push_back(len);
}
int result = c2pa_builder_set_data_hash_exclusions(
builder, flat.data(), exclusions.size());
if (result < 0)
{
throw C2paException();
}
}

void Builder::update_hash_from_stream(const std::string &format, std::istream &stream)
{
CppIStream c_stream(stream);
int result = c2pa_builder_update_hash_from_stream(builder, format.c_str(), c_stream.c_stream);
if (result < 0)
{
throw C2paException();
}
}

std::vector<unsigned char> Builder::sign_embeddable(const std::string &format)
{
const unsigned char *c2pa_manifest_bytes = nullptr;
auto result = c2pa_builder_sign_embeddable(builder, format.c_str(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

std::vector<unsigned char> Builder::format_embeddable(const std::string &format, std::vector<unsigned char> &data)
{
const unsigned char *c2pa_manifest_bytes = nullptr;
Expand Down
49 changes: 49 additions & 0 deletions tests/embeddable.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,55 @@ TEST_F(EmbeddableTest, FullWorkflowWithAJpg) {
<< "Signed manifest size must match placeholder size for in-place patching";
}

// e2e workflow with A.jpg using new context-signer embeddable APIs
TEST_F(EmbeddableTest, FullWorkflowWithAJpgContextSigner) {
// Demonstrates the new embeddable API where the signer lives on the context
Comment thread
tmathern marked this conversation as resolved.
// rather than being passed explicitly to placeholder/sign methods.
//
// Workflow:
// 1. needs_placeholder() — check whether a placeholder embed step is required
// 2. placeholder() — get the correctly-sized placeholder bytes to embed
// 3. set_data_hash_exclusions() — register where the placeholder was embedded
// 4. update_hash_from_stream() — hash the asset (SDK skips exclusion ranges)
// 5. sign_embeddable() — sign; output size is exactly placeholder.size()

auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json"));
auto source_asset = c2pa_test::get_fixture_path("A.jpg");

// Create a context with the signer attached programmatically
auto context = c2pa::Context::ContextBuilder()
.with_signer(c2pa_test::create_test_signer())
Comment thread
gpeacock marked this conversation as resolved.
.create_context();

auto builder = c2pa::Builder(context, manifest_json);

// 1: Check if a placeholder is required for JPEG
ASSERT_TRUE(builder.needs_placeholder("image/jpeg"))
<< "JPEG always requires a placeholder";

// 2: Get the placeholder bytes (size is committed internally for later validation)
auto placeholder_bytes = builder.placeholder("image/jpeg");
ASSERT_GT(placeholder_bytes.size(), 0) << "Placeholder must not be empty";
size_t placeholder_size = placeholder_bytes.size();

// 3: Register where we would embed the placeholder.
// In production this offset is where you physically write placeholder_bytes.
size_t embed_offset = 20;
builder.set_data_hash_exclusions({{embed_offset, placeholder_bytes.size()}});

// 4: Hash the asset stream, skipping the exclusion range
std::ifstream asset_stream(source_asset, std::ios::binary);
ASSERT_TRUE(asset_stream.is_open());
builder.update_hash_from_stream("image/jpeg", asset_stream);
asset_stream.close();

// 5: Sign — output must match the placeholder size (enables in-place patching)
auto signed_manifest = builder.sign_embeddable("image/jpeg");
ASSERT_GT(signed_manifest.size(), 0);
EXPECT_EQ(signed_manifest.size(), placeholder_size)
<< "Signed manifest must be exactly the same size as the placeholder";
}

// e2e workflow with C.jpg (has existing C2PA metadata)
TEST_F(EmbeddableTest, FullWorkflowWithCJpg) {
auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json"));
Expand Down