Progress and cancellation API
July 30, 2026 · View on GitHub
Overview
During multi-step long-running operations (such as signing, reading, and verification), the SDK reports progress via an optional callback registered on a Context. The primary purposes of the callback are:
- Liveness: confirming to the caller that the SDK is still making forward progress and is not hung.
- Cancellation: giving the caller a safe opportunity to stop the operation at any phase boundary.
The callback receives the current phase, a step counter, and an optional total. Returning false from the callback requests cancellation; the SDK will stop at the next safe checkpoint and return Error::OperationCancelled.
You can also request cancellation externally (from a different thread) by calling Context::cancel() without going through the callback at all.
Warning
Do not use this API for time-remaining estimates. A raw step count does not reliably translate into elapsed or remaining time.
Phases
The SDK reports progress as a sequence of named phases, each represented by the ProgressPhase enum:
| Value | Rust variant | When it fires |
|---|---|---|
| 0 | Reading | Parsing and extracting JUMBF manifest data from an asset stream |
| 1 | VerifyingManifest | Verifying the structure and integrity of a manifest store entry |
| 2 | VerifyingSignature | Verifying a COSE signature and certificate chain (fires twice per claim: step 1 before COSE parse, step 2 after OCSP/full verification) |
| 3 | VerifyingIngredient | Verifying one ingredient's embedded manifest (step = ingredient index, total = total ingredient count) |
| 4 | VerifyingAssetHash | Re-hashing the asset bytes to verify hash-binding assertions |
| 5 | AddingIngredient | Adding an ingredient to the manifest being built |
| 6 | Thumbnail | Generating a thumbnail during signing |
| 7 | Hashing | Hashing asset data to build the hash-binding assertion |
| 8 | Signing | Signing the claim with COSE (including any remote TSA timestamp fetch) |
| 9 | Embedding | Embedding the signed JUMBF manifest store into the output asset |
| 10 | FetchingRemoteManifest | Fetching a remote manifest over the network |
| 11 | Writing | Streaming the asset with the placeholder JUMBF to the output stream |
Note
ProgressPhase is marked #[non_exhaustive]. Future SDK versions may add new phases: Always include a default/wildcard arm in any match or switch statement.
Typical phase sequences
Signing a new asset:
AddingIngredient (0–N times) → Thumbnail → Hashing → Signing → Writing → Embedding
Reading / verifying an existing asset:
Reading → VerifyingManifest → VerifyingSignature (×2 steps per claim) → VerifyingIngredient (×N) → VerifyingAssetHash
Callback signature
fn(phase: ProgressPhase, step: u32, total: u32) -> bool
phase: the current phase (see table above). Callers should derive user-visible text from this value; the SDK does not provide a localized string.step: monotonically increasing counter within the current phase, starting at1. Resets to1at the start of each new phase. Use it as a liveness heartbeat: as long asstepkeeps rising, the SDK is making progress. Do not assume any particular unit; for example,Hashinguses chunk index andVerifyingIngredientuses ingredient index. The unit is phase-specific and may change between SDK versions.total: interpreted as follows:0: indeterminate; the total is not known in advance. Display a spinner and use the risingstepvalue as a liveness signal.1: single-shot phase; the callback itself is the notification. No subdivision is meaningful.> 1: determinate;step / totalgives a completion fraction suitable for a progress bar.
- Return
trueto continue,falseto cancel.
The closure must be Send + Sync on non-WASM targets. On WASM (single-threaded) those bounds are not required.
Note
Do not call SDK functions from inside the progress callback.
Rust API
Setting a callback
Example of setting a callback with the builder pattern:
use c2pa::{Context, ProgressPhase};
let ctx = Context::new()
.with_progress_callback(|phase, step, total| {
match phase {
ProgressPhase::Hashing => {
// total may be 0 (indeterminate) for streaming assets
if total > 0 {
println!("Hashing: {}/{}", step, total);
} else {
println!("Hashing…");
}
}
ProgressPhase::Signing => println!("Signing…"),
_ => println!("{:?}", phase),
}
true // return false to cancel
});
Example of setting a callback with a mutable setter for FFI adapters:
use c2pa::{Context, ProgressPhase};
let mut ctx = Context::new();
ctx.set_progress_callback(|phase, step, total| {
println!("{phase:?} {step}/{total}");
true
});
Example of out-of-band cancellation from another thread:
use c2pa::Context;
use std::sync::Arc;
let ctx = Arc::new(Context::new());
let ctx_bg = ctx.clone();
// In another thread (e.g. when the user clicks "Cancel"):
std::thread::spawn(move || {
ctx_bg.cancel();
});
The signing or reading operation using ctx will return Err(Error::OperationCancelled) at the next safe checkpoint.
Both mechanisms can be combined: the callback can cancel based on phase or elapsed time, while the flag lets any thread cancel at any point.
Error handling
When the callback returns false or the caller calls cancel(), the ongoing operation returns Error::OperationCancelled. Callers should check for this specific error and treat it as a normal, user-initiated termination rather than a failure.
match builder.sign(ctx, format, input, output) {
Ok(_) => { /* success */ }
Err(c2pa::Error::OperationCancelled) => { /* user cancelled — clean up and continue */ }
Err(e) => { /* real error */ }
}