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
12 changes: 12 additions & 0 deletions .cursor/rules/git-workflow.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
description: Git commit and branch workflow preferences
alwaysApply: true
---

# Git Workflow

- **Never commit without explicit approval.** After making changes, show a summary and ask the user if they want to commit before running any `git commit` command.
- **Run the project's unit tests and confirm they pass before proposing a commit.** If tests fail, fix them first. For this repo that is `make test`.
- Never push to remote unless explicitly asked.
- Never force-push.
- Always show `git diff --stat` or a plain-language summary of changes before proposing a commit message.
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 current version of this C++ project
project(c2pa-c VERSION 0.20.0)
project(c2pa-c VERSION 0.20.1)

# Set the version of the c2pa_rs library used
set(C2PA_VERSION "0.78.6")
set(C2PA_VERSION "0.78.7")

set(CMAKE_POLICY_DEFAULT_CMP0135 NEW)
set(CMAKE_C_STANDARD 17)
Expand Down
124 changes: 124 additions & 0 deletions docs/context-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,132 @@ auto context = c2pa::Context::ContextBuilder()
| `with_settings(settings)` | Apply a `Settings` object |
| `with_json(json_string)` | Apply settings from a JSON string |
| `with_json_settings_file(path)` | Load and apply settings from a JSON file |
| `with_signer(signer)` | Store a `Signer` in the context (consumed; used by `Builder::sign` with no explicit signer) |
| `with_progress_callback(callback)` | Register a progress/cancel callback (see [Progress callbacks and cancellation](#progress-callbacks-and-cancellation)) |
| `create_context()` | Build and return the `Context` (consumes the builder) |

## Progress callbacks and cancellation

You can register a callback on a `Context` to receive progress notifications during signing and reading operations, and to cancel an operation in flight.

### Registering a callback

Use `ContextBuilder::with_progress_callback` to attach a callback before building the context:

```cpp
#include <atomic>

std::atomic<int> phase_count{0};

auto context = c2pa::Context::ContextBuilder()
.with_progress_callback([&](c2pa::ProgressPhase phase, uint32_t step, uint32_t total) {
++phase_count;
// Return true to continue, false to cancel.
return true;
})
.create_context();

// Use the context normally — the callback fires automatically.
c2pa::Builder builder(context, manifest_json);
builder.sign("source.jpg", "output.jpg", signer);
```

The callback signature is:

```cpp
bool callback(c2pa::ProgressPhase phase, uint32_t step, uint32_t total);
```

- **`phase`** — which stage the SDK is in (see [`ProgressPhase` values](#progressphase-values) below).
- **`step`** — monotonically increasing counter within the current phase, starting at `1`. Resets to `1` at the start of each new phase. Use as a liveness signal: a rising `step` means the SDK is making forward progress.
- **`total`** — `0` = indeterminate (show a spinner); `1` = single-shot phase; `> 1` = determinate (`step / total` gives a completion fraction).
- **Return value** — return `true` to continue, `false` to request cancellation (same effect as calling `context.cancel()`).

**Do not throw** from the progress callback. Exceptions cannot cross the C/Rust boundary safely; if your callback throws, the wrapper catches it and the operation is aborted as a cancellation (you do not get your exception back at the call site). Use `return false`, `context.cancel()`, or application-side state instead.

### Cancelling from another thread

You may call `Context::cancel()` from another thread while the same `Context` remains valid and is not being destroyed or moved concurrently with that call. The SDK returns a `C2paException` with an `OperationCancelled` error at the next progress checkpoint:

```cpp
#include <thread>

auto context = c2pa::Context::ContextBuilder()
.with_progress_callback([](c2pa::ProgressPhase, uint32_t, uint32_t) {
return true; // Don't cancel from the callback — use cancel() instead.
})
.create_context();

// Kick off a cancel after 500 ms from a background thread.
std::thread cancel_thread([&context]() {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
context.cancel();
});

try {
c2pa::Builder builder(context, manifest_json);
builder.sign("large_file.jpg", "output.jpg", signer);
} catch (const c2pa::C2paException& e) {
// "OperationCancelled" if cancel() fired before signing completed.
}

cancel_thread.join();
```

`cancel()` is safe to call when no operation is in progress — it is a no-op in that case (and a no-op if the `Context` is moved-from).

### `ProgressPhase` values

| Phase | When emitted |
|-------|-------------|
| `Reading` | Start of a read/verification pass |
| `VerifyingManifest` | Manifest structure is being validated |
| `VerifyingSignature` | COSE signature is being verified |
| `VerifyingIngredient` | An ingredient manifest is being verified |
| `VerifyingAssetHash` | Asset hash is being computed and checked |
| `AddingIngredient` | An ingredient is being embedded |
| `Thumbnail` | A thumbnail is being generated |
| `Hashing` | Asset data is being hashed for signing |
| `Signing` | Claim is being signed |
| `Embedding` | Signed manifest is being embedded into the asset |
| `FetchingRemoteManifest` | A remote manifest URL is being fetched |
| `Writing` | Output is being written |
| `FetchingOCSP` | OCSP certificate status is being fetched |
| `FetchingTimestamp` | A timestamp is being fetched from a TSA |

**Typical phase sequence during signing:**

```
AddingIngredient → Thumbnail → Hashing → Signing → Embedding
```

If `verify_after_sign` is enabled, verification phases follow:

```
→ VerifyingManifest → VerifyingSignature → VerifyingAssetHash → VerifyingIngredient
```

**Typical phase sequence during reading:**

```
Reading → VerifyingManifest → VerifyingSignature → VerifyingAssetHash → VerifyingIngredient
```

### Combining with other settings

`with_progress_callback` chains with other `ContextBuilder` methods:

```cpp
auto context = c2pa::Context::ContextBuilder()
.with_settings(settings)
.with_signer(std::move(signer))
.with_progress_callback([](c2pa::ProgressPhase phase, uint32_t step, uint32_t total) {
// Update a UI progress bar, log phases, etc.
return true;
})
.create_context();
```

## Common configuration patterns

Common configurations include:
Expand Down
92 changes: 92 additions & 0 deletions include/c2pa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <cerrno>
#include <filesystem>
#include <fstream>
#include <functional>
#include <istream>
#include <ostream>
#include <string>
Expand Down Expand Up @@ -237,6 +238,56 @@ namespace c2pa
C2paSettings* settings_ptr;
};

/// @brief Phase values reported to the ProgressCallbackFunc.
///
/// @details A scoped C++ mirror of `C2paProgressPhase` from c2pa.h.
/// Values are verified at compile time to match the C enum, so any
/// future divergence in c2pa-rs will be caught as a build error.
///
/// Phases emitted during a typical sign cycle (in order):
/// AddingIngredient → Thumbnail → Hashing → Signing → Embedding →
/// (if verify_after_sign) VerifyingManifest → VerifyingSignature →
/// VerifyingAssetHash → VerifyingIngredient
///
/// Phases emitted during reading:
/// Reading → VerifyingManifest → VerifyingSignature →
/// VerifyingAssetHash → VerifyingIngredient
enum class ProgressPhase : uint8_t {
Reading = 0,
VerifyingManifest = 1,
VerifyingSignature = 2,
VerifyingIngredient = 3,
VerifyingAssetHash = 4,
AddingIngredient = 5,
Thumbnail = 6,
Hashing = 7,
Signing = 8,
Embedding = 9,
FetchingRemoteManifest = 10,
Writing = 11,
FetchingOCSP = 12,
FetchingTimestamp = 13,
};

/// @brief Type alias for the progress callback passed to ContextBuilder::with_progress_callback().
///
/// @details The callback is invoked at each major phase of signing and reading operations.
/// Returning false from the callback aborts the operation with an
/// OperationCancelled error (equivalent to calling Context::cancel()).
///
/// @param phase Current operation phase.
/// @param step 1-based step index within the phase.
/// 0 = indeterminate (use as liveness signal); resets to 1 at each new phase.
/// @param total 0 = indeterminate; 1 = single-shot; >1 = determinate (step/total = fraction).
/// @return true to continue the operation, false to request cancellation.
///
/// @note The callback must not throw. If it throws, the implementation catches the
/// exception and reports cancellation to the underlying library (same as returning
/// false); the original exception is not propagated. Prefer returning false or
/// using Context::cancel() instead of throwing.
///
using ProgressCallbackFunc = std::function<bool(ProgressPhase phase, uint32_t step, uint32_t total)>;

/// @brief C2PA context implementing IContextProvider.
/// @details Context objects manage C2PA SDK configuration and state.
/// Contexts can be created via direct construction or the ContextBuilder:
Expand Down Expand Up @@ -311,6 +362,31 @@ namespace c2pa
/// @throws C2paException if the builder or signer is invalid.
ContextBuilder& with_signer(Signer&& signer);

/// @brief Attach a progress callback to the context being built.
///
/// @details The callback is invoked at each major phase of signing and
/// reading operations performed with the resulting context.
/// Return false from the callback to abort the current operation
/// with an OperationCancelled error.
///
/// Phases emitted during a typical sign cycle (in order):
/// VerifyingIngredient → VerifyingManifest → VerifyingSignature →
/// VerifyingAssetHash → Thumbnail → Hashing → Signing → Embedding →
/// (if verify_after_sign) VerifyingManifest → … → VerifyingIngredient
///
/// Phases emitted during reading:
/// Reading → VerifyingManifest → VerifyingSignature →
/// VerifyingAssetHash → VerifyingIngredient
///
/// @param callback A callable matching ProgressCallbackFunc. The callback is
/// heap-allocated and owned by the resulting Context. Calling this method
/// more than once on the same builder replaces the previous callback.
/// The callable must not throw when invoked (see ProgressCallbackFunc).
/// @return Reference to this ContextBuilder for method chaining.
/// @throws C2paException if the builder is invalid or the C API call fails.
///
ContextBuilder& with_progress_callback(ProgressCallbackFunc callback);

/// @brief Create a Context from the current builder configuration.
/// @return A new Context instance.
/// @throws C2paException if context creation fails.
Expand All @@ -326,6 +402,7 @@ namespace c2pa

private:
C2paContextBuilder* context_builder;
std::unique_ptr<ProgressCallbackFunc> pending_callback_;
};

// Direct construction
Expand Down Expand Up @@ -376,8 +453,23 @@ namespace c2pa
/// @throws C2paException if ctx is nullptr.
explicit Context(C2paContext* ctx);

/// @brief Request cancellation of any in-progress operation on this context.
///
/// @details Safe to call from another thread while this Context remains valid
/// and is not being destroyed or moved concurrently with this call.
/// While a signing or reading operation is running on a valid Context,
/// the operation is aborted with an OperationCancelled error at the
/// next progress checkpoint. Has no effect if no operation is currently
/// in progress, or if this object is moved-from (is_valid() is false).
///
void cancel() noexcept;

private:
C2paContext* context;

/// Heap-owned ProgressCallbackFunc; non-null only when set via
/// ContextBuilder::with_progress_callback(). Deleted in the destructor.
void* callback_owner_ = nullptr;
};

/// @brief Get the version of the C2PA library.
Expand Down
Loading
Loading