1use crate::dataset::DataSet;
8use crate::element::Element;
9use crate::value::{PixelData, Value};
10use dicom_toolkit_core::error::DcmResult;
11use dicom_toolkit_dict::Tag;
12
13pub fn to_xml(dataset: &DataSet) -> DcmResult<String> {
19 let mut out = String::with_capacity(4096);
20 out.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
21 out.push('\n');
22 out.push_str("<NativeDicomModel xml:space=\"preserve\">\n");
23 write_dataset(&mut out, dataset, 1)?;
24 out.push_str("</NativeDicomModel>\n");
25 Ok(out)
26}
27
28fn indent(level: usize) -> String {
29 " ".repeat(level)
30}
31
32fn write_dataset(out: &mut String, dataset: &DataSet, level: usize) -> DcmResult<()> {
33 for (tag, elem) in dataset.iter() {
34 if tag.is_group_length() || tag.is_delimiter() {
35 continue;
36 }
37 write_element(out, tag, elem, level)?;
38 }
39 Ok(())
40}
41
42fn write_element(out: &mut String, tag: &Tag, elem: &Element, level: usize) -> DcmResult<()> {
43 let pad = indent(level);
44 let vr_str = elem.vr.code();
45
46 out.push_str(&format!(
47 r#"{}<DicomAttribute tag="{:04X}{:04X}" vr="{}">"#,
48 pad, tag.group, tag.element, vr_str
49 ));
50
51 match &elem.value {
52 Value::Empty => {
53 out.push_str("/>\n");
54 return Ok(());
55 }
56 Value::Sequence(items) => {
57 out.push('\n');
58 for (i, item) in items.iter().enumerate() {
59 out.push_str(&format!(
60 "{}<Item number=\"{}\">\n",
61 indent(level + 1),
62 i + 1
63 ));
64 write_dataset(out, item, level + 2)?;
65 out.push_str(&format!("{}</Item>\n", indent(level + 1)));
66 }
67 out.push_str(&format!("{}</DicomAttribute>\n", pad));
68 return Ok(());
69 }
70 _ => {}
71 }
72
73 out.push('\n');
74
75 match &elem.value {
76 Value::Strings(v) => {
77 for (i, s) in v.iter().enumerate() {
78 out.push_str(&format!(
79 "{}<Value number=\"{}\">{}</Value>\n",
80 indent(level + 1),
81 i + 1,
82 xml_escape(s)
83 ));
84 }
85 }
86 Value::Uid(s) => {
87 out.push_str(&format!(
88 "{}<Value number=\"1\">{}</Value>\n",
89 indent(level + 1),
90 xml_escape(s)
91 ));
92 }
93 Value::PersonNames(names) => {
94 for (i, pn) in names.iter().enumerate() {
95 out.push_str(&format!(
96 "{}<PersonName number=\"{}\">\n",
97 indent(level + 1),
98 i + 1
99 ));
100 if !pn.alphabetic.is_empty() {
101 out.push_str(&format!(
102 "{}<Alphabetic><FamilyName>{}</FamilyName></Alphabetic>\n",
103 indent(level + 2),
104 xml_escape(pn.last_name())
105 ));
106 }
107 if !pn.ideographic.is_empty() {
108 out.push_str(&format!(
109 "{}<Ideographic>{}</Ideographic>\n",
110 indent(level + 2),
111 xml_escape(&pn.ideographic)
112 ));
113 }
114 if !pn.phonetic.is_empty() {
115 out.push_str(&format!(
116 "{}<Phonetic>{}</Phonetic>\n",
117 indent(level + 2),
118 xml_escape(&pn.phonetic)
119 ));
120 }
121 out.push_str(&format!("{}</PersonName>\n", indent(level + 1)));
122 }
123 }
124 Value::Date(dates) => {
125 for (i, d) in dates.iter().enumerate() {
126 out.push_str(&format!(
127 "{}<Value number=\"{}\">{}</Value>\n",
128 indent(level + 1),
129 i + 1,
130 d
131 ));
132 }
133 }
134 Value::Time(times) => {
135 for (i, t) in times.iter().enumerate() {
136 out.push_str(&format!(
137 "{}<Value number=\"{}\">{}</Value>\n",
138 indent(level + 1),
139 i + 1,
140 t
141 ));
142 }
143 }
144 Value::DateTime(dts) => {
145 for (i, dt) in dts.iter().enumerate() {
146 out.push_str(&format!(
147 "{}<Value number=\"{}\">{}</Value>\n",
148 indent(level + 1),
149 i + 1,
150 dt
151 ));
152 }
153 }
154 Value::Ints(v) => {
155 for (i, n) in v.iter().enumerate() {
156 out.push_str(&format!(
157 "{}<Value number=\"{}\">{}</Value>\n",
158 indent(level + 1),
159 i + 1,
160 n
161 ));
162 }
163 }
164 Value::Decimals(v) => {
165 for (i, n) in v.iter().enumerate() {
166 out.push_str(&format!(
167 "{}<Value number=\"{}\">{}</Value>\n",
168 indent(level + 1),
169 i + 1,
170 n
171 ));
172 }
173 }
174 Value::U16(v) => {
175 for (i, n) in v.iter().enumerate() {
176 out.push_str(&format!(
177 "{}<Value number=\"{}\">{}</Value>\n",
178 indent(level + 1),
179 i + 1,
180 n
181 ));
182 }
183 }
184 Value::I16(v) => {
185 for (i, n) in v.iter().enumerate() {
186 out.push_str(&format!(
187 "{}<Value number=\"{}\">{}</Value>\n",
188 indent(level + 1),
189 i + 1,
190 n
191 ));
192 }
193 }
194 Value::U32(v) => {
195 for (i, n) in v.iter().enumerate() {
196 out.push_str(&format!(
197 "{}<Value number=\"{}\">{}</Value>\n",
198 indent(level + 1),
199 i + 1,
200 n
201 ));
202 }
203 }
204 Value::I32(v) => {
205 for (i, n) in v.iter().enumerate() {
206 out.push_str(&format!(
207 "{}<Value number=\"{}\">{}</Value>\n",
208 indent(level + 1),
209 i + 1,
210 n
211 ));
212 }
213 }
214 Value::U64(v) => {
215 for (i, n) in v.iter().enumerate() {
216 out.push_str(&format!(
217 "{}<Value number=\"{}\">{}</Value>\n",
218 indent(level + 1),
219 i + 1,
220 n
221 ));
222 }
223 }
224 Value::I64(v) => {
225 for (i, n) in v.iter().enumerate() {
226 out.push_str(&format!(
227 "{}<Value number=\"{}\">{}</Value>\n",
228 indent(level + 1),
229 i + 1,
230 n
231 ));
232 }
233 }
234 Value::F32(v) => {
235 for (i, n) in v.iter().enumerate() {
236 out.push_str(&format!(
237 "{}<Value number=\"{}\">{}</Value>\n",
238 indent(level + 1),
239 i + 1,
240 n
241 ));
242 }
243 }
244 Value::F64(v) => {
245 for (i, n) in v.iter().enumerate() {
246 out.push_str(&format!(
247 "{}<Value number=\"{}\">{}</Value>\n",
248 indent(level + 1),
249 i + 1,
250 n
251 ));
252 }
253 }
254 Value::Tags(tags) => {
255 for (i, t) in tags.iter().enumerate() {
256 out.push_str(&format!(
257 "{}<Value number=\"{}\">{:04X}{:04X}</Value>\n",
258 indent(level + 1),
259 i + 1,
260 t.group,
261 t.element
262 ));
263 }
264 }
265 Value::U8(bytes) => {
267 use base64::Engine;
268 let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
269 out.push_str(&format!(
270 "{}<InlineBinary>{}</InlineBinary>\n",
271 indent(level + 1),
272 b64
273 ));
274 }
275 Value::PixelData(pd) => {
276 use base64::Engine;
277 let bytes: &[u8] = match pd {
278 PixelData::Native { bytes } => bytes,
279 PixelData::Encapsulated { fragments, .. } => {
280 fragments.first().map(|f| f.as_slice()).unwrap_or(&[])
281 }
282 };
283 let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
284 out.push_str(&format!(
285 "{}<InlineBinary>{}</InlineBinary>\n",
286 indent(level + 1),
287 b64
288 ));
289 }
290 Value::Empty | Value::Sequence(_) => {}
292 }
293
294 out.push_str(&format!("{}</DicomAttribute>\n", pad));
295 Ok(())
296}
297
298fn xml_escape(s: &str) -> String {
300 s.replace('&', "&")
301 .replace('<', "<")
302 .replace('>', ">")
303 .replace('"', """)
304 .replace('\'', "'")
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use dicom_toolkit_dict::{tags, Vr};
311
312 #[test]
313 fn xml_has_root_element() {
314 let ds = DataSet::new();
315 let xml = to_xml(&ds).unwrap();
316 assert!(
317 xml.contains("<NativeDicomModel"),
318 "should have NativeDicomModel root"
319 );
320 assert!(
321 xml.contains("</NativeDicomModel>"),
322 "should close NativeDicomModel"
323 );
324 }
325
326 #[test]
327 fn xml_contains_patient_name() {
328 let mut ds = DataSet::new();
329 ds.set_string(tags::PATIENT_NAME, Vr::PN, "Doe^Jane");
330 let xml = to_xml(&ds).unwrap();
331 assert!(xml.contains("00100010"), "should contain PatientName tag");
332 assert!(xml.contains("PN"), "should contain VR");
333 }
334
335 #[test]
336 fn xml_contains_uid() {
337 let mut ds = DataSet::new();
338 ds.set_uid(tags::SOP_INSTANCE_UID, "1.2.3.4.5");
339 let xml = to_xml(&ds).unwrap();
340 assert!(xml.contains("1.2.3.4.5"), "should contain UID value");
341 }
342
343 #[test]
344 fn xml_escapes_special_chars() {
345 let mut ds = DataSet::new();
346 ds.set_string(tags::PATIENT_ID, Vr::LO, "A<B>&C");
347 let xml = to_xml(&ds).unwrap();
348 assert!(xml.contains("<"), "< should be escaped");
349 assert!(xml.contains("&"), "& should be escaped");
350 assert!(!xml.contains("A<B>"), "raw < should not appear in value");
351 }
352
353 #[test]
354 fn xml_contains_sequence() {
355 let mut ds = DataSet::new();
356 let mut item = DataSet::new();
357 item.set_string(tags::PATIENT_ID, Vr::LO, "ITEM-1");
358 ds.set_sequence(tags::REFERENCED_SOP_SEQUENCE, vec![item]);
359 let xml = to_xml(&ds).unwrap();
360 assert!(
361 xml.contains("<Item number=\"1\">"),
362 "should have Item element"
363 );
364 assert!(xml.contains("</Item>"), "should close Item");
365 }
366
367 #[test]
368 fn xml_is_well_formed() {
369 let mut ds = DataSet::new();
370 ds.set_string(tags::PATIENT_NAME, Vr::PN, "Smith^John");
371 ds.set_string(tags::PATIENT_ID, Vr::LO, "ID-001");
372 ds.set_uid(tags::SOP_INSTANCE_UID, "1.2.3");
373 ds.set_u16(tags::ROWS, 256);
374 ds.set_u16(tags::COLUMNS, 256);
375 let xml = to_xml(&ds).unwrap();
376 assert!(
378 xml.contains("</DicomAttribute>") || xml.contains("/>"),
379 "all attributes should be closed"
380 );
381 }
382}