wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use std::collections::BTreeSet;
use std::fs;
use std::io::Write;
use std::path::Path;

use dicom_core::VR;
use dicom_dictionary_std::{tags, uids};
use sha2::{Digest, Sha256};

use crate::annotations::dicom_file::{
    atomic_write_new_dicom_with_streamed_value, StreamedDicomValue,
};
use crate::{Error, Result};

use super::document::ParametricMapDocument;
use super::frame::{encode_frame, FrameKey, PixelPrecision};
use super::plan::{collect_keys, ParametricMapInstance, ParametricMapPartPlan, ParametricMapPlan};
use super::verify::{verify_instance, ExpectedInstance};
use super::writer::build_object;

impl ParametricMapDocument {
    pub fn write_single(
        &self,
        path: impl AsRef<Path>,
        maximum_instance_bytes: u64,
    ) -> Result<ParametricMapInstance> {
        let plan = self.plan(maximum_instance_bytes)?;
        if plan.parts.len() != 1 {
            return Err(Error::InvalidInput(format!(
                "PM requires {} concatenation parts; use an output directory",
                plan.parts.len()
            )));
        }
        let mut instances = self.write_planned_parts(&plan, &[path.as_ref()])?;
        instances.pop().ok_or_else(|| {
            Error::InvalidInput("PM plan unexpectedly contained no output part".into())
        })
    }

    pub fn write_planned_parts(
        &self,
        plan: &ParametricMapPlan,
        paths: &[impl AsRef<Path>],
    ) -> Result<Vec<ParametricMapInstance>> {
        self.validate_plan_paths(plan, paths)?;
        let total = u16::try_from(plan.parts.len()).map_err(|_| {
            Error::InvalidInput("PM concatenation contains too many instances".into())
        })?;
        let mut cursor = self.output_frames();
        let mut instances = Vec::with_capacity(plan.parts.len());
        for (index, (part, path)) in plan.parts.iter().zip(paths).enumerate() {
            let keys = collect_keys(&mut cursor, part.frame_count)?;
            let number = u16::try_from(index + 1).map_err(|_| {
                Error::InvalidInput("PM concatenation part number exceeds US".into())
            })?;
            let path = path.as_ref();
            self.write_part(plan, part, &keys, number, total, path)?;
            instances.push(ParametricMapInstance::from_part(
                part,
                &plan.series_instance_uid,
            ));
        }
        if cursor.next()?.is_some() {
            return Err(Error::InvalidInput(
                "PM raster changed after size planning".into(),
            ));
        }
        Ok(instances)
    }

    fn write_part(
        &self,
        plan: &ParametricMapPlan,
        part: &ParametricMapPartPlan,
        keys: &[FrameKey],
        number: u16,
        total: u16,
        path: &Path,
    ) -> Result<()> {
        let spec = plan.instance_spec(part, keys, number, total)?;
        let object = build_object(self, &spec)?;
        let streamed = StreamedDicomValue::new(
            pixel_tag(self.precision),
            pixel_vr(self.precision),
            part.pixel_value_length,
        )?;
        let expected = ExpectedInstance::new(self, &spec, part);
        atomic_write_new_dicom_with_streamed_value(
            path,
            object,
            uids::PARAMETRIC_MAP_STORAGE,
            &part.sop_instance_uid,
            streamed,
            |writer| write_part_pixels(self, keys, part, path, writer),
            |temporary| verify_instance(temporary, &expected),
        )
    }

    fn validate_plan_paths(
        &self,
        plan: &ParametricMapPlan,
        paths: &[impl AsRef<Path>],
    ) -> Result<()> {
        if plan.semantic_digest != self.semantic_digest() {
            return Err(Error::InvalidInput(
                "PM plan does not belong to this raster document".into(),
            ));
        }
        if paths.len() != plan.parts.len() {
            return Err(Error::InvalidInput(format!(
                "PM plan requires {} output paths, got {}",
                plan.parts.len(),
                paths.len()
            )));
        }
        let mut unique = BTreeSet::new();
        for path in paths {
            validate_new_path(path.as_ref(), &mut unique)?;
        }
        Ok(())
    }
}

fn validate_new_path(path: &Path, unique: &mut BTreeSet<std::path::PathBuf>) -> Result<()> {
    if !unique.insert(path.to_path_buf()) {
        return Err(Error::InvalidInput(format!(
            "duplicate PM output path: {}",
            path.display()
        )));
    }
    match fs::symlink_metadata(path) {
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Ok(_) => Err(Error::InvalidInput(format!(
            "output path already exists: {}",
            path.display()
        ))),
        Err(source) => Err(Error::Io {
            path: path.to_path_buf(),
            source,
        }),
    }
}

fn write_part_pixels(
    document: &ParametricMapDocument,
    keys: &[FrameKey],
    part: &ParametricMapPartPlan,
    output_path: &Path,
    writer: &mut impl Write,
) -> Result<()> {
    let mut digest = Sha256::new();
    let mut written = 0_u64;
    for key in keys {
        let frame = encode_frame(&document.raster, document.descriptor, *key)?;
        if frame.all_missing {
            return Err(Error::InvalidInput(
                "PM raster changed after write planning".into(),
            ));
        }
        writer.write_all(&frame.bytes).map_err(|source| Error::Io {
            path: output_path.to_path_buf(),
            source,
        })?;
        digest.update(&frame.bytes);
        written = written
            .checked_add(
                u64::try_from(frame.bytes.len())
                    .map_err(|_| Error::InvalidInput("PM frame length does not fit u64".into()))?,
            )
            .ok_or_else(|| Error::InvalidInput("PM write length overflows u64".into()))?;
    }
    if written != u64::from(part.pixel_value_length)
        || format!("{:x}", digest.finalize()) != part.pixel_sha256
    {
        return Err(Error::InvalidInput(
            "PM raster bytes changed after write planning".into(),
        ));
    }
    Ok(())
}

fn pixel_tag(precision: PixelPrecision) -> dicom_core::Tag {
    match precision {
        PixelPrecision::Float32 => tags::FLOAT_PIXEL_DATA,
        PixelPrecision::Float64 => tags::DOUBLE_FLOAT_PIXEL_DATA,
    }
}

fn pixel_vr(precision: PixelPrecision) -> VR {
    match precision {
        PixelPrecision::Float32 => VR::OF,
        PixelPrecision::Float64 => VR::OD,
    }
}