wsi_dicom_annotations/annotations/parametric_map/
write.rs1use 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}