wsi_dicom_annotations/annotations/parametric_map/
planning.rs1use dicom_dictionary_std::uids;
2use sha2::{Digest, Sha256};
3
4use crate::annotations::dicom_dataset::new_dicom_uid;
5use crate::annotations::dicom_file::encoded_dicom_prefix_length;
6use crate::{Error, Result};
7
8use super::document::ParametricMapDocument;
9use super::frame::{encode_frame, frame_byte_length, FrameKey};
10use super::plan::{
11 collect_keys, ParametricMapPartPlan, ParametricMapPlan, PartDraft, PlanIdentifiers,
12};
13use super::writer::{build_object, ConcatenationSpec, InstanceSpec};
14
15const MAX_FRAMES_PER_INSTANCE: u32 = 65_535;
16const EXPLICIT_LONG_HEADER_BYTES: u64 = 12;
17
18#[derive(Clone, Copy)]
19struct InstanceLimits {
20 maximum_bytes: u64,
21 frame_bytes: u64,
22}
23
24#[derive(Clone, Copy)]
25struct PartPosition {
26 number: u16,
27 total: u16,
28 concatenated: bool,
29}
30
31impl ParametricMapDocument {
32 pub fn plan(&self, maximum_instance_bytes: u64) -> Result<ParametricMapPlan> {
33 let frame_bytes = frame_byte_length(self.descriptor, self.precision)?;
34 let frame_bytes = u64::try_from(frame_bytes)
35 .map_err(|_| Error::InvalidInput("PM frame byte length does not fit u64".into()))?;
36 if maximum_instance_bytes <= EXPLICIT_LONG_HEADER_BYTES + frame_bytes {
37 return Err(Error::InvalidInput(format!(
38 "maximum PM instance size {maximum_instance_bytes} cannot contain one encoded frame"
39 )));
40 }
41 let identifiers = PlanIdentifiers::new();
42 let limits = InstanceLimits {
43 maximum_bytes: maximum_instance_bytes,
44 frame_bytes,
45 };
46 if let Some(draft) = self.plan_single_instance(&identifiers, limits)? {
47 return self.finish_plan(identifiers, vec![draft], maximum_instance_bytes, false);
48 }
49 let drafts = plan_concatenation_parts(self, &identifiers, limits)?;
50 self.finish_plan(identifiers, drafts, maximum_instance_bytes, true)
51 }
52
53 fn plan_single_instance(
54 &self,
55 identifiers: &PlanIdentifiers,
56 limits: InstanceLimits,
57 ) -> Result<Option<PartDraft>> {
58 if self.frame_count > MAX_FRAMES_PER_INSTANCE {
59 return Ok(None);
60 }
61 let keys = collect_keys(&mut self.output_frames(), self.frame_count)?;
62 let sop_instance_uid = new_dicom_uid();
63 let spec = identifiers.instance_spec(&sop_instance_uid, &keys, 0, 1, 1, false);
64 if encoded_length(self, &spec, limits.frame_bytes)? > limits.maximum_bytes {
65 return Ok(None);
66 }
67 Ok(Some(PartDraft {
68 sop_instance_uid,
69 frame_offset: 0,
70 frame_count: self.frame_count,
71 }))
72 }
73
74 fn finish_plan(
75 &self,
76 identifiers: PlanIdentifiers,
77 drafts: Vec<PartDraft>,
78 maximum_instance_bytes: u64,
79 concatenated: bool,
80 ) -> Result<ParametricMapPlan> {
81 let total = u16::try_from(drafts.len())
82 .map_err(|_| Error::InvalidInput("PM concatenation exceeds 65535 instances".into()))?;
83 let mut cursor = self.output_frames();
84 let mut aggregate_digest = Sha256::new();
85 let mut parts = Vec::with_capacity(drafts.len());
86 for (index, draft) in drafts.into_iter().enumerate() {
87 let keys = collect_keys(&mut cursor, draft.frame_count)?;
88 let number = u16::try_from(index + 1).map_err(|_| {
89 Error::InvalidInput("PM concatenation part number exceeds US".into())
90 })?;
91 parts.push(finalize_part(
92 self,
93 &identifiers,
94 draft,
95 &keys,
96 PartPosition {
97 number,
98 total,
99 concatenated,
100 },
101 maximum_instance_bytes,
102 &mut aggregate_digest,
103 )?);
104 }
105 let aggregate_digest: [u8; 32] = aggregate_digest.finalize().into();
106 if cursor.next()?.is_some() || aggregate_digest != self.pixel_digest {
107 return Err(Error::InvalidInput(
108 "PM raster changed after semantic preflight".into(),
109 ));
110 }
111 Ok(ParametricMapPlan {
112 semantic_digest: self.semantic_digest().to_string(),
113 series_instance_uid: identifiers.series_instance_uid,
114 dimension_organization_uid: identifiers.dimension_organization_uid,
115 concatenation_uid: concatenated.then_some(identifiers.concatenation_uid),
116 concatenation_source_sop_instance_uid: concatenated
117 .then_some(identifiers.concatenation_source_sop_instance_uid),
118 content_date: identifiers.content_date,
119 content_time: identifiers.content_time,
120 parts,
121 })
122 }
123}
124
125fn finalize_part(
126 document: &ParametricMapDocument,
127 identifiers: &PlanIdentifiers,
128 draft: PartDraft,
129 keys: &[FrameKey],
130 position: PartPosition,
131 maximum_instance_bytes: u64,
132 aggregate_digest: &mut Sha256,
133) -> Result<ParametricMapPartPlan> {
134 let spec = identifiers.instance_spec(
135 &draft.sop_instance_uid,
136 keys,
137 draft.frame_offset,
138 position.number,
139 position.total,
140 position.concatenated,
141 );
142 let mut part_digest = Sha256::new();
143 let mut pixel_length = 0_u64;
144 for key in keys {
145 let frame = encode_frame(&document.raster, document.descriptor, *key)?;
146 if frame.all_missing {
147 return Err(Error::InvalidInput(
148 "PM raster changed while finalizing the write plan".into(),
149 ));
150 }
151 part_digest.update(&frame.bytes);
152 aggregate_digest.update(&frame.bytes);
153 pixel_length = pixel_length
154 .checked_add(
155 u64::try_from(frame.bytes.len())
156 .map_err(|_| Error::InvalidInput("PM frame length does not fit u64".into()))?,
157 )
158 .ok_or_else(|| Error::InvalidInput("PM pixel value length overflows u64".into()))?;
159 }
160 let pixel_value_length = u32::try_from(pixel_length)
161 .map_err(|_| Error::InvalidInput("PM pixel value exceeds DICOM VL".into()))?;
162 let object = build_object(document, &spec)?;
163 let encoded_file_length =
164 encoded_dicom_prefix_length(object, uids::PARAMETRIC_MAP_STORAGE, spec.sop_instance_uid)?
165 .checked_add(EXPLICIT_LONG_HEADER_BYTES)
166 .and_then(|value| value.checked_add(pixel_length))
167 .ok_or_else(|| Error::InvalidInput("PM file length overflows u64".into()))?;
168 if encoded_file_length > maximum_instance_bytes {
169 return Err(Error::InvalidInput(format!(
170 "planned PM part {number} is {encoded_file_length} bytes, exceeding the {maximum_instance_bytes}-byte instance limit"
171 , number = position.number
172 )));
173 }
174 Ok(ParametricMapPartPlan {
175 sop_instance_uid: draft.sop_instance_uid,
176 frame_offset: draft.frame_offset,
177 frame_count: draft.frame_count,
178 pixel_value_length,
179 encoded_file_length,
180 pixel_sha256: format!("{:x}", part_digest.finalize()),
181 })
182}
183
184fn plan_concatenation_parts(
185 document: &ParametricMapDocument,
186 identifiers: &PlanIdentifiers,
187 limits: InstanceLimits,
188) -> Result<Vec<PartDraft>> {
189 let value_limit = u64::from(u32::MAX) / limits.frame_bytes;
190 let configured_limit = limits.maximum_bytes / limits.frame_bytes;
191 let candidate_limit = u64::from(MAX_FRAMES_PER_INSTANCE)
192 .min(value_limit)
193 .min(configured_limit);
194 let candidate_limit = u32::try_from(candidate_limit)
195 .map_err(|_| Error::InvalidInput("PM candidate frame count does not fit u32".into()))?;
196 if candidate_limit == 0 {
197 return Err(Error::InvalidInput(
198 "maximum PM instance size cannot contain one pixel frame".into(),
199 ));
200 }
201 build_part_drafts(document, identifiers, limits, candidate_limit)
202}
203
204fn build_part_drafts(
205 document: &ParametricMapDocument,
206 identifiers: &PlanIdentifiers,
207 limits: InstanceLimits,
208 candidate_limit: u32,
209) -> Result<Vec<PartDraft>> {
210 let mut cursor = document.output_frames();
211 let mut pending = Vec::new();
212 let mut drafts = Vec::new();
213 let mut frame_offset = 0_u32;
214 while frame_offset < document.frame_count {
215 let wanted = candidate_limit.min(document.frame_count - frame_offset);
216 let wanted = usize::try_from(wanted).map_err(|_| {
217 Error::InvalidInput("PM candidate frame count exceeds addressable memory".into())
218 })?;
219 while pending.len() < wanted {
220 let frame = cursor.next()?.ok_or_else(|| {
221 Error::InvalidInput("PM raster ended during size planning".into())
222 })?;
223 pending.push(frame.key);
224 }
225 let number = u16::try_from(drafts.len() + 1)
226 .map_err(|_| Error::InvalidInput("PM concatenation exceeds 65535 instances".into()))?;
227 let fitting = largest_fitting_prefix(
228 document,
229 identifiers,
230 &pending,
231 frame_offset,
232 number,
233 limits,
234 )?;
235 drafts.push(PartDraft {
236 sop_instance_uid: new_dicom_uid(),
237 frame_offset,
238 frame_count: fitting,
239 });
240 let fitting_usize = usize::try_from(fitting).map_err(|_| {
241 Error::InvalidInput("PM planned frame count exceeds addressable memory".into())
242 })?;
243 pending.drain(..fitting_usize);
244 frame_offset = frame_offset.checked_add(fitting).ok_or_else(|| {
245 Error::InvalidInput("PM concatenation frame offset overflows UL".into())
246 })?;
247 }
248 Ok(drafts)
249}
250
251fn largest_fitting_prefix(
252 document: &ParametricMapDocument,
253 identifiers: &PlanIdentifiers,
254 keys: &[FrameKey],
255 frame_offset: u32,
256 number: u16,
257 limits: InstanceLimits,
258) -> Result<u32> {
259 let mut low = 1_usize;
260 let mut high = keys.len();
261 let mut best = 0_usize;
262 while low <= high {
263 let middle = low + (high - low) / 2;
264 let spec = InstanceSpec {
265 sop_instance_uid: "2.25.340282366920938463463374607431768211455",
266 series_instance_uid: &identifiers.series_instance_uid,
267 dimension_organization_uid: &identifiers.dimension_organization_uid,
268 frames: &keys[..middle],
269 frame_offset,
270 concatenation: Some(ConcatenationSpec {
271 uid: &identifiers.concatenation_uid,
272 source_sop_instance_uid: &identifiers.concatenation_source_sop_instance_uid,
273 number,
274 total: u16::MAX,
275 }),
276 content_date: &identifiers.content_date,
277 content_time: &identifiers.content_time,
278 };
279 if encoded_length(document, &spec, limits.frame_bytes)? <= limits.maximum_bytes {
280 best = middle;
281 low = middle + 1;
282 } else {
283 high = middle - 1;
284 }
285 }
286 let best = u32::try_from(best)
287 .map_err(|_| Error::InvalidInput("PM planned frame count does not fit u32".into()))?;
288 if best == 0 {
289 return Err(Error::InvalidInput(format!(
290 "maximum PM instance size {} cannot contain one frame and required metadata",
291 limits.maximum_bytes
292 )));
293 }
294 Ok(best)
295}
296
297fn encoded_length(
298 document: &ParametricMapDocument,
299 spec: &InstanceSpec<'_>,
300 frame_bytes: u64,
301) -> Result<u64> {
302 let frame_count = u64::try_from(spec.frames.len())
303 .map_err(|_| Error::InvalidInput("PM instance frame count does not fit u64".into()))?;
304 let pixel_length = frame_count
305 .checked_mul(frame_bytes)
306 .ok_or_else(|| Error::InvalidInput("PM pixel value length overflows".into()))?;
307 if pixel_length > u64::from(u32::MAX) {
308 return Ok(u64::MAX);
309 }
310 let object = build_object(document, spec)?;
311 encoded_dicom_prefix_length(object, uids::PARAMETRIC_MAP_STORAGE, spec.sop_instance_uid)?
312 .checked_add(EXPLICIT_LONG_HEADER_BYTES)
313 .and_then(|value| value.checked_add(pixel_length))
314 .ok_or_else(|| Error::InvalidInput("PM file length overflows u64".into()))
315}