Skip to main content

dicom_toolkit_data/
value.rs

1//! DICOM value types — the `Value` enum and its supporting types.
2//!
3//! Ports DCMTK's per-VR element classes into a single Rust enum, with dedicated
4//! structs for the richer DICOM scalar types (dates, times, person names).
5
6use crate::dataset::DataSet;
7use dicom_toolkit_core::error::{DcmError, DcmResult};
8use dicom_toolkit_dict::Tag;
9use std::fmt;
10
11// ── DicomDate ──────────────────────────────────────────────────────────────────
12
13/// A DICOM DA (Date) value: YYYYMMDD, with optional month and day.
14///
15/// Partial dates are represented by leaving `month` and/or `day` as `0`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub struct DicomDate {
18    pub year: u16,
19    /// 0 when not specified (partial date contains year only).
20    pub month: u8,
21    /// 0 when not specified (partial date contains year+month only).
22    pub day: u8,
23}
24
25impl DicomDate {
26    /// Parse a DICOM DA string: YYYYMMDD, YYYYMM, or YYYY.
27    pub fn parse(s: &str) -> DcmResult<Self> {
28        let s = s.trim();
29        match s.len() {
30            4 => {
31                let year = parse_u16_str(&s[0..4])?;
32                Ok(Self {
33                    year,
34                    month: 0,
35                    day: 0,
36                })
37            }
38            6 => {
39                let year = parse_u16_str(&s[0..4])?;
40                let month = parse_u8_str(&s[4..6])?;
41                Ok(Self {
42                    year,
43                    month,
44                    day: 0,
45                })
46            }
47            8 => {
48                let year = parse_u16_str(&s[0..4])?;
49                let month = parse_u8_str(&s[4..6])?;
50                let day = parse_u8_str(&s[6..8])?;
51                Ok(Self { year, month, day })
52            }
53            _ => Err(DcmError::Other(format!("invalid DICOM date: {:?}", s))),
54        }
55    }
56
57    /// Parse a DICOM DA string, also accepting the legacy "YYYY.MM.DD" format.
58    pub fn from_da_str(s: &str) -> DcmResult<Self> {
59        let s = s.trim();
60        if s.len() == 10 && s.as_bytes().get(4) == Some(&b'.') && s.as_bytes().get(7) == Some(&b'.')
61        {
62            let year = parse_u16_str(&s[0..4])?;
63            let month = parse_u8_str(&s[5..7])?;
64            let day = parse_u8_str(&s[8..10])?;
65            return Ok(Self { year, month, day });
66        }
67        Self::parse(s)
68    }
69}
70
71impl fmt::Display for DicomDate {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        if self.month == 0 {
74            write!(f, "{:04}", self.year)
75        } else if self.day == 0 {
76            write!(f, "{:04}{:02}", self.year, self.month)
77        } else {
78            write!(f, "{:04}{:02}{:02}", self.year, self.month, self.day)
79        }
80    }
81}
82
83// ── DicomTime ──────────────────────────────────────────────────────────────────
84
85/// A DICOM TM (Time) value: HHMMSS.FFFFFF, with optional components.
86///
87/// Partial times are allowed: `HH`, `HHMM`, `HHMMSS`, `HHMMSS.F{1-6}`.
88/// Missing components are stored as `0`.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct DicomTime {
91    pub hour: u8,
92    pub minute: u8,
93    pub second: u8,
94    /// Fractional seconds in microseconds (0–999999).
95    pub fraction: u32,
96}
97
98impl DicomTime {
99    /// Parse a DICOM TM string.
100    pub fn parse(s: &str) -> DcmResult<Self> {
101        let s = s.trim();
102        if s.is_empty() {
103            return Err(DcmError::Other("empty DICOM time string".into()));
104        }
105
106        // Find the fractional part if present
107        let (time_part, fraction) = if let Some(dot_pos) = s.find('.') {
108            let frac_str = &s[dot_pos + 1..];
109            // Pad or truncate to 6 digits
110            let mut padded = String::from(frac_str);
111            while padded.len() < 6 {
112                padded.push('0');
113            }
114            let frac = parse_u32_str(&padded[..6])?;
115            (&s[..dot_pos], frac)
116        } else {
117            (s, 0u32)
118        };
119
120        match time_part.len() {
121            2 => {
122                let hour = parse_u8_str(&time_part[0..2])?;
123                if hour > 23 {
124                    return Err(DcmError::Other(format!(
125                        "invalid hour in DICOM time: {hour}"
126                    )));
127                }
128                Ok(Self {
129                    hour,
130                    minute: 0,
131                    second: 0,
132                    fraction: 0,
133                })
134            }
135            4 => {
136                let hour = parse_u8_str(&time_part[0..2])?;
137                let minute = parse_u8_str(&time_part[2..4])?;
138                if hour > 23 {
139                    return Err(DcmError::Other(format!(
140                        "invalid hour in DICOM time: {hour}"
141                    )));
142                }
143                if minute > 59 {
144                    return Err(DcmError::Other(format!(
145                        "invalid minute in DICOM time: {minute}"
146                    )));
147                }
148                Ok(Self {
149                    hour,
150                    minute,
151                    second: 0,
152                    fraction: 0,
153                })
154            }
155            6 => {
156                let hour = parse_u8_str(&time_part[0..2])?;
157                let minute = parse_u8_str(&time_part[2..4])?;
158                let second = parse_u8_str(&time_part[4..6])?;
159                if hour > 23 {
160                    return Err(DcmError::Other(format!(
161                        "invalid hour in DICOM time: {hour}"
162                    )));
163                }
164                if minute > 59 {
165                    return Err(DcmError::Other(format!(
166                        "invalid minute in DICOM time: {minute}"
167                    )));
168                }
169                if second > 59 {
170                    return Err(DcmError::Other(format!(
171                        "invalid second in DICOM time: {second}"
172                    )));
173                }
174                Ok(Self {
175                    hour,
176                    minute,
177                    second,
178                    fraction,
179                })
180            }
181            _ => Err(DcmError::Other(format!("invalid DICOM time: {:?}", s))),
182        }
183    }
184}
185
186impl fmt::Display for DicomTime {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(f, "{:02}{:02}{:02}", self.hour, self.minute, self.second)?;
189        if self.fraction > 0 {
190            write!(f, ".{:06}", self.fraction)?;
191        }
192        Ok(())
193    }
194}
195
196// ── DicomDateTime ─────────────────────────────────────────────────────────────
197
198/// A DICOM DT (DateTime) value: YYYYMMDDHHMMSS.FFFFFF+ZZZZ.
199#[derive(Debug, Clone, PartialEq)]
200pub struct DicomDateTime {
201    pub date: DicomDate,
202    pub time: Option<DicomTime>,
203    /// UTC offset in minutes, e.g. +0530 → 330, -0500 → -300.
204    pub offset_minutes: Option<i16>,
205}
206
207impl DicomDateTime {
208    /// Parse a DICOM DT string.
209    pub fn parse(s: &str) -> DcmResult<Self> {
210        let s = s.trim();
211        if s.len() < 4 {
212            return Err(DcmError::Other(format!("invalid DICOM datetime: {:?}", s)));
213        }
214
215        // Separate UTC offset: find trailing +/- that belong to timezone
216        // Timezone is the last +HHMM or -HHMM
217        let (dt_part, offset_minutes) = extract_tz_offset(s)?;
218
219        // Date is always the first 8 chars (YYYYMMDD), but may be shorter
220        let date_len = dt_part.len().min(8);
221        let date_str = &dt_part[..date_len];
222        // Pad date string to at least 4 chars
223        let date = DicomDate::parse(date_str)?;
224
225        let time = if dt_part.len() > 8 {
226            Some(DicomTime::parse(&dt_part[8..])?)
227        } else {
228            None
229        };
230
231        Ok(Self {
232            date,
233            time,
234            offset_minutes,
235        })
236    }
237}
238
239/// Extracts an optional trailing timezone offset (+HHMM or -HHMM) from a DT string.
240fn extract_tz_offset(s: &str) -> DcmResult<(&str, Option<i16>)> {
241    // Look for + or - that is not part of the date/time portion.
242    // The date+time part is at most 21 chars (YYYYMMDDHHMMSS.FFFFFF),
243    // so any sign after position 4 is a potential offset.
244    let bytes = s.as_bytes();
245    for i in (1..s.len()).rev() {
246        if bytes[i] == b'+' || bytes[i] == b'-' {
247            let tz_str = &s[i..];
248            if tz_str.len() == 5 {
249                let sign: i16 = if bytes[i] == b'+' { 1 } else { -1 };
250                let hh = parse_u8_str(&tz_str[1..3])? as i16;
251                let mm = parse_u8_str(&tz_str[3..5])? as i16;
252                return Ok((&s[..i], Some(sign * (hh * 60 + mm))));
253            }
254        }
255    }
256    Ok((s, None))
257}
258
259impl fmt::Display for DicomDateTime {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "{}", self.date)?;
262        if let Some(ref t) = self.time {
263            write!(f, "{}", t)?;
264        }
265        if let Some(offset) = self.offset_minutes {
266            let sign = if offset >= 0 { '+' } else { '-' };
267            let abs = offset.unsigned_abs();
268            write!(f, "{}{:02}{:02}", sign, abs / 60, abs % 60)?;
269        }
270        Ok(())
271    }
272}
273
274// ── PersonName ────────────────────────────────────────────────────────────────
275
276/// A DICOM PN (Person Name) value.
277///
278/// Each name consists of up to three component groups (alphabetic, ideographic,
279/// phonetic) separated by `=`. Within each group, the five name components
280/// (family, given, middle, prefix, suffix) are separated by `^`.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct PersonName {
283    pub alphabetic: String,
284    pub ideographic: String,
285    pub phonetic: String,
286}
287
288impl PersonName {
289    /// Parse a DICOM PN string.
290    pub fn parse(s: &str) -> Self {
291        let mut parts = s.splitn(3, '=');
292        PersonName {
293            alphabetic: parts.next().unwrap_or("").to_string(),
294            ideographic: parts.next().unwrap_or("").to_string(),
295            phonetic: parts.next().unwrap_or("").to_string(),
296        }
297    }
298
299    fn component(group: &str, index: usize) -> &str {
300        group.split('^').nth(index).unwrap_or("")
301    }
302
303    pub fn last_name(&self) -> &str {
304        Self::component(&self.alphabetic, 0)
305    }
306
307    pub fn first_name(&self) -> &str {
308        Self::component(&self.alphabetic, 1)
309    }
310
311    pub fn middle_name(&self) -> &str {
312        Self::component(&self.alphabetic, 2)
313    }
314
315    pub fn prefix(&self) -> &str {
316        Self::component(&self.alphabetic, 3)
317    }
318
319    pub fn suffix(&self) -> &str {
320        Self::component(&self.alphabetic, 4)
321    }
322}
323
324impl fmt::Display for PersonName {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        // Emit only as many groups as are non-empty, trimming trailing empty groups.
327        if !self.phonetic.is_empty() {
328            write!(
329                f,
330                "{}={}={}",
331                self.alphabetic, self.ideographic, self.phonetic
332            )
333        } else if !self.ideographic.is_empty() {
334            write!(f, "{}={}", self.alphabetic, self.ideographic)
335        } else {
336            write!(f, "{}", self.alphabetic)
337        }
338    }
339}
340
341// ── PixelData ─────────────────────────────────────────────────────────────────
342
343/// One compressed frame worth of encapsulated Pixel Data fragments.
344#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct EncapsulatedFrame {
346    pub fragments: Vec<Vec<u8>>,
347}
348
349/// Pixel data stored either as native (uncompressed) bytes or encapsulated
350/// (compressed) fragments.
351#[derive(Debug, Clone, PartialEq)]
352pub enum PixelData {
353    /// Uncompressed pixel data.
354    Native { bytes: Vec<u8> },
355    /// Encapsulated (compressed) pixel data with optional offset table.
356    Encapsulated {
357        offset_table: Vec<u32>,
358        fragments: Vec<Vec<u8>>,
359    },
360}
361
362impl PixelData {
363    /// Split encapsulated pixel data into per-frame compressed payloads.
364    pub fn encapsulated_frames(&self, number_of_frames: u32) -> DcmResult<Vec<Vec<u8>>> {
365        encapsulated_frames(self, number_of_frames)
366    }
367}
368
369/// Build encapsulated Pixel Data from per-frame compressed fragments.
370///
371/// Basic Offset Table entries are generated on fragment-item boundaries, using
372/// the same `8 + fragment.len()` accounting expected by [`encapsulated_frames`].
373pub fn build_encapsulated_pixel_data(frames: &[EncapsulatedFrame]) -> DcmResult<PixelData> {
374    if frames.is_empty() {
375        return Err(DcmError::Other(
376            "build_encapsulated_pixel_data requires at least one frame".into(),
377        ));
378    }
379
380    let mut offset_table = Vec::with_capacity(frames.len());
381    let mut fragments = Vec::new();
382    let mut offset = 0u32;
383
384    for (frame_index, frame) in frames.iter().enumerate() {
385        if frame.fragments.is_empty() {
386            return Err(DcmError::Other(format!(
387                "encapsulated frame {} has no fragments",
388                frame_index + 1
389            )));
390        }
391
392        offset_table.push(offset);
393        for fragment in &frame.fragments {
394            offset = offset
395                .checked_add(fragment_item_length(fragment)?)
396                .ok_or_else(|| {
397                    DcmError::Other("fragment stream exceeds u32 offset range".into())
398                })?;
399            fragments.push(fragment.clone());
400        }
401    }
402
403    Ok(PixelData::Encapsulated {
404        offset_table,
405        fragments,
406    })
407}
408
409/// Convenience helper for the common one-fragment-per-frame case.
410pub fn encapsulated_pixel_data_from_frames(frames: &[Vec<u8>]) -> DcmResult<PixelData> {
411    let frames: Vec<EncapsulatedFrame> = frames
412        .iter()
413        .cloned()
414        .map(|fragment| EncapsulatedFrame {
415            fragments: vec![fragment],
416        })
417        .collect();
418    build_encapsulated_pixel_data(&frames)
419}
420
421/// Split encapsulated pixel data into per-frame compressed payloads.
422///
423/// Supports:
424/// - single-frame encapsulated objects
425/// - multi-frame data with one fragment per frame and an empty BOT
426/// - multi-fragment-per-frame data described by the Basic Offset Table
427pub fn encapsulated_frames(
428    pixel_data: &PixelData,
429    number_of_frames: u32,
430) -> DcmResult<Vec<Vec<u8>>> {
431    if number_of_frames == 0 {
432        return Err(DcmError::Other(
433            "number_of_frames must be at least 1 for encapsulated Pixel Data".into(),
434        ));
435    }
436
437    let PixelData::Encapsulated {
438        offset_table,
439        fragments,
440    } = pixel_data
441    else {
442        return Err(DcmError::Other(
443            "encapsulated_frames requires encapsulated Pixel Data".into(),
444        ));
445    };
446
447    if fragments.is_empty() {
448        return Err(DcmError::Other(
449            "encapsulated Pixel Data has no fragments".into(),
450        ));
451    }
452
453    if number_of_frames == 1 {
454        return Ok(vec![fragments.concat()]);
455    }
456
457    if offset_table.is_empty() {
458        if fragments.len() == number_of_frames as usize {
459            return Ok(fragments.clone());
460        }
461        return Err(DcmError::Other(format!(
462            "encapsulated Pixel Data for {number_of_frames} frames requires a Basic Offset Table or one fragment per frame, found {} fragment(s)",
463            fragments.len()
464        )));
465    }
466
467    if offset_table.len() != number_of_frames as usize {
468        return Err(DcmError::Other(format!(
469            "Basic Offset Table has {} entries, expected {number_of_frames}",
470            offset_table.len()
471        )));
472    }
473
474    let fragment_offsets = fragment_start_offsets(fragments)?;
475    let total_length = total_fragment_stream_length(fragments)?;
476    let mut frames = Vec::with_capacity(number_of_frames as usize);
477
478    for frame_index in 0..number_of_frames as usize {
479        let start_offset = offset_table[frame_index];
480        let start_fragment = fragment_offsets
481            .iter()
482            .position(|&offset| offset == start_offset)
483            .ok_or_else(|| {
484                DcmError::Other(format!(
485                    "Basic Offset Table entry {} does not align to a fragment boundary",
486                    frame_index + 1
487                ))
488            })?;
489
490        let end_fragment = if let Some(&next_offset) = offset_table.get(frame_index + 1) {
491            if next_offset < start_offset {
492                return Err(DcmError::Other(format!(
493                    "Basic Offset Table entry {} points before the current frame start",
494                    frame_index + 2
495                )));
496            }
497            fragment_offsets
498                .iter()
499                .position(|&offset| offset == next_offset)
500                .ok_or_else(|| {
501                    DcmError::Other(format!(
502                        "Basic Offset Table entry {} does not align to a fragment boundary",
503                        frame_index + 2
504                    ))
505                })?
506        } else {
507            if start_offset > total_length {
508                return Err(DcmError::Other(
509                    "Basic Offset Table points beyond the fragment stream".into(),
510                ));
511            }
512            fragments.len()
513        };
514
515        if end_fragment <= start_fragment {
516            return Err(DcmError::Other(format!(
517                "frame {} resolves to an empty fragment range",
518                frame_index + 1
519            )));
520        }
521
522        let mut frame = Vec::new();
523        for fragment in &fragments[start_fragment..end_fragment] {
524            frame.extend_from_slice(fragment);
525        }
526        frames.push(frame);
527    }
528
529    Ok(frames)
530}
531
532// ── Value ─────────────────────────────────────────────────────────────────────
533
534/// The value held by a DICOM data element.
535///
536/// Each variant corresponds to one or more DICOM VRs. Numeric string VRs
537/// (DS, IS) are stored already decoded as `f64`/`i64`.
538#[derive(Debug, Clone, PartialEq)]
539pub enum Value {
540    /// No value (zero-length element).
541    Empty,
542    /// AE, CS, LO, LT, SH, ST, UC, UR, UT — multi-valued via backslash.
543    Strings(Vec<String>),
544    /// PN — person name with up to three component groups.
545    PersonNames(Vec<PersonName>),
546    /// UI — UID string.
547    Uid(String),
548    /// DA — date values.
549    Date(Vec<DicomDate>),
550    /// TM — time values.
551    Time(Vec<DicomTime>),
552    /// DT — datetime values.
553    DateTime(Vec<DicomDateTime>),
554    /// IS — integer string, decoded.
555    Ints(Vec<i64>),
556    /// DS — decimal string, decoded.
557    Decimals(Vec<f64>),
558    /// OB, UN — raw bytes.
559    U8(Vec<u8>),
560    /// US, OW — raw 16-bit words (interpret by VR).
561    U16(Vec<u16>),
562    /// SS — signed 16-bit integers.
563    I16(Vec<i16>),
564    /// UL, OL — 32-bit unsigned integers.
565    U32(Vec<u32>),
566    /// SL — 32-bit signed integers.
567    I32(Vec<i32>),
568    /// UV, OV — 64-bit unsigned integers.
569    U64(Vec<u64>),
570    /// SV — 64-bit signed integers.
571    I64(Vec<i64>),
572    /// FL, OF — 32-bit floats.
573    F32(Vec<f32>),
574    /// FD, OD — 64-bit floats.
575    F64(Vec<f64>),
576    /// AT — attribute tag pairs.
577    Tags(Vec<Tag>),
578    /// SQ — sequence of items (datasets).
579    Sequence(Vec<DataSet>),
580    /// Pixel data — (7FE0,0010).
581    PixelData(PixelData),
582}
583
584impl Value {
585    /// Returns the number of values (VM).
586    pub fn multiplicity(&self) -> usize {
587        match self {
588            Value::Empty => 0,
589            Value::Strings(v) => v.len(),
590            Value::PersonNames(v) => v.len(),
591            Value::Uid(_) => 1,
592            Value::Date(v) => v.len(),
593            Value::Time(v) => v.len(),
594            Value::DateTime(v) => v.len(),
595            Value::Ints(v) => v.len(),
596            Value::Decimals(v) => v.len(),
597            Value::U8(v) => v.len(),
598            Value::U16(v) => v.len(),
599            Value::I16(v) => v.len(),
600            Value::U32(v) => v.len(),
601            Value::I32(v) => v.len(),
602            Value::U64(v) => v.len(),
603            Value::I64(v) => v.len(),
604            Value::F32(v) => v.len(),
605            Value::F64(v) => v.len(),
606            Value::Tags(v) => v.len(),
607            Value::Sequence(v) => v.len(),
608            Value::PixelData(_) => 1,
609        }
610    }
611
612    pub fn is_empty(&self) -> bool {
613        self.multiplicity() == 0
614    }
615
616    /// Returns the first string value, if this is a `Strings` or `Uid` variant.
617    pub fn as_string(&self) -> Option<&str> {
618        match self {
619            Value::Strings(v) => v.first().map(|s| s.as_str()),
620            Value::Uid(s) => Some(s.as_str()),
621            Value::PersonNames(v) => v.first().map(|p| p.alphabetic.as_str()),
622            _ => None,
623        }
624    }
625
626    pub fn as_strings(&self) -> Option<&[String]> {
627        match self {
628            Value::Strings(v) => Some(v.as_slice()),
629            _ => None,
630        }
631    }
632
633    pub fn as_u16(&self) -> Option<u16> {
634        match self {
635            Value::U16(v) => v.first().copied(),
636            _ => None,
637        }
638    }
639
640    pub fn as_u32(&self) -> Option<u32> {
641        match self {
642            Value::U32(v) => v.first().copied(),
643            _ => None,
644        }
645    }
646
647    pub fn as_i32(&self) -> Option<i32> {
648        match self {
649            Value::I32(v) => v.first().copied(),
650            _ => None,
651        }
652    }
653
654    pub fn as_f64(&self) -> Option<f64> {
655        match self {
656            Value::F64(v) => v.first().copied(),
657            Value::Decimals(v) => v.first().copied(),
658            _ => None,
659        }
660    }
661
662    pub fn as_bytes(&self) -> Option<&[u8]> {
663        match self {
664            Value::U8(v) => Some(v.as_slice()),
665            Value::PixelData(PixelData::Native { bytes }) => Some(bytes.as_slice()),
666            _ => None,
667        }
668    }
669
670    /// Returns a human-readable string representation (like dcmdump output).
671    pub fn to_display_string(&self) -> String {
672        match self {
673            Value::Empty => String::new(),
674            Value::Strings(v) => v.join("\\"),
675            Value::PersonNames(v) => v
676                .iter()
677                .map(|p| p.to_string())
678                .collect::<Vec<_>>()
679                .join("\\"),
680            Value::Uid(s) => s.clone(),
681            Value::Date(v) => v
682                .iter()
683                .map(|d| d.to_string())
684                .collect::<Vec<_>>()
685                .join("\\"),
686            Value::Time(v) => v
687                .iter()
688                .map(|t| t.to_string())
689                .collect::<Vec<_>>()
690                .join("\\"),
691            Value::DateTime(v) => v
692                .iter()
693                .map(|dt| dt.to_string())
694                .collect::<Vec<_>>()
695                .join("\\"),
696            Value::Ints(v) => v
697                .iter()
698                .map(|n| n.to_string())
699                .collect::<Vec<_>>()
700                .join("\\"),
701            Value::Decimals(v) => v
702                .iter()
703                .map(|n| format_f64(*n))
704                .collect::<Vec<_>>()
705                .join("\\"),
706            Value::U8(v) => format!("({} bytes)", v.len()),
707            Value::U16(v) => v
708                .iter()
709                .map(|n| n.to_string())
710                .collect::<Vec<_>>()
711                .join("\\"),
712            Value::I16(v) => v
713                .iter()
714                .map(|n| n.to_string())
715                .collect::<Vec<_>>()
716                .join("\\"),
717            Value::U32(v) => v
718                .iter()
719                .map(|n| n.to_string())
720                .collect::<Vec<_>>()
721                .join("\\"),
722            Value::I32(v) => v
723                .iter()
724                .map(|n| n.to_string())
725                .collect::<Vec<_>>()
726                .join("\\"),
727            Value::U64(v) => v
728                .iter()
729                .map(|n| n.to_string())
730                .collect::<Vec<_>>()
731                .join("\\"),
732            Value::I64(v) => v
733                .iter()
734                .map(|n| n.to_string())
735                .collect::<Vec<_>>()
736                .join("\\"),
737            Value::F32(v) => v
738                .iter()
739                .map(|n| format!("{}", n))
740                .collect::<Vec<_>>()
741                .join("\\"),
742            Value::F64(v) => v
743                .iter()
744                .map(|n| format_f64(*n))
745                .collect::<Vec<_>>()
746                .join("\\"),
747            Value::Tags(v) => v
748                .iter()
749                .map(|t| format!("({:04X},{:04X})", t.group, t.element))
750                .collect::<Vec<_>>()
751                .join("\\"),
752            Value::Sequence(v) => format!("(Sequence with {} item(s))", v.len()),
753            Value::PixelData(PixelData::Native { bytes }) => {
754                format!("(PixelData, {} bytes)", bytes.len())
755            }
756            Value::PixelData(PixelData::Encapsulated { fragments, .. }) => {
757                format!("(PixelData, {} fragment(s))", fragments.len())
758            }
759        }
760    }
761
762    /// Approximate encoded byte length (for dcmdump `# length` field).
763    pub(crate) fn encoded_len(&self) -> usize {
764        match self {
765            Value::Empty => 0,
766            Value::Strings(v) => {
767                let total: usize = v.iter().map(|s| s.len()).sum();
768                total + v.len().saturating_sub(1)
769            }
770            Value::PersonNames(v) => {
771                let total: usize = v.iter().map(|p| p.to_string().len()).sum();
772                total + v.len().saturating_sub(1)
773            }
774            Value::Uid(s) => s.len(),
775            Value::Date(v) => v.len() * 8,
776            Value::Time(v) => v.len() * 14,
777            Value::DateTime(v) => v.len() * 26,
778            Value::Ints(v) => {
779                v.iter().map(|n| n.to_string().len()).sum::<usize>() + v.len().saturating_sub(1)
780            }
781            Value::Decimals(v) => {
782                v.iter().map(|n| format_f64(*n).len()).sum::<usize>() + v.len().saturating_sub(1)
783            }
784            Value::U8(v) => v.len(),
785            Value::U16(v) => v.len() * 2,
786            Value::I16(v) => v.len() * 2,
787            Value::U32(v) => v.len() * 4,
788            Value::I32(v) => v.len() * 4,
789            Value::U64(v) => v.len() * 8,
790            Value::I64(v) => v.len() * 8,
791            Value::F32(v) => v.len() * 4,
792            Value::F64(v) => v.len() * 8,
793            Value::Tags(v) => v.len() * 4,
794            Value::Sequence(_) => 0,
795            Value::PixelData(PixelData::Native { bytes }) => bytes.len(),
796            Value::PixelData(PixelData::Encapsulated { fragments, .. }) => {
797                fragments.iter().map(|f| f.len()).sum()
798            }
799        }
800    }
801}
802
803// ── Helpers ───────────────────────────────────────────────────────────────────
804
805fn parse_u8_str(s: &str) -> DcmResult<u8> {
806    s.parse::<u8>()
807        .map_err(|_| DcmError::Other(format!("expected u8, got {:?}", s)))
808}
809
810fn parse_u16_str(s: &str) -> DcmResult<u16> {
811    s.parse::<u16>()
812        .map_err(|_| DcmError::Other(format!("expected u16, got {:?}", s)))
813}
814
815fn parse_u32_str(s: &str) -> DcmResult<u32> {
816    s.parse::<u32>()
817        .map_err(|_| DcmError::Other(format!("expected u32, got {:?}", s)))
818}
819
820fn fragment_start_offsets(fragments: &[Vec<u8>]) -> DcmResult<Vec<u32>> {
821    let mut offsets = Vec::with_capacity(fragments.len());
822    let mut cursor = 0u32;
823    for fragment in fragments {
824        offsets.push(cursor);
825        cursor = cursor
826            .checked_add(fragment_item_length(fragment)?)
827            .ok_or_else(|| DcmError::Other("fragment stream exceeds u32 offset range".into()))?;
828    }
829    Ok(offsets)
830}
831
832fn total_fragment_stream_length(fragments: &[Vec<u8>]) -> DcmResult<u32> {
833    fragments.iter().try_fold(0u32, |total, fragment| {
834        total
835            .checked_add(fragment_item_length(fragment)?)
836            .ok_or_else(|| DcmError::Other("fragment stream exceeds u32 offset range".into()))
837    })
838}
839
840fn fragment_item_length(fragment: &[u8]) -> DcmResult<u32> {
841    let len = u32::try_from(fragment.len())
842        .map_err(|_| DcmError::Other("fragment length exceeds u32 range".into()))?;
843    len.checked_add(8)
844        .ok_or_else(|| DcmError::Other("fragment item length exceeds u32 range".into()))
845}
846
847/// Format an f64 without trailing zeros but with at least one decimal place.
848fn format_f64(v: f64) -> String {
849    if v.fract() == 0.0 && v.abs() < 1e15 {
850        format!("{:.1}", v)
851    } else {
852        format!("{}", v)
853    }
854}
855
856// ── Tests ─────────────────────────────────────────────────────────────────────
857
858#[cfg(test)]
859mod tests {
860    use super::*;
861
862    // ── DicomDate ───────────────────────────────────────────────────────
863
864    #[test]
865    fn date_full_parse() {
866        let d = DicomDate::parse("20231215").unwrap();
867        assert_eq!(d.year, 2023);
868        assert_eq!(d.month, 12);
869        assert_eq!(d.day, 15);
870    }
871
872    #[test]
873    fn date_year_only() {
874        let d = DicomDate::parse("2023").unwrap();
875        assert_eq!(d.year, 2023);
876        assert_eq!(d.month, 0);
877        assert_eq!(d.day, 0);
878    }
879
880    #[test]
881    fn date_year_month() {
882        let d = DicomDate::parse("202312").unwrap();
883        assert_eq!(d.year, 2023);
884        assert_eq!(d.month, 12);
885        assert_eq!(d.day, 0);
886    }
887
888    #[test]
889    fn date_display_full() {
890        let d = DicomDate {
891            year: 2023,
892            month: 12,
893            day: 15,
894        };
895        assert_eq!(d.to_string(), "20231215");
896    }
897
898    #[test]
899    fn date_display_partial_year() {
900        let d = DicomDate {
901            year: 2023,
902            month: 0,
903            day: 0,
904        };
905        assert_eq!(d.to_string(), "2023");
906    }
907
908    #[test]
909    fn date_display_partial_year_month() {
910        let d = DicomDate {
911            year: 2023,
912            month: 12,
913            day: 0,
914        };
915        assert_eq!(d.to_string(), "202312");
916    }
917
918    #[test]
919    fn date_legacy_format() {
920        let d = DicomDate::from_da_str("2023.12.15").unwrap();
921        assert_eq!(d.year, 2023);
922        assert_eq!(d.month, 12);
923        assert_eq!(d.day, 15);
924    }
925
926    #[test]
927    fn date_invalid() {
928        assert!(DicomDate::parse("20231").is_err());
929        assert!(DicomDate::parse("2023121").is_err());
930        assert!(DicomDate::parse("abcdefgh").is_err());
931    }
932
933    // ── DicomTime ───────────────────────────────────────────────────────
934
935    #[test]
936    fn time_full_parse() {
937        let t = DicomTime::parse("143022.500000").unwrap();
938        assert_eq!(t.hour, 14);
939        assert_eq!(t.minute, 30);
940        assert_eq!(t.second, 22);
941        assert_eq!(t.fraction, 500000);
942    }
943
944    #[test]
945    fn time_partial_hour() {
946        let t = DicomTime::parse("14").unwrap();
947        assert_eq!(t.hour, 14);
948        assert_eq!(t.minute, 0);
949        assert_eq!(t.second, 0);
950        assert_eq!(t.fraction, 0);
951    }
952
953    #[test]
954    fn time_partial_hour_minute() {
955        let t = DicomTime::parse("1430").unwrap();
956        assert_eq!(t.hour, 14);
957        assert_eq!(t.minute, 30);
958        assert_eq!(t.second, 0);
959    }
960
961    #[test]
962    fn time_partial_no_fraction() {
963        let t = DicomTime::parse("143022").unwrap();
964        assert_eq!(t.hour, 14);
965        assert_eq!(t.minute, 30);
966        assert_eq!(t.second, 22);
967        assert_eq!(t.fraction, 0);
968    }
969
970    #[test]
971    fn time_fraction_short() {
972        // Short fraction is zero-padded on the right
973        let t = DicomTime::parse("143022.5").unwrap();
974        assert_eq!(t.fraction, 500000);
975    }
976
977    #[test]
978    fn time_display() {
979        let t = DicomTime {
980            hour: 14,
981            minute: 30,
982            second: 22,
983            fraction: 500000,
984        };
985        assert_eq!(t.to_string(), "143022.500000");
986    }
987
988    #[test]
989    fn time_display_no_fraction() {
990        let t = DicomTime {
991            hour: 14,
992            minute: 30,
993            second: 22,
994            fraction: 0,
995        };
996        assert_eq!(t.to_string(), "143022");
997    }
998
999    // ── DicomDateTime ───────────────────────────────────────────────────
1000
1001    #[test]
1002    fn datetime_full_parse() {
1003        let dt = DicomDateTime::parse("20231215143022.000000+0530").unwrap();
1004        assert_eq!(dt.date.year, 2023);
1005        assert_eq!(dt.date.month, 12);
1006        assert_eq!(dt.date.day, 15);
1007        let t = dt.time.unwrap();
1008        assert_eq!(t.hour, 14);
1009        assert_eq!(t.minute, 30);
1010        assert_eq!(t.second, 22);
1011        assert_eq!(dt.offset_minutes, Some(330)); // +05:30 = 5*60+30 = 330
1012    }
1013
1014    #[test]
1015    fn datetime_negative_offset() {
1016        let dt = DicomDateTime::parse("20231215143022.000000-0500").unwrap();
1017        assert_eq!(dt.offset_minutes, Some(-300));
1018    }
1019
1020    #[test]
1021    fn datetime_no_time() {
1022        let dt = DicomDateTime::parse("20231215").unwrap();
1023        assert_eq!(dt.date.year, 2023);
1024        assert!(dt.time.is_none());
1025        assert!(dt.offset_minutes.is_none());
1026    }
1027
1028    #[test]
1029    fn datetime_display_roundtrip() {
1030        // Use non-zero fraction so Display includes it, enabling exact round-trip.
1031        let s = "20231215143022.500000+0530";
1032        let dt = DicomDateTime::parse(s).unwrap();
1033        assert_eq!(dt.to_string(), s);
1034    }
1035
1036    #[test]
1037    fn datetime_display_roundtrip_no_fraction() {
1038        // Without a fractional second the display omits the decimal.
1039        let s = "20231215143022+0530";
1040        let dt = DicomDateTime::parse(s).unwrap();
1041        assert_eq!(dt.to_string(), s);
1042    }
1043
1044    // ── PersonName ──────────────────────────────────────────────────────
1045
1046    #[test]
1047    fn pn_simple() {
1048        let pn = PersonName::parse("Eichelberg^Marco^^Dr.");
1049        assert_eq!(pn.last_name(), "Eichelberg");
1050        assert_eq!(pn.first_name(), "Marco");
1051        assert_eq!(pn.middle_name(), "");
1052        assert_eq!(pn.prefix(), "Dr.");
1053        assert_eq!(pn.suffix(), "");
1054    }
1055
1056    #[test]
1057    fn pn_multi_component() {
1058        let pn = PersonName::parse("Smith^John=\u{5C71}\u{7530}^\u{592A}\u{90CE}=\u{3084}\u{307E}\u{3060}^\u{305F}\u{308D}\u{3046}");
1059        assert_eq!(pn.last_name(), "Smith");
1060        assert_eq!(pn.first_name(), "John");
1061        assert!(!pn.ideographic.is_empty());
1062        assert!(!pn.phonetic.is_empty());
1063    }
1064
1065    #[test]
1066    fn pn_display_single_group() {
1067        let pn = PersonName::parse("Smith^John");
1068        assert_eq!(pn.to_string(), "Smith^John");
1069    }
1070
1071    #[test]
1072    fn pn_display_two_groups() {
1073        let pn = PersonName::parse("Smith^John=SJ");
1074        assert_eq!(pn.to_string(), "Smith^John=SJ");
1075    }
1076
1077    // ── Value ───────────────────────────────────────────────────────────
1078
1079    #[test]
1080    fn value_multiplicity() {
1081        assert_eq!(Value::Empty.multiplicity(), 0);
1082        assert_eq!(
1083            Value::Strings(vec!["a".into(), "b".into()]).multiplicity(),
1084            2
1085        );
1086        assert_eq!(Value::U16(vec![1, 2, 3]).multiplicity(), 3);
1087        assert_eq!(Value::Uid("1.2.3".into()).multiplicity(), 1);
1088        assert_eq!(Value::Sequence(vec![]).multiplicity(), 0);
1089    }
1090
1091    #[test]
1092    fn value_is_empty() {
1093        assert!(Value::Empty.is_empty());
1094        assert!(Value::Strings(vec![]).is_empty());
1095        assert!(!Value::Strings(vec!["x".into()]).is_empty());
1096    }
1097
1098    #[test]
1099    fn value_as_string() {
1100        let v = Value::Strings(vec!["hello".into(), "world".into()]);
1101        assert_eq!(v.as_string(), Some("hello"));
1102        assert_eq!(v.as_strings().unwrap().len(), 2);
1103    }
1104
1105    #[test]
1106    fn value_as_uid() {
1107        let v = Value::Uid("1.2.840.10008.1.1".into());
1108        assert_eq!(v.as_string(), Some("1.2.840.10008.1.1"));
1109    }
1110
1111    #[test]
1112    fn value_as_numeric() {
1113        let v = Value::U16(vec![512]);
1114        assert_eq!(v.as_u16(), Some(512));
1115
1116        let v = Value::U32(vec![65536]);
1117        assert_eq!(v.as_u32(), Some(65536));
1118
1119        let v = Value::I32(vec![-1]);
1120        assert_eq!(v.as_i32(), Some(-1));
1121
1122        let v = Value::F64(vec![2.78]);
1123        assert_eq!(v.as_f64(), Some(2.78));
1124    }
1125
1126    #[test]
1127    fn value_to_display_string_strings() {
1128        let v = Value::Strings(vec!["foo".into(), "bar".into()]);
1129        assert_eq!(v.to_display_string(), "foo\\bar");
1130    }
1131
1132    #[test]
1133    fn value_to_display_string_u16() {
1134        let v = Value::U16(vec![512, 256]);
1135        assert_eq!(v.to_display_string(), "512\\256");
1136    }
1137
1138    #[test]
1139    fn value_to_display_string_sequence() {
1140        let v = Value::Sequence(vec![]);
1141        assert_eq!(v.to_display_string(), "(Sequence with 0 item(s))");
1142    }
1143
1144    #[test]
1145    fn value_as_bytes() {
1146        let v = Value::U8(vec![1, 2, 3]);
1147        assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
1148    }
1149
1150    #[test]
1151    fn encapsulated_frames_single_frame_concatenates_fragments() {
1152        let pixel_data = PixelData::Encapsulated {
1153            offset_table: vec![0],
1154            fragments: vec![vec![1, 2], vec![3, 4]],
1155        };
1156
1157        let frames = encapsulated_frames(&pixel_data, 1).unwrap();
1158        assert_eq!(frames, vec![vec![1, 2, 3, 4]]);
1159    }
1160
1161    #[test]
1162    fn encapsulated_frames_handles_empty_bot_one_fragment_per_frame() {
1163        let pixel_data = PixelData::Encapsulated {
1164            offset_table: vec![],
1165            fragments: vec![vec![1, 2], vec![3, 4]],
1166        };
1167
1168        let frames = encapsulated_frames(&pixel_data, 2).unwrap();
1169        assert_eq!(frames, vec![vec![1, 2], vec![3, 4]]);
1170    }
1171
1172    #[test]
1173    fn encapsulated_frames_uses_basic_offset_table_for_multi_fragment_frames() {
1174        let pixel_data = PixelData::Encapsulated {
1175            offset_table: vec![0, 22],
1176            fragments: vec![vec![1, 2], vec![3, 4, 5, 6], vec![7, 8, 9]],
1177        };
1178
1179        let frames = encapsulated_frames(&pixel_data, 2).unwrap();
1180        assert_eq!(frames, vec![vec![1, 2, 3, 4, 5, 6], vec![7, 8, 9]]);
1181    }
1182
1183    #[test]
1184    fn encapsulated_frames_rejects_malformed_offset_table() {
1185        let pixel_data = PixelData::Encapsulated {
1186            offset_table: vec![0, 99],
1187            fragments: vec![vec![1, 2], vec![3, 4]],
1188        };
1189
1190        let err = encapsulated_frames(&pixel_data, 2).unwrap_err();
1191        assert!(err.to_string().contains("does not align"));
1192    }
1193
1194    #[test]
1195    fn build_encapsulated_pixel_data_uses_fragment_item_boundaries() {
1196        let pixel_data = encapsulated_pixel_data_from_frames(&[vec![1, 2, 3], vec![4, 5]]).unwrap();
1197
1198        match pixel_data {
1199            PixelData::Encapsulated {
1200                offset_table,
1201                fragments,
1202            } => {
1203                assert_eq!(offset_table, vec![0, 11]);
1204                assert_eq!(fragments, vec![vec![1, 2, 3], vec![4, 5]]);
1205            }
1206            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
1207        }
1208    }
1209
1210    #[test]
1211    fn build_encapsulated_pixel_data_handles_multi_fragment_frames() {
1212        let pixel_data = build_encapsulated_pixel_data(&[
1213            EncapsulatedFrame {
1214                fragments: vec![vec![1, 2], vec![3, 4, 5, 6]],
1215            },
1216            EncapsulatedFrame {
1217                fragments: vec![vec![7, 8, 9]],
1218            },
1219        ])
1220        .unwrap();
1221
1222        match &pixel_data {
1223            PixelData::Encapsulated { offset_table, .. } => {
1224                assert_eq!(offset_table, &vec![0, 22]);
1225            }
1226            PixelData::Native { .. } => panic!("expected encapsulated pixel data"),
1227        }
1228
1229        let frames = encapsulated_frames(&pixel_data, 2).unwrap();
1230        assert_eq!(frames, vec![vec![1, 2, 3, 4, 5, 6], vec![7, 8, 9]]);
1231    }
1232
1233    #[test]
1234    fn build_encapsulated_pixel_data_rejects_empty_frames() {
1235        assert!(build_encapsulated_pixel_data(&[]).is_err());
1236        assert!(build_encapsulated_pixel_data(&[EncapsulatedFrame { fragments: vec![] }]).is_err());
1237    }
1238}