collective

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

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -527,6 +527,8 @@ version = "0.1.0" dependencies = [  "moxcms",  "rayon",+ "serde",+ "serde_json", ]  [[package]]diff --git a/crates/cms/Cargo.toml b/crates/cms/Cargo.toml--- a/crates/cms/Cargo.toml+++ b/crates/cms/Cargo.toml@@ -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"diff --git a/crates/cms/src/lib.rs b/crates/cms/src/lib.rs--- a/crates/cms/src/lib.rs+++ b/crates/cms/src/lib.rs@@ -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]diff --git a/crates/cms/src/transform.rs b/crates/cms/src/transform.rs--- a/crates/cms/src/transform.rs+++ b/crates/cms/src/transform.rs@@ -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).diff --git a/crates/cms/tests/serde.rs b/crates/cms/tests/serde.rsnew file mode 100644--- /dev/null+++ b/crates/cms/tests/serde.rs@@ -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);+}