1#![allow(clippy::derive_partial_eq_without_eq)]
2#[cfg(feature = "cli")]
36use clap::ValueEnum;
37#[cfg(feature = "sop-class")]
38use dicom_core::dictionary::UidDictionary;
39use dicom_core::dictionary::{DataDictionary, DataDictionaryEntry};
40use dicom_core::header::Header;
41use dicom_core::value::{PrimitiveValue, Value as DicomValue};
42use dicom_core::VR;
43#[cfg(feature = "sop-class")]
44use dicom_dictionary_std::StandardSopClassDictionary;
45use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
46use dicom_json::DicomJson;
47use dicom_object::mem::{InMemDicomObject, InMemElement};
48use dicom_object::{FileDicomObject, FileMetaTable, StandardDataDictionary};
49use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
50use owo_colors::*;
51use std::borrow::Cow;
52use std::fmt::{self, Display, Formatter};
53use std::io::{stdout, Result as IoResult, Write};
54use std::str::FromStr;
55
56#[derive(Clone, Debug, PartialEq, Default)]
57#[cfg_attr(feature = "cli", derive(ValueEnum))]
58pub enum DumpFormat {
59 #[default]
70 Text,
71 Json,
74}
75
76#[derive(Debug, Default, Clone, PartialEq)]
114#[non_exhaustive]
115pub struct DumpOptions {
116 pub format: DumpFormat,
118 pub color: ColorMode,
120 pub width: Option<u32>,
122 pub no_text_limit: bool,
124 pub no_limit: bool,
126}
127
128impl DumpOptions {
129 pub fn new() -> Self {
130 Default::default()
131 }
132
133 pub fn format(&mut self, format: DumpFormat) -> &mut Self {
137 self.format = format;
138 self
139 }
140
141 pub fn width(&mut self, width: u32) -> &mut Self {
147 self.width = Some(width);
148 self
149 }
150
151 pub fn width_auto(&mut self) -> &mut Self {
162 self.width = None;
163 self
164 }
165
166 pub fn no_text_limit(&mut self, no_text_limit: bool) -> &mut Self {
168 self.no_text_limit = no_text_limit;
169 self
170 }
171
172 pub fn no_limit(&mut self, no_limit: bool) -> &mut Self {
175 self.no_limit = no_limit;
176 self
177 }
178
179 pub fn color_mode(&mut self, color: ColorMode) -> &mut Self {
181 self.color = color;
182 self
183 }
184
185 pub fn dump_file<D>(&self, obj: &FileDicomObject<InMemDicomObject<D>>) -> IoResult<()>
187 where
188 D: DataDictionary,
189 {
190 self.dump_file_impl(stdout(), obj, true)
191 }
192
193 pub fn dump_file_to<D>(
195 &self,
196 to: impl Write,
197 obj: &FileDicomObject<InMemDicomObject<D>>,
198 ) -> IoResult<()>
199 where
200 D: DataDictionary,
201 {
202 self.dump_file_impl(to, obj, false)
203 }
204
205 fn dump_file_impl<D>(
206 &self,
207 mut to: impl Write,
208 obj: &FileDicomObject<InMemDicomObject<D>>,
209 to_stdout: bool,
210 ) -> IoResult<()>
211 where
212 D: DataDictionary,
213 {
214 match self.color {
215 ColorMode::Never => owo_colors::set_override(false),
216 ColorMode::Always => owo_colors::set_override(true),
217 ColorMode::Auto => owo_colors::unset_override(),
218 }
219
220 let meta = obj.meta();
221
222 let width = determine_width(self.width);
223
224 let (no_text_limit, no_limit) = if to_stdout {
225 (self.no_text_limit, self.no_limit)
226 } else {
227 (true, true)
228 };
229 match self.format {
230 DumpFormat::Text => {
231 meta_dump(&mut to, meta, if no_limit { u32::MAX } else { width })?;
232
233 writeln!(to, "{:-<58}", "")?;
234
235 dump(&mut to, obj, width, 0, no_text_limit, no_limit)?;
236
237 Ok(())
238 }
239 DumpFormat::Json => {
240 let json_obj = DicomJson::from(obj);
241 serde_json::to_writer_pretty(stdout(), &json_obj)?;
242 Ok(())
243 }
244 }
245 }
246
247 #[inline]
249 pub fn dump_object<D>(&self, obj: &InMemDicomObject<D>) -> IoResult<()>
250 where
251 D: DataDictionary,
252 {
253 self.dump_object_impl(stdout(), obj, true)
254 }
255
256 #[inline]
258 pub fn dump_object_to<D>(&self, to: impl Write, obj: &InMemDicomObject<D>) -> IoResult<()>
259 where
260 D: DataDictionary,
261 {
262 self.dump_object_impl(to, obj, false)
263 }
264
265 fn dump_object_impl<D>(
266 &self,
267 mut to: impl Write,
268 obj: &InMemDicomObject<D>,
269 to_stdout: bool,
270 ) -> IoResult<()>
271 where
272 D: DataDictionary,
273 {
274 match self.format {
275 DumpFormat::Text => {
276 match (self.color, to_stdout) {
277 (ColorMode::Never, _) => colored::control::set_override(false),
278 (ColorMode::Always, _) => colored::control::set_override(true),
279 (ColorMode::Auto, false) => colored::control::set_override(false),
280 (ColorMode::Auto, true) => colored::control::unset_override(),
281 }
282
283 let width = determine_width(self.width);
284
285 let (no_text_limit, no_limit) = if to_stdout {
286 (self.no_text_limit, self.no_limit)
287 } else {
288 (true, true)
289 };
290
291 dump(&mut to, obj, width, 0, no_text_limit, no_limit)?;
292
293 Ok(())
294 }
295 DumpFormat::Json => {
296 let json_obj = DicomJson::from(obj);
297 serde_json::to_writer_pretty(to, &json_obj)?;
298 Ok(())
299 }
300 }
301 }
302}
303
304#[derive(Debug, Default, Copy, Clone, Eq, Hash, PartialEq)]
306pub enum ColorMode {
307 #[default]
315 Auto,
316 Never,
318 Always,
320}
321
322impl std::fmt::Display for ColorMode {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 match self {
325 ColorMode::Never => f.write_str("never"),
326 ColorMode::Auto => f.write_str("auto"),
327 ColorMode::Always => f.write_str("always"),
328 }
329 }
330}
331
332impl FromStr for ColorMode {
333 type Err = ColorModeError;
334 fn from_str(color: &str) -> Result<Self, Self::Err> {
335 match color {
336 "never" => Ok(ColorMode::Never),
337 "auto" => Ok(ColorMode::Auto),
338 "always" => Ok(ColorMode::Always),
339 _ => Err(ColorModeError),
340 }
341 }
342}
343
344#[derive(Debug, Default, Copy, Clone, Eq, Hash, PartialEq)]
346pub struct ColorModeError;
347
348impl Display for ColorModeError {
349 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
350 f.write_str("invalid color mode")
351 }
352}
353
354impl std::error::Error for ColorModeError {}
355
356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357enum DumpValue<T>
358where
359 T: ToString,
360{
361 TagNum(T),
362 Alias(T),
363 Num(T),
364 Str(T),
365 DateTime(T),
366 Invalid(T),
367 Nothing,
368}
369
370impl<T> fmt::Display for DumpValue<T>
371where
372 T: fmt::Display,
373{
374 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
375 fn write_value_with_width(value: impl fmt::Display, f: &mut fmt::Formatter) -> fmt::Result {
376 if let Some(width) = f.width() {
377 write!(f, "{value:width$}")
378 } else {
379 write!(f, "{value}")
380 }
381 }
382
383 match self {
384 DumpValue::TagNum(v) => {
385 let value = v.if_supports_color(Stream::Stdout, |v| v.dimmed());
386 write_value_with_width(value, f)
387 }
388 DumpValue::Alias(v) => {
389 let value = v.if_supports_color(Stream::Stdout, |v| v.bold());
390 write_value_with_width(value, f)
391 }
392 DumpValue::Num(v) => {
393 let value = v.if_supports_color(Stream::Stdout, |v| v.cyan());
394 write_value_with_width(value, f)
395 }
396 DumpValue::Str(v) => {
397 let value = v.if_supports_color(Stream::Stdout, |v| v.yellow());
398 write_value_with_width(value, f)
399 }
400 DumpValue::DateTime(v) => {
401 let value = v.if_supports_color(Stream::Stdout, |v| v.green());
402 write_value_with_width(value, f)
403 }
404 DumpValue::Invalid(v) => {
405 let value = v.if_supports_color(Stream::Stdout, |v| v.red());
406 write_value_with_width(value, f)
407 }
408 DumpValue::Nothing => {
409 let value = "(no value)".if_supports_color(Stream::Stdout, |v| v.italic());
410 write_value_with_width(value, f)
411 }
412 }
413 }
414}
415
416pub fn dump_file<D>(obj: &FileDicomObject<InMemDicomObject<D>>) -> IoResult<()>
420where
421 D: DataDictionary,
422{
423 DumpOptions::new().dump_file(obj)
424}
425
426pub fn dump_file_to<D>(to: impl Write, obj: &FileDicomObject<InMemDicomObject<D>>) -> IoResult<()>
430where
431 D: DataDictionary,
432{
433 DumpOptions::new().dump_file_to(to, obj)
434}
435
436pub fn dump_object<D>(obj: &InMemDicomObject<D>) -> IoResult<()>
438where
439 D: DataDictionary,
440{
441 DumpOptions::new().dump_object(obj)
442}
443
444pub fn dump_object_to<D>(to: impl Write, obj: &InMemDicomObject<D>) -> IoResult<()>
446where
447 D: DataDictionary,
448{
449 DumpOptions::new().dump_object_to(to, obj)
450}
451
452#[inline]
453fn whitespace_or_null(c: char) -> bool {
454 c.is_whitespace() || c == '\0'
455}
456
457fn meta_dump<W>(to: &mut W, meta: &FileMetaTable, width: u32) -> IoResult<()>
458where
459 W: ?Sized + Write,
460{
461 let sop_class_uid = meta
462 .media_storage_sop_class_uid
463 .trim_end_matches(whitespace_or_null);
464
465 #[cfg(feature = "sop-class")]
466 #[inline]
467 fn translate_sop_class(uid: &str) -> Option<&'static str> {
468 StandardSopClassDictionary.by_uid(uid).map(|e| e.name)
469 }
470 #[cfg(not(feature = "sop-class"))]
471 #[inline]
472 fn translate_sop_class(_uid: &str) -> Option<&'static str> {
473 None
474 }
475
476 if let Some(name) = translate_sop_class(sop_class_uid) {
477 writeln!(
478 to,
479 "{}: {} ({})",
480 "Media Storage SOP Class UID".if_supports_color(Stream::Stdout, |v| v.bold()),
481 sop_class_uid,
482 name,
483 )?;
484 } else {
485 writeln!(
486 to,
487 "{}: {}",
488 "Media Storage SOP Class UID".if_supports_color(Stream::Stdout, |v| v.bold()),
489 sop_class_uid,
490 )?;
491 }
492 writeln!(
493 to,
494 "{}: {}",
495 "Media Storage SOP Instance UID".if_supports_color(Stream::Stdout, |v| v.bold()),
496 meta.media_storage_sop_instance_uid
497 .trim_end_matches(whitespace_or_null),
498 )?;
499 if let Some(ts) = TransferSyntaxRegistry.get(&meta.transfer_syntax) {
500 writeln!(
501 to,
502 "{}: {} ({})",
503 "Transfer Syntax".if_supports_color(Stream::Stdout, |v| v.bold()),
504 ts.uid(),
505 ts.name()
506 )?;
507 } else {
508 writeln!(
509 to,
510 "{}: {} («UNKNOWN»)",
511 "Transfer Syntax".if_supports_color(Stream::Stdout, |v| v.bold()),
512 meta.transfer_syntax.trim_end_matches(whitespace_or_null)
513 )?;
514 }
515 writeln!(
516 to,
517 "{}: {}",
518 "Implementation Class UID".if_supports_color(Stream::Stdout, |v| v.bold()),
519 meta.implementation_class_uid
520 .trim_end_matches(whitespace_or_null),
521 )?;
522
523 if let Some(v) = meta.implementation_version_name.as_ref() {
524 writeln!(
525 to,
526 "{}: {}",
527 "Implementation version name".if_supports_color(Stream::Stdout, |v| v.bold()),
528 v.trim_end()
529 )?;
530 }
531
532 if let Some(v) = meta.source_application_entity_title.as_ref() {
533 writeln!(
534 to,
535 "{}: {}",
536 "Source Application Entity Title".if_supports_color(Stream::Stdout, |v| v.bold()),
537 v.trim_end()
538 )?;
539 }
540
541 if let Some(v) = meta.sending_application_entity_title.as_ref() {
542 writeln!(
543 to,
544 "{}: {}",
545 "Sending Application Entity Title".if_supports_color(Stream::Stdout, |v| v.bold()),
546 v.trim_end()
547 )?;
548 }
549
550 if let Some(v) = meta.receiving_application_entity_title.as_ref() {
551 writeln!(
552 to,
553 "{}: {}",
554 "Receiving Application Entity Title".if_supports_color(Stream::Stdout, |v| v.bold()),
555 v.trim_end()
556 )?;
557 }
558
559 if let Some(v) = meta.private_information_creator_uid.as_ref() {
560 writeln!(
561 to,
562 "{}: {}",
563 "Private Information Creator UID".if_supports_color(Stream::Stdout, |v| v.bold()),
564 v.trim_end_matches(whitespace_or_null)
565 )?;
566 }
567
568 if let Some(v) = meta.private_information.as_ref() {
569 writeln!(
570 to,
571 "{}: {}",
572 "Private Information".if_supports_color(Stream::Stdout, |v| v.bold()),
573 format_value_list(v.iter().map(|n| format!("{n:02X}")), Some(width), false)
574 )?;
575 }
576
577 writeln!(to)?;
578 Ok(())
579}
580
581fn dump<W, D>(
582 to: &mut W,
583 obj: &InMemDicomObject<D>,
584 width: u32,
585 depth: u32,
586 no_text_limit: bool,
587 no_limit: bool,
588) -> IoResult<()>
589where
590 W: ?Sized + Write,
591 D: DataDictionary,
592{
593 for elem in obj {
594 dump_element(&mut *to, elem, width, depth, no_text_limit, no_limit)?;
595 }
596
597 Ok(())
598}
599
600pub fn dump_element<W, D>(
601 to: &mut W,
602 elem: &InMemElement<D>,
603 width: u32,
604 depth: u32,
605 no_text_limit: bool,
606 no_limit: bool,
607) -> IoResult<()>
608where
609 W: ?Sized + Write,
610 D: DataDictionary,
611{
612 let indent = vec![b' '; (depth * 2) as usize];
613 let tag_alias = StandardDataDictionary
614 .by_tag(elem.tag())
615 .map(DataDictionaryEntry::alias)
616 .unwrap_or("«Unknown Attribute»");
617 to.write_all(&indent)?;
618 let vm = match elem.vr() {
619 VR::OB | VR::OW | VR::UN => 1,
620 _ => elem.value().multiplicity(),
621 };
622
623 match elem.value() {
624 DicomValue::Sequence(seq) => {
625 writeln!(
626 to,
627 "{} {:28} {} ({} Item{})",
628 DumpValue::TagNum(elem.tag()),
629 DumpValue::Alias(tag_alias),
630 elem.vr(),
631 vm,
632 if vm == 1 { "" } else { "s" },
633 )?;
634 for item in seq.items() {
635 dump_item(&mut *to, item, width, depth + 2, no_text_limit, no_limit)?;
636 }
637 to.write_all(&indent)?;
638 writeln!(
639 to,
640 "{} {}",
641 DumpValue::TagNum("(FFFE,E0DD)"),
642 DumpValue::Alias("SequenceDelimitationItem"),
643 )?;
644 }
645 DicomValue::PixelSequence(seq) => {
646 let vr = elem.vr();
648 let num_items = 1 + seq.fragments().len();
649 writeln!(
650 to,
651 "{} {:28} {} (PixelSequence, {} Item{})",
652 DumpValue::TagNum(elem.tag()),
653 "PixelData".bold(),
654 vr,
655 num_items,
656 if num_items == 1 { "" } else { "s" },
657 )?;
658
659 let offset_table = seq.offset_table();
661 let byte_len = offset_table.len() * 4;
662 let summary = offset_table_summary(
663 offset_table,
664 Some(width)
665 .filter(|_| !no_limit)
666 .map(|w| w.saturating_sub(38 + depth * 2)),
667 );
668 writeln!(
669 to,
670 " {} offset table ({:>2}, {:>2} bytes): {}",
671 DumpValue::TagNum("(FFFE,E000)"),
672 offset_table.len(),
673 byte_len,
674 summary,
675 )?;
676
677 for fragment in seq.fragments() {
679 let byte_len = fragment.len();
680 let summary = item_value_summary(
681 fragment,
682 Some(width)
683 .filter(|_| !no_limit)
684 .map(|w| w.saturating_sub(38 + depth * 2)),
685 );
686 writeln!(
687 to,
688 " {} pi ({:>3} bytes): {}",
689 DumpValue::TagNum("(FFFE,E000)"),
690 byte_len,
691 summary
692 )?;
693 }
694 }
695 DicomValue::Primitive(value) => {
696 let vr = elem.vr();
697 let byte_len = elem.header().len.0;
698 writeln!(
699 to,
700 "{} {:28} {} ({},{:>3} bytes): {}",
701 DumpValue::TagNum(elem.tag()),
702 DumpValue::Alias(tag_alias),
703 vr,
704 vm,
705 byte_len,
706 value_summary(
707 value,
708 vr,
709 width.saturating_sub(63 + depth * 2),
710 no_text_limit,
711 no_limit,
712 ),
713 )?;
714 }
715 }
716
717 Ok(())
718}
719
720fn dump_item<W, D>(
721 to: &mut W,
722 item: &InMemDicomObject<D>,
723 width: u32,
724 depth: u32,
725 no_text_limit: bool,
726 no_limit: bool,
727) -> IoResult<()>
728where
729 W: ?Sized + Write,
730 D: DataDictionary,
731{
732 let indent: String = " ".repeat(depth as usize);
733 writeln!(
734 to,
735 "{}{} na {}",
736 indent,
737 DumpValue::TagNum("(FFFE,E000)"),
738 DumpValue::Alias("Item"),
739 )?;
740 dump(to, item, width, depth + 1, no_text_limit, no_limit)?;
741 writeln!(
742 to,
743 "{}{} {}",
744 indent,
745 DumpValue::TagNum("(FFFE,E00D)"),
746 DumpValue::Alias("ItemDelimitationItem"),
747 )?;
748 Ok(())
749}
750
751fn value_summary(
752 value: &PrimitiveValue,
753 vr: VR,
754 max_characters: u32,
755 no_text_limit: bool,
756 no_limit: bool,
757) -> DumpValue<String> {
758 use PrimitiveValue::*;
759
760 let max_characters = match (no_limit, no_text_limit, vr) {
761 (true, _, _) => None,
762 (
763 false,
764 true,
765 VR::CS
766 | VR::AE
767 | VR::AS
768 | VR::DA
769 | VR::DS
770 | VR::DT
771 | VR::IS
772 | VR::LO
773 | VR::LT
774 | VR::PN
775 | VR::SH
776 | VR::ST
777 | VR::TM
778 | VR::UC
779 | VR::UI
780 | VR::UR
781 | VR::UT,
782 ) => None,
783 (false, _, _) => Some(max_characters),
784 };
785 match (value, vr) {
786 (F32(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
787 (F64(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
788 (I32(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
789 (I64(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
790 (U32(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
791 (U64(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
792 (I16(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
793 (U16(values), VR::OW) => DumpValue::Num(format_value_list(
794 values.into_iter().map(|n| format!("{n:02X}")),
795 max_characters,
796 false,
797 )),
798 (U16(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
799 (U8(values), VR::OB) | (U8(values), VR::UN) => DumpValue::Num(format_value_list(
800 values.into_iter().map(|n| format!("{n:02X}")),
801 max_characters,
802 false,
803 )),
804 (U8(values), _) => DumpValue::Num(format_value_list(values, max_characters, false)),
805 (Tags(values), _) => DumpValue::Str(format_value_list(values, max_characters, false)),
806 (Strs(values), VR::DA) => {
807 match value.to_multi_date() {
808 Ok(values) => {
809 DumpValue::DateTime(format_value_list(values, max_characters, false))
811 }
812 Err(_e) => {
813 DumpValue::Invalid(format_value_list(values, max_characters, true))
815 }
816 }
817 }
818 (Strs(values), VR::TM) => {
819 match value.to_multi_time() {
820 Ok(values) => {
821 DumpValue::DateTime(format_value_list(values, max_characters, false))
823 }
824 Err(_e) => {
825 DumpValue::Invalid(format_value_list(values, max_characters, true))
827 }
828 }
829 }
830 (Strs(values), VR::DT) => {
831 match value.to_multi_datetime() {
832 Ok(values) => {
833 DumpValue::DateTime(format_value_list(values, max_characters, false))
835 }
836 Err(_e) => {
837 DumpValue::Invalid(format_value_list(values, max_characters, true))
839 }
840 }
841 }
842 (Strs(values), _) => DumpValue::Str(format_value_list(
843 values
844 .iter()
845 .map(|s| s.trim_end_matches(whitespace_or_null)),
846 max_characters,
847 true,
848 )),
849 (Date(values), _) => DumpValue::DateTime(format_value_list(values, max_characters, false)),
850 (Time(values), _) => DumpValue::DateTime(format_value_list(values, max_characters, false)),
851 (DateTime(values), _) => {
852 DumpValue::DateTime(format_value_list(values, max_characters, false))
853 }
854 (Str(value), _) => {
855 let txt = format!(
856 "\"{}\"",
857 value
858 .to_string()
859 .trim_end_matches(whitespace_or_null)
860 .replace('\n', "␊")
862 .replace('\r', "␍")
863 .replace('\0', "␀")
864 .replace(|c: char| c.is_control(), "�")
865 );
866 if let Some(max) = max_characters {
867 DumpValue::Str(cut_str(&txt, max).to_string())
868 } else {
869 DumpValue::Str(txt)
870 }
871 }
872 (Empty, _) => DumpValue::Nothing,
873 }
874}
875
876fn item_value_summary(data: &[u8], max_characters: Option<u32>) -> DumpValue<String> {
877 DumpValue::Num(format_value_list(
878 data.iter().map(|n| format!("{n:02X}")),
879 max_characters,
880 false,
881 ))
882}
883
884fn offset_table_summary(data: &[u32], max_characters: Option<u32>) -> String {
885 if data.is_empty() {
886 format!("{}", "(empty)".italic())
887 } else {
888 format_value_list(
889 data.iter().map(|n| format!("{n:04X}")),
890 max_characters,
891 false,
892 )
893 }
894}
895
896fn format_value_list<I>(values: I, max_characters: Option<u32>, quoted: bool) -> String
897where
898 I: IntoIterator,
899 I::IntoIter: ExactSizeIterator,
900 I::Item: std::fmt::Display,
901{
902 let values = values.into_iter();
903 let len = values.len();
904 let mut acc_size = 0;
905 let mut pieces = String::new();
906 if len > 1 {
907 pieces.push('[');
908 }
909 for piece in values {
910 let mut piece = piece.to_string();
911 piece = piece
913 .replace('\n', "␊")
914 .replace('\r', "␍")
915 .replace('\0', "␀")
916 .replace(|c: char| c.is_control(), "�");
917
918 if acc_size > 0 {
919 pieces.push_str(", ");
920 }
921
922 if quoted {
923 piece = piece.replace('\"', "\\\"");
924 pieces.push('"');
925 }
926
927 acc_size += piece.len();
928 pieces.push_str(&piece);
929 if quoted {
930 pieces.push('"');
931 }
932 if max_characters
934 .filter(|max| (*max as usize) < acc_size)
935 .is_some()
936 {
937 break;
938 }
939 }
940 if len > 1 {
941 pieces.push(']');
942 }
943 if let Some(max_characters) = max_characters {
944 cut_str(&pieces, max_characters).into_owned()
945 } else {
946 pieces
947 }
948}
949
950fn cut_str(s: &str, max_characters: u32) -> Cow<'_, str> {
951 let max = (max_characters.saturating_sub(3)) as usize;
952 let len = s.chars().count();
953
954 if len > max {
955 s.chars()
956 .take(max)
957 .chain("...".chars())
958 .collect::<String>()
959 .into()
960 } else {
961 s.into()
962 }
963}
964
965fn determine_width(user_width: Option<u32>) -> u32 {
966 user_width
967 .or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as u32))
968 .unwrap_or(120)
969}
970
971#[cfg(test)]
972mod tests {
973
974 use dicom_core::{value::DicomDate, DataElement, PrimitiveValue, VR};
975 use dicom_dictionary_std::tags;
976 use dicom_object::{FileMetaTableBuilder, InMemDicomObject};
977
978 use super::whitespace_or_null;
979 use crate::{ColorMode, DumpOptions};
980
981 #[test]
982 fn trims_all_whitespace() {
983 assert_eq!(" ".trim_end_matches(whitespace_or_null), "");
984 assert_eq!("\0".trim_end_matches(whitespace_or_null), "");
985 assert_eq!("1.4.5.6\0".trim_end_matches(whitespace_or_null), "1.4.5.6");
986 assert_eq!("AETITLE ".trim_end_matches(whitespace_or_null), "AETITLE");
987 }
988
989 #[test]
990 fn dump_file_to_covers_properties() {
991 let obj = InMemDicomObject::from_element_iter(vec![DataElement::new(
993 tags::SOP_INSTANCE_UID,
994 VR::UI,
995 PrimitiveValue::from("1.2.888.123"),
996 )]);
997
998 let file = obj
999 .with_meta(
1000 FileMetaTableBuilder::new()
1001 .transfer_syntax("1.2.840.10008.1.2")
1003 .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.1"),
1005 )
1006 .unwrap();
1007
1008 let mut out = Vec::new();
1009 DumpOptions::new()
1010 .color_mode(ColorMode::Never)
1011 .dump_file_to(&mut out, &file)
1012 .unwrap();
1013
1014 let lines: Vec<_> = std::str::from_utf8(&out)
1015 .expect("output is not valid UTF-8")
1016 .split('\n')
1017 .collect();
1018 if cfg!(feature = "sop-class") {
1019 assert_eq!(
1020 lines[0],
1021 "Media Storage SOP Class UID: 1.2.840.10008.5.1.4.1.1.1 (Computed Radiography Image Storage)"
1022 );
1023 } else {
1024 assert_eq!(
1025 lines[0],
1026 "Media Storage SOP Class UID: 1.2.840.10008.5.1.4.1.1.1"
1027 );
1028 }
1029 assert_eq!(lines[1], "Media Storage SOP Instance UID: 1.2.888.123");
1030 assert_eq!(
1031 lines[2],
1032 "Transfer Syntax: 1.2.840.10008.1.2 (Implicit VR Little Endian)"
1033 );
1034 assert!(lines[3].starts_with("Implementation Class UID: "));
1035 assert!(lines[4].starts_with("Implementation version name: "));
1036 assert_eq!(lines[5], "");
1037 assert_eq!(
1038 lines[6],
1039 "----------------------------------------------------------"
1040 );
1041
1042 let parts: Vec<&str> = lines[7].split(" ").filter(|p| !p.is_empty()).collect();
1043 assert_eq!(&parts[..3], &["(0008,0018)", "SOPInstanceUID", "UI"]);
1044 }
1045
1046 #[test]
1047 fn dump_object_to_covers_properties() {
1048 let obj = InMemDicomObject::from_element_iter([
1050 DataElement::new(
1051 tags::SOP_INSTANCE_UID,
1052 VR::UI,
1053 PrimitiveValue::from("1.2.888.123"),
1054 ),
1055 DataElement::new(
1056 tags::STUDY_DATE,
1057 VR::DA,
1058 PrimitiveValue::from(DicomDate::from_ymd(2017, 1, 1).unwrap()),
1059 ),
1060 DataElement::new(tags::CONTENT_DATE, VR::DA, PrimitiveValue::Empty),
1061 DataElement::new(tags::MODALITY, VR::CS, PrimitiveValue::from("OT")),
1062 DataElement::new(
1063 tags::INSTITUTION_NAME,
1064 VR::LO,
1065 PrimitiveValue::from("Hospital"),
1066 ),
1067 DataElement::new(
1068 tags::INSTITUTION_ADDRESS,
1069 VR::ST,
1070 PrimitiveValue::from("Country Roads 1\nWest Virginia"),
1071 ),
1072 DataElement::new(tags::SAMPLES_PER_PIXEL, VR::US, PrimitiveValue::from(3_u16)),
1073 ]);
1074
1075 let mut out = Vec::new();
1076 DumpOptions::new()
1077 .color_mode(ColorMode::Never)
1078 .dump_object_to(&mut out, &obj)
1079 .unwrap();
1080
1081 let lines: Vec<_> = std::str::from_utf8(&out)
1082 .expect("output is not valid UTF-8")
1083 .split('\n')
1084 .collect();
1085
1086 check_line(
1087 lines[0],
1088 ("(0008,0018)", "SOPInstanceUID", "UI", "\"1.2.888.123\""),
1089 );
1090 check_line(lines[1], ("(0008,0020)", "StudyDate", "DA", "2017-01-01"));
1091 check_line(lines[2], ("(0008,0023)", "ContentDate", "DA", "(no value)"));
1092 check_line(lines[3], ("(0008,0060)", "Modality", "CS", "\"OT\""));
1093 check_line(
1094 lines[4],
1095 ("(0008,0080)", "InstitutionName", "LO", "\"Hospital\""),
1096 );
1097 check_line(
1098 lines[5],
1099 (
1100 "(0008,0081)",
1101 "InstitutionAddress",
1102 "ST",
1103 "\"Country Roads 1␊West Virginia\"",
1104 ),
1105 );
1106 check_line(lines[6], ("(0028,0002)", "SamplesPerPixel", "US", "3"));
1107
1108 fn check_line(line: &str, expected: (&str, &str, &str, &str)) {
1109 let parts: Vec<&str> = line.split(" ").filter(|p| !p.is_empty()).collect();
1110 let value = line.split(':').nth(1).unwrap().trim();
1111 assert_eq!(&parts[..3], &[expected.0, expected.1, expected.2]);
1112 assert_eq!(value, expected.3);
1113 }
1114 }
1115
1116 #[test]
1117 fn dump_json() {
1118 let obj = InMemDicomObject::from_element_iter(vec![DataElement::new(
1120 tags::SOP_INSTANCE_UID,
1121 VR::UI,
1122 PrimitiveValue::from("1.2.888.123"),
1123 )]);
1124
1125 let mut out = Vec::new();
1126 DumpOptions::new()
1127 .color_mode(ColorMode::Never)
1128 .format(crate::DumpFormat::Json)
1129 .dump_object_to(&mut out, &obj)
1130 .unwrap();
1131
1132 let json = std::str::from_utf8(&out).expect("output is not valid UTF-8");
1133 assert_eq!(
1134 json,
1135 r#"{
1136 "00080018": {
1137 "vr": "UI",
1138 "Value": [
1139 "1.2.888.123"
1140 ]
1141 }
1142}"#
1143 );
1144 }
1145}