Skip to main content

wsi_dicom_annotations/annotations/parametric_map/
write.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::io::Write;
4use std::path::Path;
5
6use dicom_core::VR;
7use dicom_dictionary_std::{tags, uids};
8use sha2::{Digest, Sha256};
9
10use crate::annotations::dicom_file::{
11    atomic_write_new_dicom_with_streamed_value, StreamedDicomValue,
12};
13use crate::{Error, Result};
14
15use super::document::ParametricMapDocument;
16use super::frame::{encode_frame, FrameKey, PixelPrecision};
17use super::plan::{collect_keys, ParametricMapInstance, ParametricMapPartPlan, ParametricMapPlan};
18use super::verify::{verify_instance, ExpectedInstance};
19use super::writer::build_object;
20
21impl ParametricMapDocument {
22    pub fn write_single(
23        &self,
24        path: impl AsRef<Path>,
25        maximum_instance_bytes: u64,
26    ) -> Result<ParametricMapInstance> {
27        let plan = self.plan(maximum_instance_bytes)?;
28        if plan.parts.len() != 1 {
29            return Err(Error::InvalidInput(format!(
30                "PM requires {} concatenation parts; use an output directory",
31                plan.parts.len()
32            )));
33        }
34        let mut instances = self.write_planned_parts(&plan, &[path.as_ref()])?;
35        instances.pop().ok_or_else(|| {
36            Error::InvalidInput("PM plan unexpectedly contained no output part".into())
37        })
38    }
39
40    pub fn write_planned_parts(
41        &self,
42        plan: &ParametricMapPlan,
43        paths: &[impl AsRef<Path>],
44    ) -> Result<Vec<ParametricMapInstance>> {
45        self.validate_plan_paths(plan, paths)?;
46        let total = u16::try_from(plan.parts.len()).map_err(|_| {
47            Error::InvalidInput("PM concatenation contains too many instances".into())
48        })?;
49        let mut cursor = self.output_frames();
50        let mut instances = Vec::with_capacity(plan.parts.len());
51        for (index, (part, path)) in plan.parts.iter().zip(paths).enumerate() {
52            let keys = collect_keys(&mut cursor, part.frame_count)?;
53            let number = u16::try_from(index + 1).map_err(|_| {
54                Error::InvalidInput("PM concatenation part number exceeds US".into())
55            })?;
56            let path = path.as_ref();
57            self.write_part(plan, part, &keys, number, total, path)?;
58            instances.push(ParametricMapInstance::from_part(
59                part,
60                &plan.series_instance_uid,
61            ));
62        }
63        if cursor.next()?.is_some() {
64            return Err(Error::InvalidInput(
65                "PM raster changed after size planning".into(),
66            ));
67        }
68        Ok(instances)
69    }
70
71    fn write_part(
72        &self,
73        plan: &ParametricMapPlan,
74        part: &ParametricMapPartPlan,
75        keys: &[FrameKey],
76        number: u16,
77        total: u16,
78        path: &Path,
79    ) -> Result<()> {
80        let spec = plan.instance_spec(part, keys, number, total)?;
81        let object = build_object(self, &spec)?;
82        let streamed = StreamedDicomValue::new(
83            pixel_tag(self.precision),
84            pixel_vr(self.precision),
85            part.pixel_value_length,
86        )?;
87        let expected = ExpectedInstance::new(self, &spec, part);
88        atomic_write_new_dicom_with_streamed_value(
89            path,
90            object,
91            uids::PARAMETRIC_MAP_STORAGE,
92            &part.sop_instance_uid,
93            streamed,
94            |writer| write_part_pixels(self, keys, part, path, writer),
95            |temporary| verify_instance(temporary, &expected),
96        )
97    }
98
99    fn validate_plan_paths(
100        &self,
101        plan: &ParametricMapPlan,
102        paths: &[impl AsRef<Path>],
103    ) -> Result<()> {
104        if plan.semantic_digest != self.semantic_digest() {
105            return Err(Error::InvalidInput(
106                "PM plan does not belong to this raster document".into(),
107            ));
108        }
109        if paths.len() != plan.parts.len() {
110            return Err(Error::InvalidInput(format!(
111                "PM plan requires {} output paths, got {}",
112                plan.parts.len(),
113                paths.len()
114            )));
115        }
116        let mut unique = BTreeSet::new();
117        for path in paths {
118            validate_new_path(path.as_ref(), &mut unique)?;
119        }
120        Ok(())
121    }
122}
123
124fn validate_new_path(path: &Path, unique: &mut BTreeSet<std::path::PathBuf>) -> Result<()> {
125    if !unique.insert(path.to_path_buf()) {
126        return Err(Error::InvalidInput(format!(
127            "duplicate PM output path: {}",
128            path.display()
129        )));
130    }
131    match fs::symlink_metadata(path) {
132        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
133        Ok(_) => Err(Error::InvalidInput(format!(
134            "output path already exists: {}",
135            path.display()
136        ))),
137        Err(source) => Err(Error::Io {
138            path: path.to_path_buf(),
139            source,
140        }),
141    }
142}
143
144fn write_part_pixels(
145    document: &ParametricMapDocument,
146    keys: &[FrameKey],
147    part: &ParametricMapPartPlan,
148    output_path: &Path,
149    writer: &mut impl Write,
150) -> Result<()> {
151    let mut digest = Sha256::new();
152    let mut written = 0_u64;
153    for key in keys {
154        let frame = encode_frame(&document.raster, document.descriptor, *key)?;
155        if frame.all_missing {
156            return Err(Error::InvalidInput(
157                "PM raster changed after write planning".into(),
158            ));
159        }
160        writer.write_all(&frame.bytes).map_err(|source| Error::Io {
161            path: output_path.to_path_buf(),
162            source,
163        })?;
164        digest.update(&frame.bytes);
165        written = written
166            .checked_add(
167                u64::try_from(frame.bytes.len())
168                    .map_err(|_| Error::InvalidInput("PM frame length does not fit u64".into()))?,
169            )
170            .ok_or_else(|| Error::InvalidInput("PM write length overflows u64".into()))?;
171    }
172    if written != u64::from(part.pixel_value_length)
173        || format!("{:x}", digest.finalize()) != part.pixel_sha256
174    {
175        return Err(Error::InvalidInput(
176            "PM raster bytes changed after write planning".into(),
177        ));
178    }
179    Ok(())
180}
181
182fn pixel_tag(precision: PixelPrecision) -> dicom_core::Tag {
183    match precision {
184        PixelPrecision::Float32 => tags::FLOAT_PIXEL_DATA,
185        PixelPrecision::Float64 => tags::DOUBLE_FLOAT_PIXEL_DATA,
186    }
187}
188
189fn pixel_vr(precision: PixelPrecision) -> VR {
190    match precision {
191        PixelPrecision::Float32 => VR::OF,
192        PixelPrecision::Float64 => VR::OD,
193    }
194}