Skip to main content

dicom_viewer/dcm/
metadata.rs

1//! Flatten a DICOM file's tags into rows the metadata panel can display.
2
3use anyhow::{Context, Result};
4use dicom::core::dictionary::{DataDictionary, DataDictionaryEntryRef};
5use dicom::core::header::Header;
6use dicom::core::{Tag, VR};
7use dicom::object::mem::InMemElement;
8use dicom::object::OpenFileOptions;
9use dicom_dictionary_std::{tags, StandardDataDictionary};
10use std::path::Path;
11
12/// One row in the metadata panel.
13///
14/// Produced by [`collect_tags`] and rendered by the right-hand metadata
15/// panel. Sequences are flattened to a single row with a placeholder
16/// `<sequence, N item(s)>` in [`Self::value`]; nested items are not
17/// recursed into.
18#[derive(Debug, Clone)]
19pub struct TagRow {
20    /// DICOM tag (group, element).
21    pub tag: Tag,
22    /// VR mnemonic ("CS", "DS", "SQ", …).
23    pub vr: String,
24    /// Standard data-dictionary alias for the tag, or `"(unknown)"`.
25    pub name: String,
26    /// Formatted value. Binary values are surfaced as `"<binary value>"`;
27    /// long strings are truncated with an ellipsis.
28    pub value: String,
29}
30
31impl TagRow {
32    /// `"(GGGG,EEEE)"`-style tag label for the panel.
33    pub fn tag_label(&self) -> String {
34        format!("({:04X},{:04X})", self.tag.0, self.tag.1)
35    }
36    /// Human-readable section name for the tag's group, used to group
37    /// rows in the panel.
38    pub fn group_label(&self) -> String {
39        match self.tag.0 {
40            0x0002 => "0002 — File Meta",
41            0x0008 => "0008 — Identifying",
42            0x0010 => "0010 — Patient",
43            0x0018 => "0018 — Acquisition",
44            0x0020 => "0020 — Image",
45            0x0028 => "0028 — Image Pixel",
46            0x0032 => "0032 — Study",
47            0x0038 => "0038 — Visit",
48            0x0040 => "0040 — Procedure",
49            0x0054 => "0054 — Nuclear Medicine",
50            0x0070 => "0070 — Presentation",
51            0x0072 => "0072 — Hanging Protocol",
52            0x0088 => "0088 — Storage",
53            0x0400 => "0400 — Digital Signature",
54            0x2000 => "2000 — Film",
55            0x3006 => "3006 — RT Structure",
56            0x300A => "300A — RT Plan",
57            0x300C => "300C — RT Relationship",
58            0x300E => "300E — RT Approval",
59            0x4000 => "4000 — Text",
60            0x7FE0 => "7FE0 — Pixel Data",
61            g if g & 1 == 1 => "Private group",
62            _ => "Other",
63        }
64        .to_string()
65    }
66}
67
68/// Flatten every top-level DICOM tag in `path` into a sorted list of
69/// [`TagRow`]s. Stops at `PIXEL_DATA` — pixel bytes never reach this code
70/// path.
71///
72/// # Errors
73/// Returns an error when the file can't be opened with the dicom-rs
74/// metadata reader.
75pub fn collect_tags(path: &Path) -> Result<Vec<TagRow>> {
76    let obj = OpenFileOptions::new()
77        .read_until(tags::PIXEL_DATA)
78        .open_file(path)
79        .with_context(|| format!("open {}", path.display()))?;
80
81    let mut rows: Vec<TagRow> = Vec::new();
82    for elem in obj.iter() {
83        let tag: Tag = Header::tag(elem);
84        let vr_label = format!("{:?}", elem.vr());
85        let name = lookup_name(tag);
86        let value = format_value(elem);
87        rows.push(TagRow {
88            tag,
89            vr: vr_label,
90            name,
91            value,
92        });
93    }
94    rows.sort_by_key(|r| (r.tag.0, r.tag.1));
95    Ok(rows)
96}
97
98fn lookup_name(tag: Tag) -> String {
99    let dict = StandardDataDictionary;
100    let entry: Option<&DataDictionaryEntryRef<'static>> = dict.by_tag(tag);
101    match entry {
102        Some(e) => e.alias.to_string(),
103        None => "(unknown)".to_string(),
104    }
105}
106
107fn format_value(elem: &InMemElement) -> String {
108    const MAX_LEN: usize = 240;
109
110    if elem.vr() == VR::SQ {
111        let count = elem.items().map(<[_]>::len).unwrap_or(0);
112        return format!("<sequence, {count} item(s)>");
113    }
114
115    let raw = match elem.value().to_str() {
116        Ok(s) => s.to_string(),
117        Err(_) => {
118            return "<binary value>".to_string();
119        }
120    };
121
122    let cleaned: String = raw
123        .chars()
124        .map(|c: char| if c.is_control() && c != '\n' { ' ' } else { c })
125        .collect();
126    let trimmed = cleaned.trim_end_matches('\0').trim().to_string();
127    if trimmed.chars().count() > MAX_LEN {
128        let cut: String = trimmed.chars().take(MAX_LEN).collect();
129        format!("{cut}…")
130    } else {
131        trimmed
132    }
133}