Browse / github.com/storytold/craft-libs / patch
craft-cms: optional serde for Intent and TransformOptions (closes #64)
craft-cms: optional serde for Intent and TransformOptions Apps store rendering intents and conversion options in documents and settings (proof setup, PDF export, colour settings). `Intent` and `TransformOptions` derive no serde traits, so each app writes its own mapping. Proposal: an optional `serde` feature that derives `Serialize`/`Deserialize` for `Intent` (names: `perceptual`, `relative-colorimetric`, `saturation`, `absolute-colorimetric`) and `TransformOptions` (with defaults for missing fields). `Intent::from_u32` stays for ICC headers.
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- zachshallbetter (imported from pull request #88, open)
- Base
e9546e191dd62f2bc1d61eca45dbdc2f1b3c1d8e- Size
- +62 −0 in 5 files
- Review
- pending
- Shared
- 2026-10-28
- Id
278c1e56d51af7a151dcff88
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock crates/cms/Cargo.toml crates/cms/src/lib.rs crates/cms/src/transform.rs crates/cms/tests/serde.rs
@@ -527,6 +527,8 @@ version = "0.1.0" dependencies = [ "moxcms", "rayon",+ "serde",+ "serde_json", ] [[package]]@@ -12,8 +12,16 @@ exclude = ["tests/data", "tests/fogra39.rs"] [lints] workspace = true +[features]+default = []+serde = ["dep:serde"]++[dependencies]+serde = { version = "1", features = ["derive"], optional = true }+ [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rayon = { workspace = true } [dev-dependencies] moxcms = "0.8"+serde_json = "1"@@ -39,6 +39,8 @@ pub use transform::{SampleKind, Transform, TransformOptions, black_point, cached /// ICC rendering intent. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]+#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] pub enum Intent { Perceptual = 0, #[default]@@ -20,7 +20,15 @@ pub enum SampleKind { } /// Options for building a [`Transform`].+///+/// When compiled with the `serde` feature, this struct implements [`Serialize`](serde::Serialize)+/// and [`Deserialize`](serde::Deserialize) with kebab-case field naming and defaults for omitted fields.+/// This format is intended for transient configuration interchange (such as IPC payloads, command-line+/// options, and temporary rendering preferences). Deserialization is backward-compatible with omitted+/// fields via `#[serde(default)]`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]+#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))] pub struct TransformOptions { pub intent: Intent, /// Adobe-style black point compensation (ignored for absolute colorimetric).new file mode 100644@@ -0,0 +1,42 @@+#![cfg(feature = "serde")]++use craft_cms::{Intent, TransformOptions};++#[test]+fn test_serde_intent_kebab_case() {+ let intents = [+ (Intent::Perceptual, "\"perceptual\""),+ (Intent::RelativeColorimetric, "\"relative-colorimetric\""),+ (Intent::Saturation, "\"saturation\""),+ (Intent::AbsoluteColorimetric, "\"absolute-colorimetric\""),+ ];++ for (intent, json) in intents {+ let serialized = serde_json::to_string(&intent).expect("serialize intent");+ assert_eq!(serialized, json);+ let deserialized: Intent = serde_json::from_str(json).expect("deserialize intent");+ assert_eq!(deserialized, intent);+ }+}++#[test]+fn test_serde_transform_options_defaults() {+ let empty_json = "{}";+ let opts: TransformOptions = serde_json::from_str(empty_json).expect("deserialize empty options");+ assert_eq!(opts, TransformOptions::default());++ let partial_json = r#"{"intent": "perceptual"}"#;+ let opts_partial: TransformOptions = serde_json::from_str(partial_json).expect("deserialize partial");+ assert_eq!(opts_partial.intent, Intent::Perceptual);+ assert!(opts_partial.bpc);+ assert!(opts_partial.precise_float);++ let full_json = r#"{"intent":"saturation","bpc":false,"precise-float":false}"#;+ let opts_full: TransformOptions = serde_json::from_str(full_json).expect("deserialize full");+ assert_eq!(opts_full.intent, Intent::Saturation);+ assert!(!opts_full.bpc);+ assert!(!opts_full.precise_float);++ let reserialized = serde_json::to_string(&opts_full).expect("serialize options");+ assert_eq!(reserialized, full_json);+}