Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
98 changes: 97 additions & 1 deletion c2pa_c_ffi/src/c_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use std::{
use c2pa::Ingredient;
use c2pa::{
assertions::DataHash, identity::validator::CawgValidator, Builder as C2paBuilder,
CallbackSigner, Context, Reader as C2paReader, Settings as C2paSettings, SigningAlg,
CallbackSigner, Context, ProgressPhase, Reader as C2paReader, Settings as C2paSettings,
SigningAlg,
};
use tokio::runtime::Builder;

Expand Down Expand Up @@ -93,6 +94,23 @@ mod cbindgen_fix {
type C2paContextBuilder = Context;
type C2paContext = Arc<Context>;

/// Wraps a `*const c_void` (C `void*`) so it can be moved into a `Send + Sync` closure.
///
/// Accessing through [`as_ptr()`](SendPtr::as_ptr) rather than the raw `.0` field
/// ensures the Rust 2021 disjoint-capture analysis captures the whole `SendPtr`
/// (which is `Send + Sync`) rather than the inner `*const c_void` (which is not).
///
/// # Safety
/// The caller must guarantee the pointer is valid for the closure's lifetime.
struct SendPtr(*const c_void);
unsafe impl Send for SendPtr {}
unsafe impl Sync for SendPtr {}
impl SendPtr {
fn as_ptr(&self) -> *const c_void {
self.0
}
}

/// List of supported signing algorithms.
#[repr(C)]
pub enum C2paSigningAlg {
Expand Down Expand Up @@ -495,6 +513,62 @@ pub unsafe extern "C" fn c2pa_context_builder_set_signer(
0
}

/// C-callable progress callback function type.
///
/// # Parameters
/// * `context` – the opaque `user_data` pointer passed to
/// `c2pa_context_builder_set_progress_callback`.
/// * `phase` – numeric value of the [`ProgressPhase`] (see SDK header for constants).
/// Callers should derive any user-visible text from this value in the appropriate language.
/// * `step` – 1-based index of the current step within this phase. Always `1` for
/// single-shot phases.
/// * `total` – total number of steps in this phase. `0` means the total is not known
/// in advance (indeterminate); show a spinner rather than a progress bar. Always `1`
/// for single-shot phases.
///
/// # Return value
/// Return non-zero to continue the operation, zero to cancel.
pub type ProgressCCallback =
unsafe extern "C" fn(context: *const c_void, phase: u8, step: u32, total: u32) -> c_int;

/// Attaches a C progress callback to a context builder.
///
/// The `callback` is invoked at key checkpoints during signing and reading
/// operations. Returning `0` from the callback requests cancellation; the SDK
/// will return an error at the next safe stopping point.
///
/// # Parameters
/// * `builder` – a valid `C2paContextBuilder` pointer.
/// * `user_data` – opaque `void*` captured by the closure and passed as the first argument
/// of every `callback` invocation. Pass `NULL` if the callback does not need user data.
/// * `callback` – C function pointer matching [`ProgressCCallback`].
///
/// # Returns
/// `0` on success, non-zero on error (check `c2pa_error()`).
///
/// # Safety
/// * `builder` must be valid and not yet built.
/// * `user_data` must remain valid for the entire lifetime of the built context.
#[no_mangle]
pub unsafe extern "C" fn c2pa_context_builder_set_progress_callback(
builder: *mut C2paContextBuilder,
user_data: *const c_void,
callback: ProgressCCallback,
) -> c_int {
let builder = deref_mut_or_return_int!(builder, C2paContextBuilder);
// Wrap user_data so the closure can be Send + Sync.
// SAFETY: caller guarantees `user_data` outlives the context.
let ud = SendPtr(user_data);
// Both the C function pointer and user_data are captured in the closure, so
// no separate context-pointer field is needed on Context.
let c_callback = move |phase: ProgressPhase, step: u32, total: u32| {
// SAFETY: caller guarantees `callback` and `ud` are valid.
unsafe { (callback)(ud.as_ptr(), phase as u8, step, total) != 0 }
};
builder.set_progress_callback(c_callback);
0
}

/// Builds an immutable, shareable context from the builder.
///
/// The builder is consumed by this operation and becomes invalid.
Expand Down Expand Up @@ -542,6 +616,28 @@ pub unsafe extern "C" fn c2pa_context_new() -> *mut C2paContext {
box_tracked!(Context::new().into_shared())
}

/// Requests cancellation of any in-progress operation on this context.
///
/// Thread-safe — may be called from any thread that holds a valid `C2paContext`
/// pointer. The SDK will return an `OperationCancelled` error at the next safe
/// checkpoint inside the running operation.
///
/// # Parameters
/// * `ctx` – a valid, non-null `C2paContext` pointer obtained from
/// `c2pa_context_builder_build()` or `c2pa_context_new()`.
///
/// # Returns
/// `0` on success, non-zero if `ctx` is null or invalid.
///
/// # Safety
/// `ctx` must be a valid pointer and must not be freed concurrently with this call.
#[no_mangle]
pub unsafe extern "C" fn c2pa_context_cancel(ctx: *mut C2paContext) -> c_int {
let ctx = deref_or_return_int!(ctx, C2paContext);
ctx.cancel();
0
}

///
/// # Errors
/// Returns NULL if there were errors, otherwise returns a JSON string.
Expand Down
48 changes: 48 additions & 0 deletions docs/progress_callbacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Progress Callback API

## Overview

The SDK reports progress during long-running operations (signing, reading, verification) via an optional callback on `Context`. The callback receives the current phase and a completion fraction. Returning `false` (or `0` in C) requests cancellation; the operation stops at the next safe point and returns `Error::OperationCancelled`.

## Rust API

**Types:**
- `ProgressPhase` – enum of phases: `Ingredients`, `Thumbnail`, `Hashing`, `Signing`, `Embedding`, `Verification`, `RemoteFetch` (`#[repr(u8)]` for FFI)
- `ProgressCallbackFunc` – `dyn Fn(ProgressPhase, f32) -> bool` (closure type)

**Context methods:**
- `with_progress_callback(callback)` – set the callback (builder pattern)
- `set_progress_callback(callback)` – set the callback (mutable)
- `cancel()` – request cancellation from any thread
- `is_cancelled()` – check if cancellation was requested

**Callback signature:** `(phase: ProgressPhase, pct: f32) -> bool`
- `phase` – current phase (caller maps to localized text)
- `pct` – fraction complete in range 0.0–1.0 (e.g. 0.75 = 75%)
- Return `true` to continue, `false` to cancel

**Example:**
```rust
let ctx = Context::new()
.with_progress_callback(|phase, pct| {
println!("{:?} {:.0}%", phase, pct * 100.0);
true // return false to cancel
});

let ctx = Arc::new(ctx);
// From another thread: ctx.cancel();
```

## C FFI

- `c2pa_context_builder_set_progress_callback(builder, user_data, callback)` – attach callback and user data
- `c2pa_context_cancel(ctx)` – request cancellation

**C callback:** `int (*)(void* context, uint8_t phase, float pct)`
- Return non-zero to continue, zero to cancel
- `phase` matches `ProgressPhase` discriminants (0–6)
- `pct` is 0.0–1.0

## When It Fires

Callbacks fire at phase boundaries during Builder and Reader operations (e.g. after ingredients, thumbnail, hashing, signing, embedding). There are no callbacks inside long I/O loops yet; finer-grained progress would require additional implementation work.
73 changes: 64 additions & 9 deletions sdk/examples/builder_sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
// specific language governing permissions and limitations under
// each license.

//! Example App showing how to work archive and restore Builders and ingredients.
//! Example App showing how to work with Builders, ingredients, and the
//! progress/cancel API.
use std::{
io::{self, Cursor, Read, Seek},
sync::Arc,
sync::{Arc, Mutex},
time::Instant,
};

use anyhow::Result;
use c2pa::{
validation_results::ValidationState, Builder, Context, DigitalSourceType, Reader, Settings,
validation_results::ValidationState, Builder, Context, DigitalSourceType, ProgressPhase,
Reader, Settings,
};
use serde_json::json;

Expand Down Expand Up @@ -91,8 +94,46 @@ fn main() -> Result<()> {

let settings =
Settings::new().with_json(include_str!("../tests/fixtures/test_settings.json"))?;
let context = Context::new().with_settings(settings)?.into_shared();
// --- Progress / cancel API demonstration ---
//
// A context can carry a progress callback that is invoked at each major
// phase of a signing or reading operation. The callback receives the
// current phase along with a step index and total step count. Returning
// `false` from the callback (or calling `ctx.cancel()` from another
// thread) requests cancellation; the SDK will return
// `Error::OperationCancelled` at the next checkpoint.
//
// `total == 0` means the total is not known in advance (indeterminate).
// Single-shot phases always report `step=1, total=1`.
let phases_seen: Arc<Mutex<Vec<(ProgressPhase, u32, u32)>>> = Arc::new(Mutex::new(Vec::new()));
let phases_seen_cb = phases_seen.clone();

// Shared timer reset before each operation so elapsed times are relative.
let timer: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
let timer_cb = timer.clone();

let context = Context::new()
.with_settings(settings)?
.with_progress_callback(move |phase, step, total| {
let elapsed = timer_cb.lock().unwrap().elapsed();
if total == 0 {
println!(
" [{:>8.3}ms] {phase:?} {step}/? (indeterminate)",
elapsed.as_secs_f64() * 1000.0
);
} else {
println!(
" [{:>8.3}ms] {phase:?} {step}/{total}",
elapsed.as_secs_f64() * 1000.0
);
}
phases_seen_cb.lock().unwrap().push((phase, step, total));
true // return false here (or call ctx.cancel()) to abort
})
.into_shared();

println!("Capturing an ingredient, adding to builder and archiving ");
*timer.lock().unwrap() = Instant::now();
// Here we capture an ingredient with its validation into a c2pa_data object.
let ingredient_c2pa = capture_ingredient(FORMAT, &mut ingredient_source, &context)?;
// The ingredient_c2pa can be saved to a file, blob storage, a database, or wherever you want to keep it.
Expand Down Expand Up @@ -134,18 +175,32 @@ fn main() -> Result<()> {
// let debug_path = format!("{}/../target/archive_test.c2pa", env!("CARGO_MANIFEST_DIR"));
// std::fs::write(&debug_path, archive.get_ref())?;

// unpack the manifest builder from the archived stream
println!("Signing archived builder with progress tracking:");
*timer.lock().unwrap() = Instant::now();
archive.rewind()?;
let mut builder = Builder::from_shared_context(&context).with_archive(&mut archive)?;

// Now we will sign a new image that will reference the previously captured ingredient
let mut source = Cursor::new(SOURCE_IMAGE);
let mut dest = Cursor::new(Vec::new());
builder.save_to_stream(FORMAT, &mut source, &mut dest)?;
Builder::from_shared_context(&context)
.with_archive(&mut archive)?
.save_to_stream(FORMAT, &mut source, &mut dest)?;

let seen = phases_seen.lock().unwrap();
assert!(
seen.iter().any(|(p, _, _)| *p == ProgressPhase::Hashing),
"expected at least a Hashing checkpoint"
);
assert!(
seen.iter()
.any(|(p, _, _)| *p == ProgressPhase::VerifyingManifest),
"expected at least a VerifyingManifest checkpoint"
);
drop(seen);

// read and validate the signed manifest store
dest.rewind()?;

println!("Reading with progress tracking:");
*timer.lock().unwrap() = Instant::now();
let reader = Reader::from_shared_context(&context).with_stream(FORMAT, &mut dest)?;
println!("{}", reader.json());
assert_eq!(reader.validation_state(), ValidationState::Trusted);
Expand Down
Loading
Loading