//! Desaturate: an example VectorCraft object filter plug-in (plug-in ABI v1, see //! `docs/plugins.md`). //! //! It reads the selected paths as JSON, moves every colour (solid fills and strokes, gradient //! stops, freeform gradient points) towards grey by `amount` percent, keeping each colour in its //! own colour model, and hands the objects back. Only `serde_json` is used: no imports, no WASI. use serde_json::{Value, json}; const MANIFEST: &str = r#"{ "id": "org.vectorcraft.example.desaturate", "name": "Desaturate", "version": "1.0.0", "kind": "filter", "author": "VectorCraft contributors", "description": "Moves the colours of the selected paths towards grey; an example of the plug-in ABI.", "params": { "amount": {"type": "number", "min": 0, "max": 100, "default": 100} } }"#; #[unsafe(no_mangle)] pub extern "C" fn vc_abi_version() -> u32 { 1 } /// `len << 32 | ptr` of the UTF-8 manifest JSON. #[unsafe(no_mangle)] pub extern "C" fn vc_manifest() -> u64 { pack(MANIFEST.as_bytes()) } /// A block of `size` bytes the host writes the input and the parameters into. Never freed: every /// run gets a fresh instance. #[unsafe(no_mangle)] pub extern "C" fn vc_alloc(size: u32) -> u32 { let mut block = Vec::::with_capacity(size as usize); let ptr = block.as_mut_ptr(); std::mem::forget(block); ptr as u32 } /// Runs the filter: `input` holds `{"objects": [...]}`, `params` the parameters. Returns the /// output JSON as `len << 32 | ptr`, or a negative error code. /// /// # Safety /// The host guarantees both blocks came from `vc_alloc` and hold `*_len` bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn vc_run(input: *const u8, input_len: u32, params: *const u8, params_len: u32) -> i64 { // SAFETY: see the function's contract. let (input, params) = unsafe { (std::slice::from_raw_parts(input, input_len as usize), std::slice::from_raw_parts(params, params_len as usize)) }; let (Ok(mut doc), Ok(params)) = (serde_json::from_slice::(input), serde_json::from_slice::(params)) else { return -1 }; let amount = (params["amount"].as_f64().unwrap_or(100.0) / 100.0).clamp(0.0, 1.0); if doc["objects"].as_array().is_none_or(|o| o.is_empty()) { return output(&json!({"error": "Select some paths to desaturate."})); } desaturate(&mut doc, amount); output(&json!({"objects": doc["objects"]})) } /// Every colour (an object with a `model`) inside `v`, moved `k` of the way to grey. fn desaturate(v: &mut Value, k: f64) { match v { Value::Object(o) => { let model = o.get("model").and_then(Value::as_str).map(str::to_string); let get = |key: &str| o.get(key).and_then(Value::as_f64).unwrap_or(0.0); let new: Option> = match model.as_deref() { Some("rgb") => { let (r, g, b) = (get("r"), get("g"), get("b")); let y = 0.2126 * r + 0.7152 * g + 0.0722 * b; Some(vec![("r", r + (y - r) * k), ("g", g + (y - g) * k), ("b", b + (y - b) * k)]) } Some("cmyk") => { let (c, m, y) = (get("c"), get("m"), get("y")); let mean = (c + m + y) / 3.0; Some(vec![("c", c + (mean - c) * k), ("m", m + (mean - m) * k), ("y", y + (mean - y) * k)]) } Some("lab") => Some(vec![("a", get("a") * (1.0 - k)), ("b", get("b") * (1.0 - k))]), _ => None, }; match new { Some(new) => { for (key, x) in new { o.insert(key.into(), json!(x)); } } None => o.values_mut().for_each(|c| desaturate(c, k)), } } Value::Array(a) => a.iter_mut().for_each(|c| desaturate(c, k)), _ => {} } } /// Keeps `v` serialised in memory for the host to read, as `len << 32 | ptr`. fn output(v: &Value) -> i64 { let Ok(bytes) = serde_json::to_vec(v) else { return -2 }; let bytes = bytes.leak(); pack(bytes) as i64 } fn pack(bytes: &[u8]) -> u64 { ((bytes.len() as u64) << 32) | bytes.as_ptr() as u64 }