Skip to main content

wsi_dicom_annotations/annotations/parametric_map/
planning.rs

1use 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}