From ff9dcb6c82fcc95b0e344bce032871c9acde7d25 Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Sat, 14 Dec 2024 05:27:39 -0800 Subject: [PATCH 1/2] Add support for non-finite float handling in DICOM JSON --- json/src/de/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ json/src/ser/value.rs | 22 ++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/json/src/de/mod.rs b/json/src/de/mod.rs index bea7f6a8a..d0dd2db11 100644 --- a/json/src/de/mod.rs +++ b/json/src/de/mod.rs @@ -535,4 +535,41 @@ mod tests { assert!(super::from_value::(serialized).is_ok()); } + + #[test] + fn can_resolve_nan_and_inf_float() { + let serialized = serde_json::json!({ + "00020011": { + "vr": "FL", + "Value": [ + 5492.8545, + 5462.5205, + "NaN", + "-inf", + "inf" + ] + } + }); + + let obj: InMemDicomObject = super::from_value(serialized).unwrap(); + let tag = Tag(0x0002, 0x0011); + let element = obj.get(tag).unwrap(); + + // verify NAN, INFINITY, and NEG_INFINITY are correctly deserialized to f32::NAN, f32::INFINITY, and f32::NEG_INFINITY + let actual_values = element.float32_slice().unwrap(); + let expected_values = &[ + 5492.8545, + 5462.5205, + f32::NAN, + f32::NEG_INFINITY, + f32::INFINITY, + ]; + + // need special comparison for NAN values since assert_eq will not match + assert_eq!(actual_values.len(), expected_values.len()); + assert!(actual_values + .iter() + .zip(expected_values.iter()) + .all(|(&a, &b)| (a == b) || (a.is_nan() && b.is_nan()))); + } } diff --git a/json/src/ser/value.rs b/json/src/ser/value.rs index 40e403538..a00a753fd 100644 --- a/json/src/ser/value.rs +++ b/json/src/ser/value.rs @@ -1,4 +1,6 @@ //! DICOM value serialization +use core::f32; + use dicom_core::PrimitiveValue; use serde::ser::SerializeSeq; use serde::Serialize; @@ -102,6 +104,12 @@ impl Serialize for AsNumbers<'_> { for number in numbers { if number.is_finite() { ser.serialize_element(&number)?; + } else if number.is_nan() { + ser.serialize_element(&f32::NAN.to_string())?; + } else if number.is_infinite() && number.is_sign_positive() { + ser.serialize_element(&f32::INFINITY.to_string())?; + } else if number.is_infinite() && number.is_sign_negative() { + ser.serialize_element(&f32::NEG_INFINITY.to_string())?; } else { ser.serialize_element(&Option::<()>::None)?; } @@ -113,6 +121,12 @@ impl Serialize for AsNumbers<'_> { for number in numbers { if number.is_finite() { ser.serialize_element(&number)?; + } else if number.is_nan() { + ser.serialize_element(&f64::NAN.to_string())?; + } else if number.is_infinite() && number.is_sign_positive() { + ser.serialize_element(&f64::INFINITY.to_string())?; + } else if number.is_infinite() && number.is_sign_negative() { + ser.serialize_element(&f64::NEG_INFINITY.to_string())?; } else { ser.serialize_element(&Option::<()>::None)?; } @@ -235,6 +249,14 @@ mod tests { let json = serde_json::to_value(&AsNumbers(&v)).unwrap(); assert_eq!(json, json!([23.5]),); + let v = PrimitiveValue::from([f64::NAN, f64::INFINITY, f64::NEG_INFINITY]); + let json = serde_json::to_value(&AsNumbers(&v)).unwrap(); + assert_eq!(json, json!(["NaN", "inf", "-inf"]),); + + let v = PrimitiveValue::from([f32::NAN, f32::INFINITY, f32::NEG_INFINITY]); + let json = serde_json::to_value(&AsNumbers(&v)).unwrap(); + assert_eq!(json, json!(["NaN", "inf", "-inf"]),); + let v = PrimitiveValue::Empty; let json = serde_json::to_value(&AsNumbers(&v)).unwrap(); assert_eq!(json, json!([])); From 00c272e5eecef7a4eb9e8d0c611fc724d7f07843 Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Sun, 22 Dec 2024 08:24:32 -0800 Subject: [PATCH 2/2] Update to use inline constants and add test for to_multi_floatf64 --- json/src/de/mod.rs | 34 ++++++++++++++++++++++++++-------- json/src/lib.rs | 7 +++++++ json/src/ser/value.rs | 15 ++++++++------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/json/src/de/mod.rs b/json/src/de/mod.rs index d0dd2db11..21b9a6887 100644 --- a/json/src/de/mod.rs +++ b/json/src/de/mod.rs @@ -422,6 +422,17 @@ mod tests { use super::from_str; use dicom_core::{dicom_value, DataElement, Tag, VR}; use dicom_object::InMemDicomObject; + use num_traits::Float; + + /// This asserts that two float slices are equal in size and content. + /// It needs a special comparison for NAN values since assert_eq will not match. + fn assert_float_slice_eq(actual: &[T], expected: &[T]) { + assert_eq!(actual.len(), expected.len()); + assert!(actual + .iter() + .zip(actual.iter()) + .all(|(&a, &b)| (a == b) || (a.is_nan() && b.is_nan()))); + } #[test] fn can_parse_tags() { @@ -539,7 +550,7 @@ mod tests { #[test] fn can_resolve_nan_and_inf_float() { let serialized = serde_json::json!({ - "00020011": { + "0018605A": { "vr": "FL", "Value": [ 5492.8545, @@ -552,7 +563,7 @@ mod tests { }); let obj: InMemDicomObject = super::from_value(serialized).unwrap(); - let tag = Tag(0x0002, 0x0011); + let tag = Tag(0x0018, 0x605A); let element = obj.get(tag).unwrap(); // verify NAN, INFINITY, and NEG_INFINITY are correctly deserialized to f32::NAN, f32::INFINITY, and f32::NEG_INFINITY @@ -565,11 +576,18 @@ mod tests { f32::INFINITY, ]; - // need special comparison for NAN values since assert_eq will not match - assert_eq!(actual_values.len(), expected_values.len()); - assert!(actual_values - .iter() - .zip(expected_values.iter()) - .all(|(&a, &b)| (a == b) || (a.is_nan() && b.is_nan()))); + assert_float_slice_eq(&actual_values, expected_values); + + // validate upcasting to float 64, additional precision (5492.8544921875) is expected beyond original (5492.8545) due to upcasting + let actual_values_multifloat_64 = element.to_multi_float64().unwrap(); + let expected_values_multifloat_64 = &[ + 5492.8544921875, + 5462.5205078125, + f64::NAN, + f64::NEG_INFINITY, + f64::INFINITY, + ]; + + assert_float_slice_eq(&actual_values_multifloat_64, expected_values_multifloat_64); } } diff --git a/json/src/lib.rs b/json/src/lib.rs index 3780b8629..0f8610eb6 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -83,6 +83,13 @@ mod ser; pub use crate::de::{from_reader, from_slice, from_str, from_value}; pub use crate::ser::{to_string, to_string_pretty, to_value, to_vec, to_writer}; +/// Represents the serialized representation of "NaN" (Not a Number) for 32-bit float (FL) and 64-bit float (FD) in DICOM JSON. +pub const NAN: &str = "NaN"; +/// Represents the serialized representation of "inf" (positive infinity) for 32-bit float (FL) and 64-bit float (FD) in DICOM JSON. +pub const INFINITY: &str = "inf"; +/// Represents the serialized representation of "-inf" (negative infinity) for 32-bit float (FL) and 64-bit float (FD) in DICOM JSON. +pub const NEG_INFINITY: &str = "-inf"; + /// A wrapper type for DICOM JSON serialization using [Serde](serde). /// /// Serializing this type will yield JSON data according to the standard. diff --git a/json/src/ser/value.rs b/json/src/ser/value.rs index a00a753fd..a88bb542b 100644 --- a/json/src/ser/value.rs +++ b/json/src/ser/value.rs @@ -1,10 +1,11 @@ //! DICOM value serialization -use core::f32; use dicom_core::PrimitiveValue; use serde::ser::SerializeSeq; use serde::Serialize; +use crate::{INFINITY, NAN, NEG_INFINITY}; + /// Wrapper type for [primitive values][1] /// which should always be encoded as strings. /// @@ -105,11 +106,11 @@ impl Serialize for AsNumbers<'_> { if number.is_finite() { ser.serialize_element(&number)?; } else if number.is_nan() { - ser.serialize_element(&f32::NAN.to_string())?; + ser.serialize_element(NAN)?; } else if number.is_infinite() && number.is_sign_positive() { - ser.serialize_element(&f32::INFINITY.to_string())?; + ser.serialize_element(INFINITY)?; } else if number.is_infinite() && number.is_sign_negative() { - ser.serialize_element(&f32::NEG_INFINITY.to_string())?; + ser.serialize_element(NEG_INFINITY)?; } else { ser.serialize_element(&Option::<()>::None)?; } @@ -122,11 +123,11 @@ impl Serialize for AsNumbers<'_> { if number.is_finite() { ser.serialize_element(&number)?; } else if number.is_nan() { - ser.serialize_element(&f64::NAN.to_string())?; + ser.serialize_element(NAN)?; } else if number.is_infinite() && number.is_sign_positive() { - ser.serialize_element(&f64::INFINITY.to_string())?; + ser.serialize_element(INFINITY)?; } else if number.is_infinite() && number.is_sign_negative() { - ser.serialize_element(&f64::NEG_INFINITY.to_string())?; + ser.serialize_element(NEG_INFINITY)?; } else { ser.serialize_element(&Option::<()>::None)?; }