Skip to main content

openkspace_io/ismrmrd/
header.rs

1//! Minimal ISMRMRD XML header parser.
2//!
3//! We only extract the fields required for basic cartesian recon. The full
4//! schema is large; we deliberately do a narrow extraction here to avoid
5//! pulling in a heavy XSD-driven parser.
6
7use crate::error::{IoError, IoResult};
8use quick_xml::events::Event;
9use quick_xml::Reader;
10
11#[derive(Debug, Clone, Copy, Default)]
12pub struct MatrixSize {
13    pub x: u32,
14    pub y: u32,
15    pub z: u32,
16}
17
18#[derive(Debug, Clone, Copy, Default)]
19pub struct FieldOfView {
20    pub x: f32,
21    pub y: f32,
22    pub z: f32,
23}
24
25#[derive(Debug, Clone, Copy, Default)]
26pub struct EncodingLimit {
27    pub minimum: u32,
28    pub maximum: u32,
29    pub center: u32,
30}
31
32#[derive(Debug, Clone, Default)]
33pub struct EncodingInfo {
34    pub encoded_matrix: MatrixSize,
35    pub recon_matrix: MatrixSize,
36    pub encoded_fov: FieldOfView,
37    pub recon_fov: FieldOfView,
38    pub trajectory: String,      // "cartesian", "radial", ...
39    pub ky_limit: EncodingLimit, // kspace_encoding_step_1
40    pub kz_limit: EncodingLimit, // kspace_encoding_step_2
41    pub slice_limit: EncodingLimit,
42}
43
44#[non_exhaustive]
45#[derive(Debug, Clone, Default)]
46pub struct IsmrmrdHeader {
47    pub system_vendor: String,
48    pub system_model: String,
49    pub field_strength_t: f32,
50    pub receiver_channels: u32,
51    pub encoding: EncodingInfo,
52}
53
54impl IsmrmrdHeader {
55    /// Parse an ISMRMRD XML header string.
56    ///
57    /// The parser walks tokens and captures only the fields needed for
58    /// cartesian reconstruction.
59    pub fn parse(xml: &str) -> IoResult<Self> {
60        let mut reader = Reader::from_str(xml);
61        reader.config_mut().trim_text(true);
62
63        let mut hdr = IsmrmrdHeader::default();
64
65        // Breadcrumb stack of open element local-names.
66        let mut path: Vec<String> = Vec::with_capacity(16);
67        let mut buf = Vec::new();
68
69        loop {
70            match reader.read_event_into(&mut buf) {
71                Err(e) => {
72                    return Err(IoError::Xml(format!(
73                        "xml at {}: {e}",
74                        reader.buffer_position()
75                    )))
76                }
77                Ok(Event::Eof) => break,
78                Ok(Event::Start(e)) => {
79                    let name = local_name(e.name().as_ref());
80                    path.push(name);
81                }
82                Ok(Event::End(_)) => {
83                    path.pop();
84                }
85                Ok(Event::Empty(_)) => {
86                    // self-closing tag: no text to capture
87                }
88                Ok(Event::Text(t)) => {
89                    let text = t.unescape().map_err(|e| IoError::Xml(e.to_string()))?;
90                    let text = text.trim();
91                    if text.is_empty() {
92                        continue;
93                    }
94                    apply_field(&path, text, &mut hdr);
95                }
96                _ => {}
97            }
98            buf.clear();
99        }
100
101        Ok(hdr)
102    }
103
104    /// Matrix size used for the final image (recon space).
105    #[inline]
106    pub fn recon_size(&self) -> MatrixSize {
107        self.encoding.recon_matrix
108    }
109
110    /// Matrix size of the acquired k-space (encoded space).
111    #[inline]
112    pub fn encoded_size(&self) -> MatrixSize {
113        self.encoding.encoded_matrix
114    }
115}
116
117fn local_name(bytes: &[u8]) -> String {
118    // quick-xml gives us `prefix:local` or just `local` -- we only care about local.
119    let s = std::str::from_utf8(bytes).unwrap_or("");
120    match s.rfind(':') {
121        Some(i) => s[i + 1..].to_string(),
122        None => s.to_string(),
123    }
124}
125
126/// Dispatches a text value into the struct based on the path of open tags.
127fn apply_field(path: &[String], text: &str, hdr: &mut IsmrmrdHeader) {
128    // Walk the tail of the path to match. Use endswith-style matching so we
129    // ignore the top-level <ismrmrdHeader> wrapper.
130    let tail: Vec<&str> = path.iter().map(String::as_str).collect();
131
132    // ---- acquisitionSystemInformation --------------------------------------
133    if ends_with(&tail, &["acquisitionSystemInformation", "systemVendor"]) {
134        hdr.system_vendor = text.to_string();
135    } else if ends_with(&tail, &["acquisitionSystemInformation", "systemModel"]) {
136        hdr.system_model = text.to_string();
137    } else if ends_with(
138        &tail,
139        &["acquisitionSystemInformation", "systemFieldStrength_T"],
140    ) {
141        hdr.field_strength_t = text.parse().unwrap_or(0.0);
142    } else if ends_with(&tail, &["acquisitionSystemInformation", "receiverChannels"]) {
143        hdr.receiver_channels = text.parse().unwrap_or(0);
144    }
145    // ---- encoding/encodedSpace/matrixSize ----------------------------------
146    else if ends_with(&tail, &["encoding", "encodedSpace", "matrixSize", "x"]) {
147        hdr.encoding.encoded_matrix.x = text.parse().unwrap_or(0);
148    } else if ends_with(&tail, &["encoding", "encodedSpace", "matrixSize", "y"]) {
149        hdr.encoding.encoded_matrix.y = text.parse().unwrap_or(0);
150    } else if ends_with(&tail, &["encoding", "encodedSpace", "matrixSize", "z"]) {
151        hdr.encoding.encoded_matrix.z = text.parse().unwrap_or(0);
152    }
153    // ---- encoding/encodedSpace/fieldOfView_mm ------------------------------
154    else if ends_with(&tail, &["encoding", "encodedSpace", "fieldOfView_mm", "x"]) {
155        hdr.encoding.encoded_fov.x = text.parse().unwrap_or(0.0);
156    } else if ends_with(&tail, &["encoding", "encodedSpace", "fieldOfView_mm", "y"]) {
157        hdr.encoding.encoded_fov.y = text.parse().unwrap_or(0.0);
158    } else if ends_with(&tail, &["encoding", "encodedSpace", "fieldOfView_mm", "z"]) {
159        hdr.encoding.encoded_fov.z = text.parse().unwrap_or(0.0);
160    }
161    // ---- encoding/reconSpace/matrixSize ------------------------------------
162    else if ends_with(&tail, &["encoding", "reconSpace", "matrixSize", "x"]) {
163        hdr.encoding.recon_matrix.x = text.parse().unwrap_or(0);
164    } else if ends_with(&tail, &["encoding", "reconSpace", "matrixSize", "y"]) {
165        hdr.encoding.recon_matrix.y = text.parse().unwrap_or(0);
166    } else if ends_with(&tail, &["encoding", "reconSpace", "matrixSize", "z"]) {
167        hdr.encoding.recon_matrix.z = text.parse().unwrap_or(0);
168    }
169    // ---- encoding/reconSpace/fieldOfView_mm --------------------------------
170    else if ends_with(&tail, &["encoding", "reconSpace", "fieldOfView_mm", "x"]) {
171        hdr.encoding.recon_fov.x = text.parse().unwrap_or(0.0);
172    } else if ends_with(&tail, &["encoding", "reconSpace", "fieldOfView_mm", "y"]) {
173        hdr.encoding.recon_fov.y = text.parse().unwrap_or(0.0);
174    } else if ends_with(&tail, &["encoding", "reconSpace", "fieldOfView_mm", "z"]) {
175        hdr.encoding.recon_fov.z = text.parse().unwrap_or(0.0);
176    }
177    // ---- encoding/trajectory -----------------------------------------------
178    else if ends_with(&tail, &["encoding", "trajectory"]) {
179        hdr.encoding.trajectory = text.to_string();
180    }
181    // ---- encoding/encodingLimits -------------------------------------------
182    else if path.len() >= 4
183        && path[path.len() - 4] == "encoding"
184        && path[path.len() - 3] == "encodingLimits"
185    {
186        let section = &path[path.len() - 2];
187        let which = &path[path.len() - 1];
188        let target: Option<&mut EncodingLimit> = match section.as_str() {
189            "kspace_encoding_step_1" => Some(&mut hdr.encoding.ky_limit),
190            "kspace_encoding_step_2" => Some(&mut hdr.encoding.kz_limit),
191            "slice" => Some(&mut hdr.encoding.slice_limit),
192            _ => None,
193        };
194        if let Some(t) = target {
195            match which.as_str() {
196                "minimum" => t.minimum = text.parse().unwrap_or(0),
197                "maximum" => t.maximum = text.parse().unwrap_or(0),
198                "center" => t.center = text.parse().unwrap_or(0),
199                _ => {}
200            }
201        }
202    }
203}
204
205#[inline]
206fn ends_with(path: &[&str], suffix: &[&str]) -> bool {
207    if path.len() < suffix.len() {
208        return false;
209    }
210    path[path.len() - suffix.len()..] == *suffix
211}
212
213// ----------------------------------------------------------------------------
214//                                   Tests
215// ----------------------------------------------------------------------------
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    const SAMPLE: &str = r#"<?xml version="1.0"?>
221    <ismrmrdHeader xmlns="http://www.ismrm.org/ISMRMRD">
222      <acquisitionSystemInformation>
223        <systemVendor>GE MEDICAL SYSTEMS</systemVendor>
224        <systemModel>Orchestra SDK</systemModel>
225        <systemFieldStrength_T>3.000000</systemFieldStrength_T>
226        <receiverChannels>8</receiverChannels>
227      </acquisitionSystemInformation>
228      <encoding>
229        <encodedSpace>
230          <matrixSize><x>352</x><y>216</y><z>30</z></matrixSize>
231          <fieldOfView_mm><x>280</x><y>280</y><z>4.5</z></fieldOfView_mm>
232        </encodedSpace>
233        <reconSpace>
234          <matrixSize><x>512</x><y>512</y><z>0</z></matrixSize>
235          <fieldOfView_mm><x>280</x><y>280</y><z>4.5</z></fieldOfView_mm>
236        </reconSpace>
237        <encodingLimits>
238          <kspace_encoding_step_1><minimum>0</minimum><maximum>215</maximum><center>108</center></kspace_encoding_step_1>
239          <kspace_encoding_step_2><minimum>0</minimum><maximum>0</maximum><center>0</center></kspace_encoding_step_2>
240          <slice><minimum>0</minimum><maximum>29</maximum><center>15</center></slice>
241        </encodingLimits>
242        <trajectory>cartesian</trajectory>
243      </encoding>
244    </ismrmrdHeader>"#;
245
246    #[test]
247    fn parses_header_fields() {
248        let h = IsmrmrdHeader::parse(SAMPLE).unwrap();
249        assert_eq!(h.system_vendor, "GE MEDICAL SYSTEMS");
250        assert_eq!(h.receiver_channels, 8);
251        assert!((h.field_strength_t - 3.0).abs() < 1e-6);
252
253        assert_eq!(h.encoding.encoded_matrix.x, 352);
254        assert_eq!(h.encoding.encoded_matrix.y, 216);
255        assert_eq!(h.encoding.encoded_matrix.z, 30);
256        assert_eq!(h.encoding.recon_matrix.x, 512);
257        assert_eq!(h.encoding.recon_matrix.y, 512);
258
259        assert_eq!(h.encoding.ky_limit.maximum, 215);
260        assert_eq!(h.encoding.slice_limit.maximum, 29);
261        assert_eq!(h.encoding.trajectory, "cartesian");
262    }
263}