wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use dicom_dictionary_std::uids;
use sha2::{Digest, Sha256};

use crate::annotations::dicom_dataset::new_dicom_uid;
use crate::annotations::dicom_file::encoded_dicom_prefix_length;
use crate::{Error, Result};

use super::document::ParametricMapDocument;
use super::frame::{encode_frame, frame_byte_length, FrameKey};
use super::plan::{
    collect_keys, ParametricMapPartPlan, ParametricMapPlan, PartDraft, PlanIdentifiers,
};
use super::writer::{build_object, ConcatenationSpec, InstanceSpec};

const MAX_FRAMES_PER_INSTANCE: u32 = 65_535;
const EXPLICIT_LONG_HEADER_BYTES: u64 = 12;

#[derive(Clone, Copy)]
struct InstanceLimits {
    maximum_bytes: u64,
    frame_bytes: u64,
}

#[derive(Clone, Copy)]
struct PartPosition {
    number: u16,
    total: u16,
    concatenated: bool,
}

impl ParametricMapDocument {
    pub fn plan(&self, maximum_instance_bytes: u64) -> Result<ParametricMapPlan> {
        let frame_bytes = frame_byte_length(self.descriptor, self.precision)?;
        let frame_bytes = u64::try_from(frame_bytes)
            .map_err(|_| Error::InvalidInput("PM frame byte length does not fit u64".into()))?;
        if maximum_instance_bytes <= EXPLICIT_LONG_HEADER_BYTES + frame_bytes {
            return Err(Error::InvalidInput(format!(
                "maximum PM instance size {maximum_instance_bytes} cannot contain one encoded frame"
            )));
        }
        let identifiers = PlanIdentifiers::new();
        let limits = InstanceLimits {
            maximum_bytes: maximum_instance_bytes,
            frame_bytes,
        };
        if let Some(draft) = self.plan_single_instance(&identifiers, limits)? {
            return self.finish_plan(identifiers, vec![draft], maximum_instance_bytes, false);
        }
        let drafts = plan_concatenation_parts(self, &identifiers, limits)?;
        self.finish_plan(identifiers, drafts, maximum_instance_bytes, true)
    }

    fn plan_single_instance(
        &self,
        identifiers: &PlanIdentifiers,
        limits: InstanceLimits,
    ) -> Result<Option<PartDraft>> {
        if self.frame_count > MAX_FRAMES_PER_INSTANCE {
            return Ok(None);
        }
        let keys = collect_keys(&mut self.output_frames(), self.frame_count)?;
        let sop_instance_uid = new_dicom_uid();
        let spec = identifiers.instance_spec(&sop_instance_uid, &keys, 0, 1, 1, false);
        if encoded_length(self, &spec, limits.frame_bytes)? > limits.maximum_bytes {
            return Ok(None);
        }
        Ok(Some(PartDraft {
            sop_instance_uid,
            frame_offset: 0,
            frame_count: self.frame_count,
        }))
    }

    fn finish_plan(
        &self,
        identifiers: PlanIdentifiers,
        drafts: Vec<PartDraft>,
        maximum_instance_bytes: u64,
        concatenated: bool,
    ) -> Result<ParametricMapPlan> {
        let total = u16::try_from(drafts.len())
            .map_err(|_| Error::InvalidInput("PM concatenation exceeds 65535 instances".into()))?;
        let mut cursor = self.output_frames();
        let mut aggregate_digest = Sha256::new();
        let mut parts = Vec::with_capacity(drafts.len());
        for (index, draft) in drafts.into_iter().enumerate() {
            let keys = collect_keys(&mut cursor, draft.frame_count)?;
            let number = u16::try_from(index + 1).map_err(|_| {
                Error::InvalidInput("PM concatenation part number exceeds US".into())
            })?;
            parts.push(finalize_part(
                self,
                &identifiers,
                draft,
                &keys,
                PartPosition {
                    number,
                    total,
                    concatenated,
                },
                maximum_instance_bytes,
                &mut aggregate_digest,
            )?);
        }
        let aggregate_digest: [u8; 32] = aggregate_digest.finalize().into();
        if cursor.next()?.is_some() || aggregate_digest != self.pixel_digest {
            return Err(Error::InvalidInput(
                "PM raster changed after semantic preflight".into(),
            ));
        }
        Ok(ParametricMapPlan {
            semantic_digest: self.semantic_digest().to_string(),
            series_instance_uid: identifiers.series_instance_uid,
            dimension_organization_uid: identifiers.dimension_organization_uid,
            concatenation_uid: concatenated.then_some(identifiers.concatenation_uid),
            concatenation_source_sop_instance_uid: concatenated
                .then_some(identifiers.concatenation_source_sop_instance_uid),
            content_date: identifiers.content_date,
            content_time: identifiers.content_time,
            parts,
        })
    }
}

fn finalize_part(
    document: &ParametricMapDocument,
    identifiers: &PlanIdentifiers,
    draft: PartDraft,
    keys: &[FrameKey],
    position: PartPosition,
    maximum_instance_bytes: u64,
    aggregate_digest: &mut Sha256,
) -> Result<ParametricMapPartPlan> {
    let spec = identifiers.instance_spec(
        &draft.sop_instance_uid,
        keys,
        draft.frame_offset,
        position.number,
        position.total,
        position.concatenated,
    );
    let mut part_digest = Sha256::new();
    let mut pixel_length = 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 while finalizing the write plan".into(),
            ));
        }
        part_digest.update(&frame.bytes);
        aggregate_digest.update(&frame.bytes);
        pixel_length = pixel_length
            .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 pixel value length overflows u64".into()))?;
    }
    let pixel_value_length = u32::try_from(pixel_length)
        .map_err(|_| Error::InvalidInput("PM pixel value exceeds DICOM VL".into()))?;
    let object = build_object(document, &spec)?;
    let encoded_file_length =
        encoded_dicom_prefix_length(object, uids::PARAMETRIC_MAP_STORAGE, spec.sop_instance_uid)?
            .checked_add(EXPLICIT_LONG_HEADER_BYTES)
            .and_then(|value| value.checked_add(pixel_length))
            .ok_or_else(|| Error::InvalidInput("PM file length overflows u64".into()))?;
    if encoded_file_length > maximum_instance_bytes {
        return Err(Error::InvalidInput(format!(
            "planned PM part {number} is {encoded_file_length} bytes, exceeding the {maximum_instance_bytes}-byte instance limit"
            , number = position.number
        )));
    }
    Ok(ParametricMapPartPlan {
        sop_instance_uid: draft.sop_instance_uid,
        frame_offset: draft.frame_offset,
        frame_count: draft.frame_count,
        pixel_value_length,
        encoded_file_length,
        pixel_sha256: format!("{:x}", part_digest.finalize()),
    })
}

fn plan_concatenation_parts(
    document: &ParametricMapDocument,
    identifiers: &PlanIdentifiers,
    limits: InstanceLimits,
) -> Result<Vec<PartDraft>> {
    let value_limit = u64::from(u32::MAX) / limits.frame_bytes;
    let configured_limit = limits.maximum_bytes / limits.frame_bytes;
    let candidate_limit = u64::from(MAX_FRAMES_PER_INSTANCE)
        .min(value_limit)
        .min(configured_limit);
    let candidate_limit = u32::try_from(candidate_limit)
        .map_err(|_| Error::InvalidInput("PM candidate frame count does not fit u32".into()))?;
    if candidate_limit == 0 {
        return Err(Error::InvalidInput(
            "maximum PM instance size cannot contain one pixel frame".into(),
        ));
    }
    build_part_drafts(document, identifiers, limits, candidate_limit)
}

fn build_part_drafts(
    document: &ParametricMapDocument,
    identifiers: &PlanIdentifiers,
    limits: InstanceLimits,
    candidate_limit: u32,
) -> Result<Vec<PartDraft>> {
    let mut cursor = document.output_frames();
    let mut pending = Vec::new();
    let mut drafts = Vec::new();
    let mut frame_offset = 0_u32;
    while frame_offset < document.frame_count {
        let wanted = candidate_limit.min(document.frame_count - frame_offset);
        let wanted = usize::try_from(wanted).map_err(|_| {
            Error::InvalidInput("PM candidate frame count exceeds addressable memory".into())
        })?;
        while pending.len() < wanted {
            let frame = cursor.next()?.ok_or_else(|| {
                Error::InvalidInput("PM raster ended during size planning".into())
            })?;
            pending.push(frame.key);
        }
        let number = u16::try_from(drafts.len() + 1)
            .map_err(|_| Error::InvalidInput("PM concatenation exceeds 65535 instances".into()))?;
        let fitting = largest_fitting_prefix(
            document,
            identifiers,
            &pending,
            frame_offset,
            number,
            limits,
        )?;
        drafts.push(PartDraft {
            sop_instance_uid: new_dicom_uid(),
            frame_offset,
            frame_count: fitting,
        });
        let fitting_usize = usize::try_from(fitting).map_err(|_| {
            Error::InvalidInput("PM planned frame count exceeds addressable memory".into())
        })?;
        pending.drain(..fitting_usize);
        frame_offset = frame_offset.checked_add(fitting).ok_or_else(|| {
            Error::InvalidInput("PM concatenation frame offset overflows UL".into())
        })?;
    }
    Ok(drafts)
}

fn largest_fitting_prefix(
    document: &ParametricMapDocument,
    identifiers: &PlanIdentifiers,
    keys: &[FrameKey],
    frame_offset: u32,
    number: u16,
    limits: InstanceLimits,
) -> Result<u32> {
    let mut low = 1_usize;
    let mut high = keys.len();
    let mut best = 0_usize;
    while low <= high {
        let middle = low + (high - low) / 2;
        let spec = InstanceSpec {
            sop_instance_uid: "2.25.340282366920938463463374607431768211455",
            series_instance_uid: &identifiers.series_instance_uid,
            dimension_organization_uid: &identifiers.dimension_organization_uid,
            frames: &keys[..middle],
            frame_offset,
            concatenation: Some(ConcatenationSpec {
                uid: &identifiers.concatenation_uid,
                source_sop_instance_uid: &identifiers.concatenation_source_sop_instance_uid,
                number,
                total: u16::MAX,
            }),
            content_date: &identifiers.content_date,
            content_time: &identifiers.content_time,
        };
        if encoded_length(document, &spec, limits.frame_bytes)? <= limits.maximum_bytes {
            best = middle;
            low = middle + 1;
        } else {
            high = middle - 1;
        }
    }
    let best = u32::try_from(best)
        .map_err(|_| Error::InvalidInput("PM planned frame count does not fit u32".into()))?;
    if best == 0 {
        return Err(Error::InvalidInput(format!(
            "maximum PM instance size {} cannot contain one frame and required metadata",
            limits.maximum_bytes
        )));
    }
    Ok(best)
}

fn encoded_length(
    document: &ParametricMapDocument,
    spec: &InstanceSpec<'_>,
    frame_bytes: u64,
) -> Result<u64> {
    let frame_count = u64::try_from(spec.frames.len())
        .map_err(|_| Error::InvalidInput("PM instance frame count does not fit u64".into()))?;
    let pixel_length = frame_count
        .checked_mul(frame_bytes)
        .ok_or_else(|| Error::InvalidInput("PM pixel value length overflows".into()))?;
    if pixel_length > u64::from(u32::MAX) {
        return Ok(u64::MAX);
    }
    let object = build_object(document, spec)?;
    encoded_dicom_prefix_length(object, uids::PARAMETRIC_MAP_STORAGE, spec.sop_instance_uid)?
        .checked_add(EXPLICIT_LONG_HEADER_BYTES)
        .and_then(|value| value.checked_add(pixel_length))
        .ok_or_else(|| Error::InvalidInput("PM file length overflows u64".into()))
}