1use crate::dataset::DataSet;
7use dicom_toolkit_core::error::{DcmError, DcmResult};
8use dicom_toolkit_dict::Tag;
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub struct DicomDate {
18 pub year: u16,
19 pub month: u8,
21 pub day: u8,
23}
24
25impl DicomDate {
26 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct DicomTime {
91 pub hour: u8,
92 pub minute: u8,
93 pub second: u8,
94 pub fraction: u32,
96}
97
98impl DicomTime {
99 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 let (time_part, fraction) = if let Some(dot_pos) = s.find('.') {
108 let frac_str = &s[dot_pos + 1..];
109 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#[derive(Debug, Clone, PartialEq)]
200pub struct DicomDateTime {
201 pub date: DicomDate,
202 pub time: Option<DicomTime>,
203 pub offset_minutes: Option<i16>,
205}
206
207impl DicomDateTime {
208 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 let (dt_part, offset_minutes) = extract_tz_offset(s)?;
218
219 let date_len = dt_part.len().min(8);
221 let date_str = &dt_part[..date_len];
222 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
239fn extract_tz_offset(s: &str) -> DcmResult<(&str, Option<i16>)> {
241 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#[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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct EncapsulatedFrame {
346 pub fragments: Vec<Vec<u8>>,
347}
348
349#[derive(Debug, Clone, PartialEq)]
352pub enum PixelData {
353 Native { bytes: Vec<u8> },
355 Encapsulated {
357 offset_table: Vec<u32>,
358 fragments: Vec<Vec<u8>>,
359 },
360}
361
362impl PixelData {
363 pub fn encapsulated_frames(&self, number_of_frames: u32) -> DcmResult<Vec<Vec<u8>>> {
365 encapsulated_frames(self, number_of_frames)
366 }
367}
368
369pub 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
409pub 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
421pub 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#[derive(Debug, Clone, PartialEq)]
539pub enum Value {
540 Empty,
542 Strings(Vec<String>),
544 PersonNames(Vec<PersonName>),
546 Uid(String),
548 Date(Vec<DicomDate>),
550 Time(Vec<DicomTime>),
552 DateTime(Vec<DicomDateTime>),
554 Ints(Vec<i64>),
556 Decimals(Vec<f64>),
558 U8(Vec<u8>),
560 U16(Vec<u16>),
562 I16(Vec<i16>),
564 U32(Vec<u32>),
566 I32(Vec<i32>),
568 U64(Vec<u64>),
570 I64(Vec<i64>),
572 F32(Vec<f32>),
574 F64(Vec<f64>),
576 Tags(Vec<Tag>),
578 Sequence(Vec<DataSet>),
580 PixelData(PixelData),
582}
583
584impl Value {
585 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 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 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 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
803fn 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
847fn 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#[cfg(test)]
859mod tests {
860 use super::*;
861
862 #[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 #[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 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 #[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)); }
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 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 let s = "20231215143022+0530";
1040 let dt = DicomDateTime::parse(s).unwrap();
1041 assert_eq!(dt.to_string(), s);
1042 }
1043
1044 #[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 #[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}