From 4cd2eb4e48304b02443f95b04980b91efdc6579e Mon Sep 17 00:00:00 2001 From: Aaron Allen Date: Thu, 19 Feb 2026 08:34:49 -0600 Subject: [PATCH] Add `serde` feature for color space serialization Add optional `serde` dependency with `Serialize` and `Deserialize` implementations on `Component` and all 22 color spaces. Alpha is omitted from serialization when opaque (1.0) and defaults to 1.0 on deserialization when absent. The feature is included in `full`. - Custom `Serialize`/`Deserialize` impls on `Component` to serialize as a bare `f64` - Custom impls on `Rgb` and cylindrical/subtractive spaces to handle generic type parameters and non-serializable fields - Derive-based impls on CIE, perceptual, and physiological spaces - Integration tests covering roundtrip, alpha handling, and field names for `Xyz`, `Rgb`, `Lms`, `Lab`, `Oklch`, `Hsl`, and `Cmyk` --- CHANGELOG.md | 7 ++ Cargo.lock | 98 +++++++++++++++ Cargo.toml | 6 + src/component.rs | 19 +++ src/space/cie/lab.rs | 40 ++++++ src/space/cie/lch.rs | 40 ++++++ src/space/cie/lchuv.rs | 40 ++++++ src/space/cie/luv.rs | 40 ++++++ src/space/cie/xyy.rs | 41 ++++++ src/space/cie/xyz.rs | 40 ++++++ src/space/cylindrical/hsi.rs | 47 +++++++ src/space/cylindrical/hsl.rs | 47 +++++++ src/space/cylindrical/hsv.rs | 47 +++++++ src/space/cylindrical/hwb.rs | 47 +++++++ src/space/perceptual/hpluv.rs | 40 ++++++ src/space/perceptual/hsluv.rs | 40 ++++++ src/space/perceptual/okhsl.rs | 40 ++++++ src/space/perceptual/okhsv.rs | 40 ++++++ src/space/perceptual/okhwb.rs | 40 ++++++ src/space/perceptual/oklab.rs | 40 ++++++ src/space/perceptual/oklch.rs | 40 ++++++ src/space/physiological/lms.rs | 40 ++++++ src/space/rgb/space.rs | 47 +++++++ src/space/subtractive/cmy.rs | 47 +++++++ src/space/subtractive/cmyk.rs | 50 ++++++++ tests/serde.rs | 224 +++++++++++++++++++++++++++++++++ 26 files changed, 1247 insertions(+) create mode 100644 tests/serde.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5acb3f0..038a153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Break Versioning](https://www.taoensso.com/break-ve ## [Unreleased] +### Added + +- Add optional `serde` feature for JSON (and other format) serialization and deserialization of all color spaces — + `Serialize` and `Deserialize` are implemented on `Component`, `Xyz`, `Lms`, `Rgb`, `Hsl`, `Hsv`, `Hwb`, `Hsi`, + `Cmy`, `Cmyk`, `Lab`, `Lch`, `Lchuv`, `Luv`, `Xyy`, `Oklab`, `Oklch`, `Okhsl`, `Okhsv`, `Okhwb`, `Hsluv`, and + `Hpluv` — alpha is omitted from serialization when opaque (1.0) and defaults to 1.0 on deserialization when absent + ## [v0.4.3] - 2026-02-16 ### Added diff --git a/Cargo.lock b/Cargo.lock index 1bc23d2..749661d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,8 +13,22 @@ name = "farg" version = "0.4.3" dependencies = [ "pretty_assertions", + "serde", + "serde_json", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -25,8 +39,92 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 0a155ee..fee3f36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,6 +156,7 @@ full = [ "all-illuminants", "all-observers", "all-spaces", + "serde", ] illuminant-a = [] illuminant-b = [] @@ -309,6 +310,7 @@ rgb-smpte-c = [] rgb-sony-s-gamut-3 = [] rgb-sony-s-gamut-3-cine = [] rgb-wide-gamut-rgb = ["illuminant-d50"] +serde = ["dep:serde"] space-cmy = [] space-cmyk = [] space-hsb = ["space-hsv"] @@ -330,5 +332,9 @@ space-oklab = [] space-oklab-family = ["space-okhsl", "space-okhsv", "space-okhwb", "space-oklab", "space-oklch"] space-oklch = ["space-oklab"] +[dependencies] +serde = { version = "1", optional = true, features = ["derive"] } + [dev-dependencies] pretty_assertions = "1.4" +serde_json = "1" diff --git a/src/component.rs b/src/component.rs index 2dbade8..016ea45 100644 --- a/src/component.rs +++ b/src/component.rs @@ -79,6 +79,13 @@ where } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Component { + fn deserialize>(deserializer: D) -> Result { + f64::deserialize(deserializer).map(Self) + } +} + impl Display for Component { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, "{:.precision$}", self.0, precision = f.precision().unwrap_or(4)) @@ -215,6 +222,13 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Component { + fn serialize(&self, serializer: Ser) -> Result { + self.0.serialize(serializer) + } +} + impl Sub for Component where T: Into, @@ -234,6 +248,11 @@ where self.0 -= rhs.into().0; } } +/// Returns the default alpha value (1.0) for serde deserialization. +#[cfg(feature = "serde")] +pub(crate) fn default_alpha() -> Component { + Component(1.0) +} #[cfg(test)] mod test { diff --git a/src/space/cie/lab.rs b/src/space/cie/lab.rs index 7b76741..120ce8d 100644 --- a/src/space/cie/lab.rs +++ b/src/space/cie/lab.rs @@ -430,6 +430,29 @@ impl ColorSpace<3> for Lab { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Lab { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct LabData { + l: Component, + a: Component, + b: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = LabData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + a: data.a, + b: data.b, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Lab { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -651,6 +674,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Lab { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Lab", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("a", &self.a)?; + state.serialize_field("b", &self.b)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Lab where T: Into, diff --git a/src/space/cie/lch.rs b/src/space/cie/lch.rs index 91a2b6a..cf4f71d 100644 --- a/src/space/cie/lch.rs +++ b/src/space/cie/lch.rs @@ -518,6 +518,29 @@ impl ColorSpace<3> for Lch { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Lch { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct LchData { + l: Component, + c: Component, + h: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = LchData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + c: data.c, + h: data.h, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Lch { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -740,6 +763,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Lch { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Lch", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("c", &self.c)?; + state.serialize_field("h", &self.h)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Lch where T: Into, diff --git a/src/space/cie/lchuv.rs b/src/space/cie/lchuv.rs index e168fc1..5907a7d 100644 --- a/src/space/cie/lchuv.rs +++ b/src/space/cie/lchuv.rs @@ -576,6 +576,29 @@ impl ColorSpace<3> for Lchuv { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Lchuv { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct LchuvData { + l: Component, + c: Component, + h: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = LchuvData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + c: data.c, + h: data.h, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Lchuv { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -798,6 +821,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Lchuv { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Lchuv", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("c", &self.c)?; + state.serialize_field("h", &self.h)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Lchuv where T: Into, diff --git a/src/space/cie/luv.rs b/src/space/cie/luv.rs index 6d347fd..fe00351 100644 --- a/src/space/cie/luv.rs +++ b/src/space/cie/luv.rs @@ -366,6 +366,29 @@ impl ColorSpace<3> for Luv { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Luv { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct LuvData { + l: Component, + u: Component, + v: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = LuvData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + u: data.u, + v: data.v, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Luv { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -587,6 +610,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Luv { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Luv", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("u", &self.u)?; + state.serialize_field("v", &self.v)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Luv where T: Into, diff --git a/src/space/cie/xyy.rs b/src/space/cie/xyy.rs index 5aad505..71c9d34 100644 --- a/src/space/cie/xyy.rs +++ b/src/space/cie/xyy.rs @@ -336,6 +336,30 @@ impl ColorSpace<3> for Xyy { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Xyy { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct XyyData { + x: Component, + y: Component, + #[serde(rename = "Y")] + big_y: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = XyyData::deserialize(deserializer)?; + Ok(Self { + x_chrom: data.x, + y_chrom: data.y, + big_y: data.big_y, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Xyy { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -560,6 +584,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Xyy { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Xyy", field_count)?; + state.serialize_field("x", &self.x_chrom)?; + state.serialize_field("y", &self.y_chrom)?; + state.serialize_field("Y", &self.big_y)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Xyy where T: Into, diff --git a/src/space/cie/xyz.rs b/src/space/cie/xyz.rs index 0b98c52..0fb055b 100644 --- a/src/space/cie/xyz.rs +++ b/src/space/cie/xyz.rs @@ -589,6 +589,29 @@ impl ColorSpace<3> for Xyz { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Xyz { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct XyzData { + x: Component, + y: Component, + z: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = XyzData::deserialize(deserializer)?; + Ok(Self { + x: data.x, + y: data.y, + z: data.z, + alpha: data.alpha, + context: ColorimetricContext::default(), + }) + } +} + impl Display for Xyz { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -811,6 +834,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Xyz { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Xyz", field_count)?; + state.serialize_field("x", &self.x)?; + state.serialize_field("y", &self.y)?; + state.serialize_field("z", &self.z)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Xyz where T: Into, diff --git a/src/space/cylindrical/hsi.rs b/src/space/cylindrical/hsi.rs index 127bfa7..e9b5e1c 100644 --- a/src/space/cylindrical/hsi.rs +++ b/src/space/cylindrical/hsi.rs @@ -523,6 +523,33 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Hsi +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct HsiData { + h: Component, + s: Component, + i: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = HsiData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + i: data.i, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Hsi where S: RgbSpec, @@ -798,6 +825,26 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Hsi +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Hsi", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("i", &self.i)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Hsi where S: RgbSpec, diff --git a/src/space/cylindrical/hsl.rs b/src/space/cylindrical/hsl.rs index f0c1c58..a357a5c 100644 --- a/src/space/cylindrical/hsl.rs +++ b/src/space/cylindrical/hsl.rs @@ -550,6 +550,33 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Hsl +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct HslData { + h: Component, + s: Component, + l: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = HslData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + l: data.l, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Hsl where S: RgbSpec, @@ -825,6 +852,26 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Hsl +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Hsl", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("l", &self.l)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Hsl where S: RgbSpec, diff --git a/src/space/cylindrical/hsv.rs b/src/space/cylindrical/hsv.rs index 012e9f9..103131a 100644 --- a/src/space/cylindrical/hsv.rs +++ b/src/space/cylindrical/hsv.rs @@ -629,6 +629,33 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Hsv +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct HsvData { + h: Component, + s: Component, + v: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = HsvData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + v: data.v, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Hsv where S: RgbSpec, @@ -904,6 +931,26 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Hsv +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Hsv", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("v", &self.v)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Hsv where S: RgbSpec, diff --git a/src/space/cylindrical/hwb.rs b/src/space/cylindrical/hwb.rs index c522f73..e953f64 100644 --- a/src/space/cylindrical/hwb.rs +++ b/src/space/cylindrical/hwb.rs @@ -550,6 +550,33 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Hwb +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct HwbData { + h: Component, + w: Component, + b: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = HwbData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + w: data.w, + b: data.b, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Hwb where S: RgbSpec, @@ -825,6 +852,26 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Hwb +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Hwb", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("w", &self.w)?; + state.serialize_field("b", &self.b)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Hwb where S: RgbSpec, diff --git a/src/space/perceptual/hpluv.rs b/src/space/perceptual/hpluv.rs index 294ab04..fddb4a2 100644 --- a/src/space/perceptual/hpluv.rs +++ b/src/space/perceptual/hpluv.rs @@ -504,6 +504,29 @@ impl ColorSpace<3> for Hpluv { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Hpluv { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct HpluvData { + h: Component, + s: Component, + l: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = HpluvData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + l: data.l, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Hpluv { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(2); @@ -726,6 +749,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Hpluv { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Hpluv", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("l", &self.l)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Hpluv where T: Into, diff --git a/src/space/perceptual/hsluv.rs b/src/space/perceptual/hsluv.rs index 0152d42..3483264 100644 --- a/src/space/perceptual/hsluv.rs +++ b/src/space/perceptual/hsluv.rs @@ -503,6 +503,29 @@ impl ColorSpace<3> for Hsluv { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Hsluv { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct HsluvData { + h: Component, + s: Component, + l: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = HsluvData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + l: data.l, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Hsluv { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(2); @@ -725,6 +748,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Hsluv { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Hsluv", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("l", &self.l)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Hsluv where T: Into, diff --git a/src/space/perceptual/okhsl.rs b/src/space/perceptual/okhsl.rs index cedabf1..519a7bf 100644 --- a/src/space/perceptual/okhsl.rs +++ b/src/space/perceptual/okhsl.rs @@ -493,6 +493,29 @@ impl ColorSpace<3> for Okhsl { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Okhsl { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct OkhslData { + h: Component, + s: Component, + l: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = OkhslData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + l: data.l, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Okhsl { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(2); @@ -715,6 +738,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Okhsl { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Okhsl", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("l", &self.l)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Okhsl where T: Into, diff --git a/src/space/perceptual/okhsv.rs b/src/space/perceptual/okhsv.rs index b783429..fdafb31 100644 --- a/src/space/perceptual/okhsv.rs +++ b/src/space/perceptual/okhsv.rs @@ -512,6 +512,29 @@ impl ColorSpace<3> for Okhsv { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Okhsv { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct OkhsvData { + h: Component, + s: Component, + v: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = OkhsvData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + s: data.s, + v: data.v, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Okhsv { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(2); @@ -734,6 +757,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Okhsv { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Okhsv", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("s", &self.s)?; + state.serialize_field("v", &self.v)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Okhsv where T: Into, diff --git a/src/space/perceptual/okhwb.rs b/src/space/perceptual/okhwb.rs index 9a1c83a..ac476fc 100644 --- a/src/space/perceptual/okhwb.rs +++ b/src/space/perceptual/okhwb.rs @@ -492,6 +492,29 @@ impl ColorSpace<3> for Okhwb { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Okhwb { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct OkhwbData { + h: Component, + w: Component, + b: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = OkhwbData::deserialize(deserializer)?; + Ok(Self { + h: data.h, + w: data.w, + b: data.b, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Okhwb { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(2); @@ -713,6 +736,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Okhwb { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Okhwb", field_count)?; + state.serialize_field("h", &self.h)?; + state.serialize_field("w", &self.w)?; + state.serialize_field("b", &self.b)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Okhwb where T: Into, diff --git a/src/space/perceptual/oklab.rs b/src/space/perceptual/oklab.rs index 0011aad..638ef68 100644 --- a/src/space/perceptual/oklab.rs +++ b/src/space/perceptual/oklab.rs @@ -516,6 +516,29 @@ impl ColorSpace<3> for Oklab { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Oklab { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct OklabData { + l: Component, + a: Component, + b: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = OklabData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + a: data.a, + b: data.b, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Oklab { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -737,6 +760,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Oklab { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Oklab", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("a", &self.a)?; + state.serialize_field("b", &self.b)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Oklab where T: Into, diff --git a/src/space/perceptual/oklch.rs b/src/space/perceptual/oklch.rs index 8c8dc39..e0e0d0d 100644 --- a/src/space/perceptual/oklch.rs +++ b/src/space/perceptual/oklch.rs @@ -507,6 +507,29 @@ impl ColorSpace<3> for Oklch { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Oklch { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct OklchData { + l: Component, + c: Component, + h: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = OklchData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + c: data.c, + h: data.h, + alpha: data.alpha, + context: Self::DEFAULT_CONTEXT, + }) + } +} + impl Display for Oklch { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -729,6 +752,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Oklch { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Oklch", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("c", &self.c)?; + state.serialize_field("h", &self.h)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Oklch where T: Into, diff --git a/src/space/physiological/lms.rs b/src/space/physiological/lms.rs index e96943b..d04f542 100644 --- a/src/space/physiological/lms.rs +++ b/src/space/physiological/lms.rs @@ -459,6 +459,29 @@ impl ColorSpace<3> for Lms { } } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Lms { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct LmsData { + l: Component, + m: Component, + s: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = LmsData::deserialize(deserializer)?; + Ok(Self { + l: data.l, + m: data.m, + s: data.s, + alpha: data.alpha, + context: ColorimetricContext::default(), + }) + } +} + impl Display for Lms { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let precision = f.precision().unwrap_or(4); @@ -681,6 +704,23 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Lms { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Lms", field_count)?; + state.serialize_field("l", &self.l)?; + state.serialize_field("m", &self.m)?; + state.serialize_field("s", &self.s)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Lms where T: Into, diff --git a/src/space/rgb/space.rs b/src/space/rgb/space.rs index f5825c9..56cebbd 100644 --- a/src/space/rgb/space.rs +++ b/src/space/rgb/space.rs @@ -1148,6 +1148,33 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Rgb +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct RgbData { + r: Component, + g: Component, + b: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = RgbData::deserialize(deserializer)?; + Ok(Self { + r: data.r, + g: data.g, + b: data.b, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Rgb where S: RgbSpec, @@ -1429,6 +1456,26 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Rgb +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Rgb", field_count)?; + state.serialize_field("r", &self.r)?; + state.serialize_field("g", &self.g)?; + state.serialize_field("b", &self.b)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Rgb where S: RgbSpec, diff --git a/src/space/subtractive/cmy.rs b/src/space/subtractive/cmy.rs index 078aa64..04d0dfd 100644 --- a/src/space/subtractive/cmy.rs +++ b/src/space/subtractive/cmy.rs @@ -509,6 +509,33 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Cmy +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct CmyData { + c: Component, + m: Component, + y: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = CmyData::deserialize(deserializer)?; + Ok(Self { + c: data.c, + m: data.m, + y: data.y, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Cmy where S: RgbSpec, @@ -784,6 +811,26 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Cmy +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 4 } else { 3 }; + let mut state = serializer.serialize_struct("Cmy", field_count)?; + state.serialize_field("c", &self.c)?; + state.serialize_field("m", &self.m)?; + state.serialize_field("y", &self.y)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Cmy where S: RgbSpec, diff --git a/src/space/subtractive/cmyk.rs b/src/space/subtractive/cmyk.rs index f85e2e8..c04d422 100644 --- a/src/space/subtractive/cmyk.rs +++ b/src/space/subtractive/cmyk.rs @@ -633,6 +633,35 @@ where } } +#[cfg(feature = "serde")] +impl<'de, S> serde::Deserialize<'de> for Cmyk +where + S: RgbSpec, +{ + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct CmykData { + c: Component, + m: Component, + y: Component, + k: Component, + #[serde(default = "crate::component::default_alpha")] + alpha: Component, + } + + let data = CmykData::deserialize(deserializer)?; + Ok(Self { + c: data.c, + m: data.m, + y: data.y, + k: data.k, + alpha: data.alpha, + context: S::CONTEXT, + _spec: PhantomData, + }) + } +} + impl Display for Cmyk where S: RgbSpec, @@ -910,6 +939,27 @@ where } } +#[cfg(feature = "serde")] +impl serde::Serialize for Cmyk +where + S: RgbSpec, +{ + fn serialize(&self, serializer: Ser) -> Result { + use serde::ser::SerializeStruct; + + let field_count = if self.alpha.0 < 1.0 { 5 } else { 4 }; + let mut state = serializer.serialize_struct("Cmyk", field_count)?; + state.serialize_field("c", &self.c)?; + state.serialize_field("m", &self.m)?; + state.serialize_field("y", &self.y)?; + state.serialize_field("k", &self.k)?; + if self.alpha.0 < 1.0 { + state.serialize_field("alpha", &self.alpha)?; + } + state.end() + } +} + impl Sub for Cmyk where S: RgbSpec, diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 0000000..31c6a4f --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,224 @@ +#![cfg(feature = "serde")] + +use farg::space::{ColorSpace, Rgb, Srgb, Xyz}; + +mod xyz { + use super::*; + + #[test] + fn it_roundtrips_through_json() { + let color = Xyz::new(0.5, 0.4, 0.3); + let json = serde_json::to_string(&color).unwrap(); + let back: Xyz = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.x(), back.x()); + assert_eq!(color.y(), back.y()); + assert_eq!(color.z(), back.z()); + assert_eq!(back.alpha(), 1.0); + } + + #[test] + fn it_skips_alpha_when_opaque() { + let color = Xyz::new(0.5, 0.4, 0.3); + let json = serde_json::to_string(&color).unwrap(); + let value: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert!(value.get("alpha").is_none()); + } + + #[test] + fn it_includes_alpha_when_translucent() { + let color = Xyz::new(0.5, 0.4, 0.3).with_alpha(0.7); + let json = serde_json::to_string(&color).unwrap(); + let value: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert!(value.get("alpha").is_some()); + } + + #[test] + fn it_defaults_alpha_to_one_on_deserialize() { + let json = r#"{"x":0.5,"y":0.4,"z":0.3}"#; + let color: Xyz = serde_json::from_str(json).unwrap(); + + assert_eq!(color.alpha(), 1.0); + } + + #[test] + fn it_roundtrips_with_alpha() { + let color = Xyz::new(0.5, 0.4, 0.3).with_alpha(0.5); + let json = serde_json::to_string(&color).unwrap(); + let back: Xyz = serde_json::from_str(&json).unwrap(); + + assert!((back.alpha() - 0.5).abs() < 1e-10); + } +} + +mod rgb { + use super::*; + + #[test] + fn it_roundtrips_through_json() { + let color = Rgb::::new(200, 100, 50); + let json = serde_json::to_string(&color).unwrap(); + let back: Rgb = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.red(), back.red()); + assert_eq!(color.green(), back.green()); + assert_eq!(color.blue(), back.blue()); + } + + #[test] + fn it_serializes_with_r_g_b_fields() { + let color = Rgb::::new(200, 100, 50); + let value: serde_json::Value = serde_json::to_value(&color).unwrap(); + + assert!(value.get("r").is_some()); + assert!(value.get("g").is_some()); + assert!(value.get("b").is_some()); + } +} + +mod lms { + use farg::space::Lms; + + #[test] + fn it_roundtrips_through_json() { + let color = Lms::new(0.5, 0.4, 0.3); + let json = serde_json::to_string(&color).unwrap(); + let back: Lms = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.l(), back.l()); + assert_eq!(color.m(), back.m()); + assert_eq!(color.s(), back.s()); + } +} + +#[cfg(feature = "space-xyy")] +mod xyy { + use farg::space::{ColorSpace, Xyy}; + + #[test] + fn it_uses_renamed_fields() { + let color = Xyy::new(0.3127, 0.3290, 1.0); + let value: serde_json::Value = serde_json::to_value(&color).unwrap(); + + assert!(value.get("x").is_some()); + assert!(value.get("y").is_some()); + assert!(value.get("Y").is_some()); + assert!(value.get("x_chrom").is_none()); + assert!(value.get("big_y").is_none()); + } + + #[test] + fn it_roundtrips_through_json() { + let color = Xyy::new(0.3127, 0.3290, 1.0); + let json = serde_json::to_string(&color).unwrap(); + let back: Xyy = serde_json::from_str(&json).unwrap(); + + assert!((color.x() - back.x()).abs() < 1e-10); + assert!((color.y() - back.y()).abs() < 1e-10); + assert!((color.big_y() - back.big_y()).abs() < 1e-10); + } + + #[test] + fn it_deserializes_with_capital_y() { + let json = r#"{"x":0.3127,"y":0.329,"Y":1.0}"#; + let color: Xyy = serde_json::from_str(json).unwrap(); + + assert!((color.big_y() - 1.0).abs() < 1e-10); + } + + #[test] + fn it_roundtrips_with_alpha() { + let color = Xyy::new(0.3127, 0.3290, 1.0).with_alpha(0.3); + let json = serde_json::to_string(&color).unwrap(); + let back: Xyy = serde_json::from_str(&json).unwrap(); + + assert!((back.alpha() - 0.3).abs() < 1e-10); + } +} + +#[cfg(feature = "space-lab")] +mod lab { + use farg::space::Lab; + + #[test] + fn it_roundtrips_through_json() { + let color = Lab::new(50.0, 25.0, -10.0); + let json = serde_json::to_string(&color).unwrap(); + let back: Lab = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.l(), back.l()); + assert_eq!(color.a(), back.a()); + assert_eq!(color.b(), back.b()); + } +} + +#[cfg(feature = "space-oklab")] +mod oklab { + use farg::space::Oklab; + + #[test] + fn it_roundtrips_through_json() { + let color = Oklab::new(0.7, 0.1, -0.05); + let json = serde_json::to_string(&color).unwrap(); + let back: Oklab = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.l(), back.l()); + assert_eq!(color.a(), back.a()); + assert_eq!(color.b(), back.b()); + } +} + +#[cfg(feature = "space-hsl")] +mod hsl { + use farg::space::{ColorSpace, Hsl, Srgb}; + + #[test] + fn it_roundtrips_through_json() { + let color = Hsl::::new(180.0, 50.0, 50.0); + let json = serde_json::to_string(&color).unwrap(); + let back: Hsl = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.hue(), back.hue()); + assert_eq!(color.saturation(), back.saturation()); + assert_eq!(color.lightness(), back.lightness()); + } + + #[test] + fn it_roundtrips_with_alpha() { + let color = Hsl::::new(180.0, 50.0, 50.0).with_alpha(0.8); + let json = serde_json::to_string(&color).unwrap(); + let back: Hsl = serde_json::from_str(&json).unwrap(); + + assert!((back.alpha() - 0.8).abs() < 1e-10); + } +} + +#[cfg(feature = "space-cmyk")] +mod cmyk { + use farg::space::{Cmyk, Srgb}; + + #[test] + fn it_roundtrips_through_json() { + let color = Cmyk::::new(25.0, 50.0, 75.0, 10.0); + let json = serde_json::to_string(&color).unwrap(); + let back: Cmyk = serde_json::from_str(&json).unwrap(); + + assert_eq!(color.cyan(), back.cyan()); + assert_eq!(color.magenta(), back.magenta()); + assert_eq!(color.yellow(), back.yellow()); + assert_eq!(color.key(), back.key()); + } + + #[test] + fn it_serializes_four_component_fields() { + let color = Cmyk::::new(25.0, 50.0, 75.0, 10.0); + let value: serde_json::Value = serde_json::to_value(&color).unwrap(); + + assert!(value.get("c").is_some()); + assert!(value.get("m").is_some()); + assert!(value.get("y").is_some()); + assert!(value.get("k").is_some()); + } +} -- knotmirror 0.0.1