Skip to content

Commit 13a0b5f

Browse files
authored
Merge pull request #195 from contentauth/feature/progress-cancel
feat: Adds Progress and Cancel API support
2 parents 5730bbc + 34dc612 commit 13a0b5f

6 files changed

Lines changed: 498 additions & 6 deletions

File tree

.cursor/rules/git-workflow.mdc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
description: Git commit and branch workflow preferences
3+
alwaysApply: true
4+
---
5+
6+
# Git Workflow
7+
8+
- **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.
9+
- **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`.
10+
- Never push to remote unless explicitly asked.
11+
- Never force-push.
12+
- Always show `git diff --stat` or a plain-language summary of changes before proposing a commit message.

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 current version of this C++ project
17-
project(c2pa-c VERSION 0.20.0)
17+
project(c2pa-c VERSION 0.20.1)
1818

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

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

docs/context-settings.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,132 @@ auto context = c2pa::Context::ContextBuilder()
162162
| `with_settings(settings)` | Apply a `Settings` object |
163163
| `with_json(json_string)` | Apply settings from a JSON string |
164164
| `with_json_settings_file(path)` | Load and apply settings from a JSON file |
165+
| `with_signer(signer)` | Store a `Signer` in the context (consumed; used by `Builder::sign` with no explicit signer) |
166+
| `with_progress_callback(callback)` | Register a progress/cancel callback (see [Progress callbacks and cancellation](#progress-callbacks-and-cancellation)) |
165167
| `create_context()` | Build and return the `Context` (consumes the builder) |
166168

169+
## Progress callbacks and cancellation
170+
171+
You can register a callback on a `Context` to receive progress notifications during signing and reading operations, and to cancel an operation in flight.
172+
173+
### Registering a callback
174+
175+
Use `ContextBuilder::with_progress_callback` to attach a callback before building the context:
176+
177+
```cpp
178+
#include <atomic>
179+
180+
std::atomic<int> phase_count{0};
181+
182+
auto context = c2pa::Context::ContextBuilder()
183+
.with_progress_callback([&](c2pa::ProgressPhase phase, uint32_t step, uint32_t total) {
184+
++phase_count;
185+
// Return true to continue, false to cancel.
186+
return true;
187+
})
188+
.create_context();
189+
190+
// Use the context normally — the callback fires automatically.
191+
c2pa::Builder builder(context, manifest_json);
192+
builder.sign("source.jpg", "output.jpg", signer);
193+
```
194+
195+
The callback signature is:
196+
197+
```cpp
198+
bool callback(c2pa::ProgressPhase phase, uint32_t step, uint32_t total);
199+
```
200+
201+
- **`phase`** — which stage the SDK is in (see [`ProgressPhase` values](#progressphase-values) below).
202+
- **`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.
203+
- **`total`**`0` = indeterminate (show a spinner); `1` = single-shot phase; `> 1` = determinate (`step / total` gives a completion fraction).
204+
- **Return value** — return `true` to continue, `false` to request cancellation (same effect as calling `context.cancel()`).
205+
206+
**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.
207+
208+
### Cancelling from another thread
209+
210+
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:
211+
212+
```cpp
213+
#include <thread>
214+
215+
auto context = c2pa::Context::ContextBuilder()
216+
.with_progress_callback([](c2pa::ProgressPhase, uint32_t, uint32_t) {
217+
return true; // Don't cancel from the callback — use cancel() instead.
218+
})
219+
.create_context();
220+
221+
// Kick off a cancel after 500 ms from a background thread.
222+
std::thread cancel_thread([&context]() {
223+
std::this_thread::sleep_for(std::chrono::milliseconds(500));
224+
context.cancel();
225+
});
226+
227+
try {
228+
c2pa::Builder builder(context, manifest_json);
229+
builder.sign("large_file.jpg", "output.jpg", signer);
230+
} catch (const c2pa::C2paException& e) {
231+
// "OperationCancelled" if cancel() fired before signing completed.
232+
}
233+
234+
cancel_thread.join();
235+
```
236+
237+
`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).
238+
239+
### `ProgressPhase` values
240+
241+
| Phase | When emitted |
242+
|-------|-------------|
243+
| `Reading` | Start of a read/verification pass |
244+
| `VerifyingManifest` | Manifest structure is being validated |
245+
| `VerifyingSignature` | COSE signature is being verified |
246+
| `VerifyingIngredient` | An ingredient manifest is being verified |
247+
| `VerifyingAssetHash` | Asset hash is being computed and checked |
248+
| `AddingIngredient` | An ingredient is being embedded |
249+
| `Thumbnail` | A thumbnail is being generated |
250+
| `Hashing` | Asset data is being hashed for signing |
251+
| `Signing` | Claim is being signed |
252+
| `Embedding` | Signed manifest is being embedded into the asset |
253+
| `FetchingRemoteManifest` | A remote manifest URL is being fetched |
254+
| `Writing` | Output is being written |
255+
| `FetchingOCSP` | OCSP certificate status is being fetched |
256+
| `FetchingTimestamp` | A timestamp is being fetched from a TSA |
257+
258+
**Typical phase sequence during signing:**
259+
260+
```
261+
AddingIngredient → Thumbnail → Hashing → Signing → Embedding
262+
```
263+
264+
If `verify_after_sign` is enabled, verification phases follow:
265+
266+
```
267+
→ VerifyingManifest → VerifyingSignature → VerifyingAssetHash → VerifyingIngredient
268+
```
269+
270+
**Typical phase sequence during reading:**
271+
272+
```
273+
Reading → VerifyingManifest → VerifyingSignature → VerifyingAssetHash → VerifyingIngredient
274+
```
275+
276+
### Combining with other settings
277+
278+
`with_progress_callback` chains with other `ContextBuilder` methods:
279+
280+
```cpp
281+
auto context = c2pa::Context::ContextBuilder()
282+
.with_settings(settings)
283+
.with_signer(std::move(signer))
284+
.with_progress_callback([](c2pa::ProgressPhase phase, uint32_t step, uint32_t total) {
285+
// Update a UI progress bar, log phases, etc.
286+
return true;
287+
})
288+
.create_context();
289+
```
290+
167291
## Common configuration patterns
168292
169293
Common configurations include:

include/c2pa.hpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <cerrno>
3535
#include <filesystem>
3636
#include <fstream>
37+
#include <functional>
3738
#include <istream>
3839
#include <ostream>
3940
#include <string>
@@ -237,6 +238,56 @@ namespace c2pa
237238
C2paSettings* settings_ptr;
238239
};
239240

241+
/// @brief Phase values reported to the ProgressCallbackFunc.
242+
///
243+
/// @details A scoped C++ mirror of `C2paProgressPhase` from c2pa.h.
244+
/// Values are verified at compile time to match the C enum, so any
245+
/// future divergence in c2pa-rs will be caught as a build error.
246+
///
247+
/// Phases emitted during a typical sign cycle (in order):
248+
/// AddingIngredient → Thumbnail → Hashing → Signing → Embedding →
249+
/// (if verify_after_sign) VerifyingManifest → VerifyingSignature →
250+
/// VerifyingAssetHash → VerifyingIngredient
251+
///
252+
/// Phases emitted during reading:
253+
/// Reading → VerifyingManifest → VerifyingSignature →
254+
/// VerifyingAssetHash → VerifyingIngredient
255+
enum class ProgressPhase : uint8_t {
256+
Reading = 0,
257+
VerifyingManifest = 1,
258+
VerifyingSignature = 2,
259+
VerifyingIngredient = 3,
260+
VerifyingAssetHash = 4,
261+
AddingIngredient = 5,
262+
Thumbnail = 6,
263+
Hashing = 7,
264+
Signing = 8,
265+
Embedding = 9,
266+
FetchingRemoteManifest = 10,
267+
Writing = 11,
268+
FetchingOCSP = 12,
269+
FetchingTimestamp = 13,
270+
};
271+
272+
/// @brief Type alias for the progress callback passed to ContextBuilder::with_progress_callback().
273+
///
274+
/// @details The callback is invoked at each major phase of signing and reading operations.
275+
/// Returning false from the callback aborts the operation with an
276+
/// OperationCancelled error (equivalent to calling Context::cancel()).
277+
///
278+
/// @param phase Current operation phase.
279+
/// @param step 1-based step index within the phase.
280+
/// 0 = indeterminate (use as liveness signal); resets to 1 at each new phase.
281+
/// @param total 0 = indeterminate; 1 = single-shot; >1 = determinate (step/total = fraction).
282+
/// @return true to continue the operation, false to request cancellation.
283+
///
284+
/// @note The callback must not throw. If it throws, the implementation catches the
285+
/// exception and reports cancellation to the underlying library (same as returning
286+
/// false); the original exception is not propagated. Prefer returning false or
287+
/// using Context::cancel() instead of throwing.
288+
///
289+
using ProgressCallbackFunc = std::function<bool(ProgressPhase phase, uint32_t step, uint32_t total)>;
290+
240291
/// @brief C2PA context implementing IContextProvider.
241292
/// @details Context objects manage C2PA SDK configuration and state.
242293
/// Contexts can be created via direct construction or the ContextBuilder:
@@ -311,6 +362,31 @@ namespace c2pa
311362
/// @throws C2paException if the builder or signer is invalid.
312363
ContextBuilder& with_signer(Signer&& signer);
313364

365+
/// @brief Attach a progress callback to the context being built.
366+
///
367+
/// @details The callback is invoked at each major phase of signing and
368+
/// reading operations performed with the resulting context.
369+
/// Return false from the callback to abort the current operation
370+
/// with an OperationCancelled error.
371+
///
372+
/// Phases emitted during a typical sign cycle (in order):
373+
/// VerifyingIngredient → VerifyingManifest → VerifyingSignature →
374+
/// VerifyingAssetHash → Thumbnail → Hashing → Signing → Embedding →
375+
/// (if verify_after_sign) VerifyingManifest → … → VerifyingIngredient
376+
///
377+
/// Phases emitted during reading:
378+
/// Reading → VerifyingManifest → VerifyingSignature →
379+
/// VerifyingAssetHash → VerifyingIngredient
380+
///
381+
/// @param callback A callable matching ProgressCallbackFunc. The callback is
382+
/// heap-allocated and owned by the resulting Context. Calling this method
383+
/// more than once on the same builder replaces the previous callback.
384+
/// The callable must not throw when invoked (see ProgressCallbackFunc).
385+
/// @return Reference to this ContextBuilder for method chaining.
386+
/// @throws C2paException if the builder is invalid or the C API call fails.
387+
///
388+
ContextBuilder& with_progress_callback(ProgressCallbackFunc callback);
389+
314390
/// @brief Create a Context from the current builder configuration.
315391
/// @return A new Context instance.
316392
/// @throws C2paException if context creation fails.
@@ -326,6 +402,7 @@ namespace c2pa
326402

327403
private:
328404
C2paContextBuilder* context_builder;
405+
std::unique_ptr<ProgressCallbackFunc> pending_callback_;
329406
};
330407

331408
// Direct construction
@@ -376,8 +453,23 @@ namespace c2pa
376453
/// @throws C2paException if ctx is nullptr.
377454
explicit Context(C2paContext* ctx);
378455

456+
/// @brief Request cancellation of any in-progress operation on this context.
457+
///
458+
/// @details Safe to call from another thread while this Context remains valid
459+
/// and is not being destroyed or moved concurrently with this call.
460+
/// While a signing or reading operation is running on a valid Context,
461+
/// the operation is aborted with an OperationCancelled error at the
462+
/// next progress checkpoint. Has no effect if no operation is currently
463+
/// in progress, or if this object is moved-from (is_valid() is false).
464+
///
465+
void cancel() noexcept;
466+
379467
private:
380468
C2paContext* context;
469+
470+
/// Heap-owned ProgressCallbackFunc; non-null only when set via
471+
/// ContextBuilder::with_progress_callback(). Deleted in the destructor.
472+
void* callback_owner_ = nullptr;
381473
};
382474

383475
/// @brief Get the version of the C2PA library.

0 commit comments

Comments
 (0)