wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use serde::{Deserialize, Serialize};

use super::color::parse_rgb;
use super::model::{AnnotationClass, AnnotationClassGeometry, AnnotationScheme};
use super::{MAX_CLASSES, MAX_SCHEME_BYTES, SCHEME_SCHEMA_VERSION};
use crate::annotations::json::validate_unique_object_keys;
use crate::annotations::profile::ProfileCode;
use crate::{Error, Result};

impl Serialize for AnnotationScheme {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        RawAnnotationScheme::from_scheme(self).serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for AnnotationScheme {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        RawAnnotationScheme::deserialize(deserializer)?
            .into_scheme()
            .map_err(serde::de::Error::custom)
    }
}

impl AnnotationScheme {
    pub fn from_json(json: &[u8]) -> Result<Self> {
        if json.len() > MAX_SCHEME_BYTES {
            return Err(Error::InvalidInput(
                "annotation scheme exceeds the 4 MiB input limit".into(),
            ));
        }
        validate_unique_object_keys(json, "annotation scheme")?;
        let raw: RawAnnotationScheme = serde_json::from_slice(json).map_err(|error| {
            Error::InvalidInput(format!("annotation scheme is not valid JSON: {error}"))
        })?;
        raw.into_scheme()
    }

    pub fn to_json(&self) -> Result<Vec<u8>> {
        serde_json::to_vec_pretty(&RawAnnotationScheme::from_scheme(self)).map_err(|error| {
            Error::InvalidInput(format!(
                "annotation scheme could not be serialized: {error}"
            ))
        })
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawAnnotationScheme {
    schema_version: u32,
    scheme_id: String,
    scheme_version: u32,
    display_name: String,
    classes: Vec<RawAnnotationClass>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    finding_sites: Vec<ProfileCode>,
}

impl RawAnnotationScheme {
    fn into_scheme(self) -> Result<AnnotationScheme> {
        if self.schema_version != SCHEME_SCHEMA_VERSION {
            return Err(Error::Unsupported(format!(
                "annotation scheme schema version {} is not supported",
                self.schema_version
            )));
        }
        if self.classes.len() > MAX_CLASSES {
            return Err(Error::InvalidInput(format!(
                "annotation scheme cannot define more than {MAX_CLASSES} classes"
            )));
        }
        let classes = self
            .classes
            .into_iter()
            .map(RawAnnotationClass::into_class)
            .collect::<Result<Vec<_>>>()?;
        let finding_sites = self
            .finding_sites
            .into_iter()
            .map(ProfileCode::into_code)
            .collect::<Result<Vec<_>>>()?;
        AnnotationScheme::new(
            self.scheme_id,
            self.scheme_version,
            self.display_name,
            classes,
            finding_sites,
        )
    }

    fn from_scheme(scheme: &AnnotationScheme) -> Self {
        Self {
            schema_version: scheme.schema_version,
            scheme_id: scheme.id.clone(),
            scheme_version: scheme.version,
            display_name: scheme.display_name.clone(),
            classes: scheme
                .classes
                .iter()
                .map(RawAnnotationClass::from_class)
                .collect(),
            finding_sites: scheme
                .finding_sites
                .iter()
                .map(ProfileCode::from_code)
                .collect(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawAnnotationClass {
    id: String,
    label: String,
    geometry: AnnotationClassGeometry,
    display_color: String,
    category: ProfileCode,
    property_type: ProfileCode,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    property_type_modifiers: Vec<ProfileCode>,
}

impl RawAnnotationClass {
    fn into_class(self) -> Result<AnnotationClass> {
        AnnotationClass::new(
            self.id,
            self.label,
            self.geometry,
            parse_rgb(&self.display_color)?,
            self.category.into_code()?,
            self.property_type.into_code()?,
            self.property_type_modifiers
                .into_iter()
                .map(ProfileCode::into_code)
                .collect::<Result<Vec<_>>>()?,
        )
    }

    fn from_class(class: &AnnotationClass) -> Self {
        Self {
            id: class.id.clone(),
            label: class.label.clone(),
            geometry: class.geometry,
            display_color: format!(
                "#{:02X}{:02X}{:02X}",
                class.display_color[0], class.display_color[1], class.display_color[2]
            ),
            category: ProfileCode::from_code(&class.category),
            property_type: ProfileCode::from_code(&class.property_type),
            property_type_modifiers: class
                .property_type_modifiers
                .iter()
                .map(ProfileCode::from_code)
                .collect(),
        }
    }
}