wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use chrono::{Datelike, Local, Timelike};
use dicom_core::value::{DataSetSequence, PrimitiveValue, Value};
use dicom_core::{DataElement, Length, Tag, VR};
use dicom_dictionary_std::tags;
use dicom_object::InMemDicomObject;

use crate::{Error, Result};

use super::model::{is_valid_dicom_uid, validate_text};

pub(crate) fn sequence(tag: Tag, items: Vec<InMemDicomObject>) -> DataElement<InMemDicomObject> {
    DataElement::new(
        tag,
        VR::SQ,
        Value::from(DataSetSequence::new(items, Length::UNDEFINED)),
    )
}

pub(crate) fn put_text(object: &mut InMemDicomObject, tag: Tag, vr: VR, value: &str) {
    object.put(DataElement::new(tag, vr, PrimitiveValue::from(value)));
}

pub(crate) fn dimension_index_item(
    uid: &str,
    pointer: Tag,
    functional_group_pointer: Tag,
    description: &str,
) -> InMemDicomObject {
    let mut item = InMemDicomObject::new_empty();
    put_text(&mut item, tags::DIMENSION_ORGANIZATION_UID, VR::UI, uid);
    item.put(DataElement::new(
        tags::DIMENSION_INDEX_POINTER,
        VR::AT,
        PrimitiveValue::from(pointer),
    ));
    item.put(DataElement::new(
        tags::FUNCTIONAL_GROUP_POINTER,
        VR::AT,
        PrimitiveValue::from(functional_group_pointer),
    ));
    put_text(
        &mut item,
        tags::DIMENSION_DESCRIPTION_LABEL,
        VR::LO,
        description,
    );
    item
}

pub(crate) fn put_optional_text(
    object: &mut InMemDicomObject,
    tag: Tag,
    vr: VR,
    value: Option<&str>,
) {
    if let Some(value) = value {
        put_text(object, tag, vr, value);
    }
}

pub(crate) fn new_dicom_uid() -> String {
    format!("2.25.{}", uuid::Uuid::new_v4().as_u128())
}

pub(crate) fn dicom_now() -> (String, String) {
    let now = Local::now();
    (
        format!("{:04}{:02}{:02}", now.year(), now.month(), now.day()),
        format!(
            "{:02}{:02}{:02}.{:06}",
            now.hour(),
            now.minute(),
            now.second(),
            now.nanosecond() / 1_000
        ),
    )
}

pub(crate) fn sequence_items(object: &InMemDicomObject, tag: Tag) -> Result<&[InMemDicomObject]> {
    object
        .get(tag)
        .and_then(|element| element.items())
        .ok_or_else(|| Error::InvalidInput(format!("missing or invalid sequence {tag}")))
}

pub(crate) fn required_string(object: &InMemDicomObject, tag: Tag) -> Result<String> {
    optional_string(object, tag).ok_or_else(|| {
        Error::InvalidInput(format!("missing or empty required DICOM attribute {tag}"))
    })
}

pub(crate) fn required_u16(object: &InMemDicomObject, tag: Tag) -> Result<u16> {
    object
        .get(tag)
        .and_then(|element| element.to_int::<u16>().ok())
        .ok_or_else(|| Error::InvalidInput(format!("missing or invalid attribute {tag}")))
}

pub(crate) fn required_u32(object: &InMemDicomObject, tag: Tag) -> Result<u32> {
    optional_u32(object, tag)
        .ok_or_else(|| Error::InvalidInput(format!("missing or invalid attribute {tag}")))
}

pub(crate) fn optional_string(object: &InMemDicomObject, tag: Tag) -> Option<String> {
    object
        .get(tag)
        .and_then(|element| element.to_str().ok())
        .map(|value| value.trim_end_matches('\0').trim().to_string())
        .filter(|value| !value.is_empty())
}

pub(crate) fn validated_optional_uid(
    object: &InMemDicomObject,
    tag: Tag,
    name: &str,
) -> Result<Option<String>> {
    let Some(element) = object.get(tag) else {
        return Ok(None);
    };
    if element.vr() != VR::UI {
        return Err(Error::InvalidInput(format!(
            "{name} must use the DICOM UI value representation"
        )));
    }
    let value = element
        .to_str()
        .map_err(|error| Error::InvalidInput(format!("{name} is not valid text: {error}")))?;
    let value = value.trim_end_matches('\0').trim();
    if !is_valid_dicom_uid(value) {
        return Err(Error::InvalidInput(format!(
            "{name} is not a valid DICOM UID"
        )));
    }
    Ok(Some(value.to_string()))
}

pub(crate) fn validated_optional_long_string(
    object: &InMemDicomObject,
    tag: Tag,
    name: &str,
) -> Result<Option<String>> {
    let Some(element) = object.get(tag) else {
        return Ok(None);
    };
    if element.vr() != VR::LO {
        return Err(Error::InvalidInput(format!(
            "{name} must use the DICOM LO value representation"
        )));
    }
    let value = element
        .to_str()
        .map_err(|error| Error::InvalidInput(format!("{name} is not valid text: {error}")))?;
    let value = value.trim_end_matches('\0').trim();
    validate_text(name, value, 64)?;
    Ok(Some(value.to_string()))
}

pub(crate) fn optional_strings(object: &InMemDicomObject, tag: Tag) -> Vec<String> {
    optional_string(object, tag)
        .map(|value| {
            value
                .split('\\')
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(str::to_string)
                .collect()
        })
        .unwrap_or_default()
}

pub(crate) fn optional_u32(object: &InMemDicomObject, tag: Tag) -> Option<u32> {
    object
        .get(tag)
        .and_then(|element| element.to_int::<u32>().ok())
}

pub(crate) fn read_yes_no(object: &InMemDicomObject, tag: Tag, default: bool) -> Result<bool> {
    match optional_string(object, tag).as_deref() {
        Some("YES") => Ok(true),
        Some("NO") => Ok(false),
        None => Ok(default),
        Some(value) => Err(Error::InvalidInput(format!(
            "attribute {tag} must be YES or NO, got {value:?}"
        ))),
    }
}