Skip to main content

dicom_toolkit_data/
json.rs

1//! DICOM JSON serialization and deserialization (PS3.18 Annex F).
2//!
3//! Ports DCMTK's `dcjson.h` / `dcjsonrd.h`. The DICOM JSON model encodes each
4//! element as `{ "GGGGEEEE": { "vr": "XX", "Value": [...] } }`.
5
6use crate::dataset::DataSet;
7use crate::element::Element;
8use crate::value::{DicomDate, DicomDateTime, DicomTime, PersonName, PixelData, Value};
9use dicom_toolkit_core::error::{DcmError, DcmResult};
10use dicom_toolkit_dict::{Tag, Vr};
11
12// ── Serialization ─────────────────────────────────────────────────────────────
13
14/// Controls how binary values are represented when serializing DICOM JSON.
15pub enum BinaryValueMode<'a> {
16    /// Keep binary data inline using the existing JSON behavior.
17    InlineBinary,
18    /// Emit `BulkDataURI` for eligible tags when the callback provides a URI.
19    ///
20    /// If encapsulated Pixel Data is encountered and the callback does not
21    /// provide a URI, serialization returns an error instead of emitting only
22    /// the first fragment.
23    BulkDataUri(&'a dyn Fn(Tag) -> Option<String>),
24}
25
26/// Serialize a `DataSet` to a DICOM JSON string (PS3.18 §F.2).
27pub fn to_json(dataset: &DataSet) -> DcmResult<String> {
28    let obj = dataset_to_json_object(dataset)?;
29    serde_json::to_string(&obj).map_err(|e| DcmError::Other(format!("JSON serialize error: {e}")))
30}
31
32/// Serialize a `DataSet` to a pretty-printed DICOM JSON string.
33pub fn to_json_pretty(dataset: &DataSet) -> DcmResult<String> {
34    let obj = dataset_to_json_object(dataset)?;
35    serde_json::to_string_pretty(&obj)
36        .map_err(|e| DcmError::Other(format!("JSON serialize error: {e}")))
37}
38
39/// Serialize a `DataSet` to a DICOM JSON string using an explicit binary-value policy.
40pub fn to_json_with_binary_mode(dataset: &DataSet, mode: BinaryValueMode<'_>) -> DcmResult<String> {
41    let obj = dataset_to_json_object_with_binary_mode(dataset, &mode)?;
42    serde_json::to_string(&obj).map_err(|e| DcmError::Other(format!("JSON serialize error: {e}")))
43}
44
45fn dataset_to_json_object(
46    dataset: &DataSet,
47) -> DcmResult<serde_json::Map<String, serde_json::Value>> {
48    dataset_to_json_object_internal(dataset, None)
49}
50
51fn dataset_to_json_object_with_binary_mode(
52    dataset: &DataSet,
53    mode: &BinaryValueMode<'_>,
54) -> DcmResult<serde_json::Map<String, serde_json::Value>> {
55    dataset_to_json_object_internal(dataset, Some(mode))
56}
57
58fn dataset_to_json_object_internal(
59    dataset: &DataSet,
60    binary_mode: Option<&BinaryValueMode<'_>>,
61) -> DcmResult<serde_json::Map<String, serde_json::Value>> {
62    let mut map = serde_json::Map::new();
63    for (tag, elem) in dataset.iter() {
64        // Skip group-length tags and sequence delimiter tags
65        if tag.is_group_length() || tag.is_delimiter() {
66            continue;
67        }
68        let key = format!("{:04X}{:04X}", tag.group, tag.element);
69        let json_elem = element_to_json_internal(elem, binary_mode)?;
70        map.insert(key, json_elem);
71    }
72    Ok(map)
73}
74
75fn element_to_json_internal(
76    elem: &Element,
77    binary_mode: Option<&BinaryValueMode<'_>>,
78) -> DcmResult<serde_json::Value> {
79    let vr_str = elem.vr.code().to_string();
80
81    if let Some(json) = binary_value_to_json(elem, binary_mode, &vr_str)? {
82        return Ok(json);
83    }
84
85    let value_json: Option<serde_json::Value> = match &elem.value {
86        Value::Empty => None,
87
88        Value::Strings(v) => {
89            if v.is_empty() {
90                None
91            } else if elem.vr == Vr::PN {
92                // PN strings stored as raw "Last^First^Middle^Prefix^Suffix" — convert to JSON PN format
93                let arr: Vec<serde_json::Value> = v
94                    .iter()
95                    .map(|s| {
96                        let mut obj = serde_json::Map::new();
97                        if !s.is_empty() {
98                            obj.insert("Alphabetic".into(), serde_json::Value::String(s.clone()));
99                        }
100                        serde_json::Value::Object(obj)
101                    })
102                    .collect();
103                Some(serde_json::Value::Array(arr))
104            } else {
105                Some(serde_json::Value::Array(
106                    v.iter()
107                        .map(|s| serde_json::Value::String(s.clone()))
108                        .collect(),
109                ))
110            }
111        }
112
113        Value::Uid(s) => Some(serde_json::Value::Array(vec![serde_json::Value::String(
114            s.clone(),
115        )])),
116
117        Value::PersonNames(names) => {
118            let arr: Vec<serde_json::Value> = names
119                .iter()
120                .map(|pn| {
121                    let mut obj = serde_json::Map::new();
122                    if !pn.alphabetic.is_empty() {
123                        obj.insert(
124                            "Alphabetic".into(),
125                            serde_json::Value::String(pn.alphabetic.clone()),
126                        );
127                    }
128                    if !pn.ideographic.is_empty() {
129                        obj.insert(
130                            "Ideographic".into(),
131                            serde_json::Value::String(pn.ideographic.clone()),
132                        );
133                    }
134                    if !pn.phonetic.is_empty() {
135                        obj.insert(
136                            "Phonetic".into(),
137                            serde_json::Value::String(pn.phonetic.clone()),
138                        );
139                    }
140                    serde_json::Value::Object(obj)
141                })
142                .collect();
143            if arr.is_empty() {
144                None
145            } else {
146                Some(serde_json::Value::Array(arr))
147            }
148        }
149
150        Value::Date(dates) => Some(serde_json::Value::Array(
151            dates
152                .iter()
153                .map(|d| serde_json::Value::String(d.to_string()))
154                .collect(),
155        )),
156        Value::Time(times) => Some(serde_json::Value::Array(
157            times
158                .iter()
159                .map(|t| serde_json::Value::String(t.to_string()))
160                .collect(),
161        )),
162        Value::DateTime(dts) => Some(serde_json::Value::Array(
163            dts.iter()
164                .map(|dt| serde_json::Value::String(dt.to_string()))
165                .collect(),
166        )),
167
168        Value::Ints(v) => Some(serde_json::Value::Array(
169            v.iter().map(|n| serde_json::json!(n)).collect(),
170        )),
171        Value::Decimals(v) => Some(serde_json::Value::Array(
172            v.iter()
173                .map(|n| {
174                    if n.is_finite() {
175                        serde_json::json!(n)
176                    } else {
177                        serde_json::Value::Null
178                    }
179                })
180                .collect(),
181        )),
182
183        Value::U16(v) => Some(serde_json::Value::Array(
184            v.iter().map(|n| serde_json::json!(n)).collect(),
185        )),
186        Value::I16(v) => Some(serde_json::Value::Array(
187            v.iter().map(|n| serde_json::json!(n)).collect(),
188        )),
189        Value::U32(v) => Some(serde_json::Value::Array(
190            v.iter().map(|n| serde_json::json!(n)).collect(),
191        )),
192        Value::I32(v) => Some(serde_json::Value::Array(
193            v.iter().map(|n| serde_json::json!(n)).collect(),
194        )),
195        Value::U64(v) => Some(serde_json::Value::Array(
196            v.iter().map(|n| serde_json::json!(n)).collect(),
197        )),
198        Value::I64(v) => Some(serde_json::Value::Array(
199            v.iter().map(|n| serde_json::json!(n)).collect(),
200        )),
201        Value::F32(v) => Some(serde_json::Value::Array(
202            v.iter()
203                .map(|n| {
204                    if n.is_finite() {
205                        serde_json::json!(n)
206                    } else {
207                        serde_json::Value::Null
208                    }
209                })
210                .collect(),
211        )),
212        Value::F64(v) => Some(serde_json::Value::Array(
213            v.iter()
214                .map(|n| {
215                    if n.is_finite() {
216                        serde_json::json!(n)
217                    } else {
218                        serde_json::Value::Null
219                    }
220                })
221                .collect(),
222        )),
223
224        Value::Tags(tags) => Some(serde_json::Value::Array(
225            tags.iter()
226                .map(|t| serde_json::Value::String(format!("{:04X}{:04X}", t.group, t.element)))
227                .collect(),
228        )),
229
230        Value::Sequence(items) => {
231            let arr: Vec<serde_json::Value> = items
232                .iter()
233                .map(|item| {
234                    dataset_to_json_object_internal(item, binary_mode)
235                        .map(serde_json::Value::Object)
236                        .unwrap_or(serde_json::Value::Null)
237                })
238                .collect();
239            Some(serde_json::Value::Array(arr))
240        }
241        Value::U8(bytes) => {
242            use base64::Engine;
243            let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
244            let mut obj = serde_json::Map::new();
245            obj.insert("vr".into(), serde_json::Value::String(vr_str.clone()));
246            obj.insert("InlineBinary".into(), serde_json::Value::String(b64));
247            return Ok(serde_json::Value::Object(obj));
248        }
249        Value::PixelData(pd) => {
250            let bytes = match pd {
251                PixelData::Native { bytes } => bytes.as_slice(),
252                PixelData::Encapsulated { fragments, .. } => {
253                    fragments.first().map(|f| f.as_slice()).unwrap_or(&[])
254                }
255            };
256            use base64::Engine;
257            let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
258            let mut obj = serde_json::Map::new();
259            obj.insert("vr".into(), serde_json::Value::String(vr_str.clone()));
260            obj.insert("InlineBinary".into(), serde_json::Value::String(b64));
261            return Ok(serde_json::Value::Object(obj));
262        }
263    };
264
265    let mut obj = serde_json::Map::new();
266    obj.insert("vr".into(), serde_json::Value::String(vr_str));
267    if let Some(v) = value_json {
268        obj.insert("Value".into(), v);
269    }
270    Ok(serde_json::Value::Object(obj))
271}
272
273fn binary_value_to_json(
274    elem: &Element,
275    binary_mode: Option<&BinaryValueMode<'_>>,
276    vr_str: &str,
277) -> DcmResult<Option<serde_json::Value>> {
278    let Some(binary_mode) = binary_mode else {
279        return Ok(None);
280    };
281
282    if !is_bulk_data_eligible(elem) {
283        return Ok(None);
284    }
285
286    match binary_mode {
287        BinaryValueMode::BulkDataUri(resolve_uri) => {
288            if let Some(uri) = resolve_uri(elem.tag) {
289                return Ok(Some(json_bulk_data_uri(vr_str, uri)));
290            }
291
292            if matches!(elem.value, Value::PixelData(PixelData::Encapsulated { .. })) {
293                return Err(DcmError::Other(format!(
294                    "encapsulated Pixel Data tag {} requires BulkDataURI in to_json_with_binary_mode",
295                    elem.tag
296                )));
297            }
298
299            Ok(None)
300        }
301        BinaryValueMode::InlineBinary => {
302            if let Value::PixelData(PixelData::Encapsulated { .. }) = &elem.value {
303                return Err(DcmError::Other(format!(
304                    "encapsulated Pixel Data tag {} requires BulkDataURI in to_json_with_binary_mode",
305                    elem.tag
306                )));
307            }
308            Ok(None)
309        }
310    }
311}
312
313fn is_bulk_data_eligible(elem: &Element) -> bool {
314    matches!(elem.value, Value::PixelData(_))
315        || matches!(
316            elem.vr,
317            Vr::OB | Vr::OD | Vr::OF | Vr::OL | Vr::OV | Vr::OW | Vr::UN
318        )
319}
320
321fn json_bulk_data_uri(vr_str: &str, uri: String) -> serde_json::Value {
322    let mut obj = serde_json::Map::new();
323    obj.insert("vr".into(), serde_json::Value::String(vr_str.to_string()));
324    obj.insert("BulkDataURI".into(), serde_json::Value::String(uri));
325    serde_json::Value::Object(obj)
326}
327
328// ── Deserialization ───────────────────────────────────────────────────────────
329
330/// Deserialize a DICOM JSON string into a `DataSet`.
331pub fn from_json(json: &str) -> DcmResult<DataSet> {
332    let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(json)
333        .map_err(|e| DcmError::Other(format!("JSON parse error: {e}")))?;
334    json_object_to_dataset(&map)
335}
336
337fn json_object_to_dataset(map: &serde_json::Map<String, serde_json::Value>) -> DcmResult<DataSet> {
338    let mut dataset = DataSet::new();
339    for (key, val) in map {
340        let tag = parse_json_tag(key)?;
341        let elem = json_value_to_element(tag, val)?;
342        dataset.insert(elem);
343    }
344    Ok(dataset)
345}
346
347fn parse_json_tag(key: &str) -> DcmResult<Tag> {
348    if key.len() != 8 {
349        return Err(DcmError::Other(format!("invalid JSON tag key: '{key}'")));
350    }
351    let group = u16::from_str_radix(&key[0..4], 16)
352        .map_err(|_| DcmError::Other(format!("invalid tag group: '{}'", &key[0..4])))?;
353    let element = u16::from_str_radix(&key[4..8], 16)
354        .map_err(|_| DcmError::Other(format!("invalid tag element: '{}'", &key[4..8])))?;
355    Ok(Tag::new(group, element))
356}
357
358fn json_scalar_token_string(tag: Tag, vr: Vr, value: &serde_json::Value) -> DcmResult<String> {
359    match value {
360        serde_json::Value::String(s) => Ok(s.clone()),
361        serde_json::Value::Number(n) => Ok(n.to_string()),
362        serde_json::Value::Null => Ok(String::new()),
363        _ => Err(DcmError::Other(format!(
364            "invalid JSON value for tag {tag} VR {}: expected number, string or null",
365            vr.code()
366        ))),
367    }
368}
369
370fn json_value_tokens(tag: Tag, vr: Vr, values_arr: &[serde_json::Value]) -> DcmResult<Vec<String>> {
371    values_arr
372        .iter()
373        .map(|value| json_scalar_token_string(tag, vr, value))
374        .collect()
375}
376
377fn tokens_are_all_empty(tokens: &[String]) -> bool {
378    tokens.iter().all(|token| token.is_empty())
379}
380
381fn reject_mixed_empty_tokens(tag: Tag, vr: Vr, tokens: &[String]) -> DcmResult<()> {
382    let has_empty = tokens.iter().any(|token| token.is_empty());
383    let has_non_empty = tokens.iter().any(|token| !token.is_empty());
384
385    if has_empty && has_non_empty {
386        return Err(DcmError::Other(format!(
387            "tag {tag} VR {} cannot represent mixed empty and non-empty JSON values",
388            vr.code()
389        )));
390    }
391
392    Ok(())
393}
394
395fn parse_numeric_tokens<T>(
396    tag: Tag,
397    vr: Vr,
398    values_arr: &[serde_json::Value],
399) -> DcmResult<Option<Vec<T>>>
400where
401    T: std::str::FromStr,
402    T::Err: std::fmt::Display,
403{
404    let tokens = json_value_tokens(tag, vr, values_arr)?;
405    if tokens_are_all_empty(&tokens) {
406        return Ok(None);
407    }
408
409    reject_mixed_empty_tokens(tag, vr, &tokens)?;
410
411    let values = tokens
412        .iter()
413        .map(|token| {
414            token.parse::<T>().map_err(|err| {
415                DcmError::Other(format!(
416                    "invalid {} value '{}' for tag {tag}: {err}",
417                    vr.code(),
418                    token
419                ))
420            })
421        })
422        .collect::<DcmResult<Vec<T>>>()?;
423
424    Ok(Some(values))
425}
426
427fn json_value_to_element(tag: Tag, val: &serde_json::Value) -> DcmResult<Element> {
428    let obj = val
429        .as_object()
430        .ok_or_else(|| DcmError::Other(format!("expected JSON object for tag {tag}")))?;
431
432    let vr_str = obj
433        .get("vr")
434        .and_then(|v| v.as_str())
435        .ok_or_else(|| DcmError::Other(format!("missing 'vr' in JSON element for tag {tag}")))?;
436
437    let vr = Vr::from_bytes([vr_str.as_bytes()[0], vr_str.as_bytes()[1]])
438        .ok_or_else(|| DcmError::Other(format!("unknown VR '{vr_str}' in JSON for tag {tag}")))?;
439
440    // Check for InlineBinary first
441    if let Some(b64_val) = obj.get("InlineBinary") {
442        use base64::Engine;
443        let b64_str = b64_val
444            .as_str()
445            .ok_or_else(|| DcmError::Other("InlineBinary must be a string".into()))?;
446        let bytes = base64::engine::general_purpose::STANDARD
447            .decode(b64_str)
448            .map_err(|e| DcmError::Other(format!("base64 decode error: {e}")))?;
449        return Ok(Element::bytes(tag, vr, bytes));
450    }
451
452    if let Some(uri_val) = obj.get("BulkDataURI") {
453        let _uri = uri_val
454            .as_str()
455            .ok_or_else(|| DcmError::Other("BulkDataURI must be a string".into()))?;
456        return Err(DcmError::Other(format!(
457            "BulkDataURI deserialization is not supported for tag {tag}"
458        )));
459    }
460
461    let values_arr = match obj.get("Value") {
462        None => return Ok(Element::new(tag, vr, Value::Empty)),
463        Some(v) => v
464            .as_array()
465            .ok_or_else(|| DcmError::Other(format!("'Value' must be array for tag {tag}")))?,
466    };
467
468    let value = match vr {
469        Vr::SQ => {
470            let items: DcmResult<Vec<DataSet>> = values_arr
471                .iter()
472                .map(|item| {
473                    let item_obj = item
474                        .as_object()
475                        .ok_or_else(|| DcmError::Other("SQ item must be a JSON object".into()))?;
476                    json_object_to_dataset(item_obj)
477                })
478                .collect();
479            Value::Sequence(items?)
480        }
481
482        Vr::PN => {
483            let names: DcmResult<Vec<PersonName>> = values_arr
484                .iter()
485                .map(|pn_val| {
486                    if pn_val.is_null() {
487                        return Ok(PersonName::parse(""));
488                    }
489                    let pn_obj = pn_val
490                        .as_object()
491                        .ok_or_else(|| DcmError::Other("PN value must be a JSON object".into()))?;
492                    let alphabetic = pn_obj
493                        .get("Alphabetic")
494                        .and_then(|v| v.as_str())
495                        .unwrap_or("")
496                        .to_string();
497                    let ideographic = pn_obj
498                        .get("Ideographic")
499                        .and_then(|v| v.as_str())
500                        .unwrap_or("")
501                        .to_string();
502                    let phonetic = pn_obj
503                        .get("Phonetic")
504                        .and_then(|v| v.as_str())
505                        .unwrap_or("")
506                        .to_string();
507                    Ok(PersonName {
508                        alphabetic,
509                        ideographic,
510                        phonetic,
511                    })
512                })
513                .collect();
514            Value::PersonNames(names?)
515        }
516
517        Vr::DA => {
518            let tokens = json_value_tokens(tag, vr, values_arr)?;
519            if tokens_are_all_empty(&tokens) {
520                Value::Empty
521            } else {
522                reject_mixed_empty_tokens(tag, vr, &tokens)?;
523                let dates: DcmResult<Vec<DicomDate>> =
524                    tokens.iter().map(|token| DicomDate::parse(token)).collect();
525                Value::Date(dates?)
526            }
527        }
528
529        Vr::TM => {
530            let tokens = json_value_tokens(tag, vr, values_arr)?;
531            if tokens_are_all_empty(&tokens) {
532                Value::Empty
533            } else {
534                reject_mixed_empty_tokens(tag, vr, &tokens)?;
535                let times: DcmResult<Vec<DicomTime>> =
536                    tokens.iter().map(|token| DicomTime::parse(token)).collect();
537                Value::Time(times?)
538            }
539        }
540
541        Vr::DT => {
542            let tokens = json_value_tokens(tag, vr, values_arr)?;
543            if tokens_are_all_empty(&tokens) {
544                Value::Empty
545            } else {
546                reject_mixed_empty_tokens(tag, vr, &tokens)?;
547                let dts: DcmResult<Vec<DicomDateTime>> = tokens
548                    .iter()
549                    .map(|token| DicomDateTime::parse(token))
550                    .collect();
551                Value::DateTime(dts?)
552            }
553        }
554
555        Vr::UI => {
556            let tokens = json_value_tokens(tag, vr, values_arr)?;
557            if tokens_are_all_empty(&tokens) {
558                Value::Empty
559            } else {
560                reject_mixed_empty_tokens(tag, vr, &tokens)?;
561                let uid = tokens.first().cloned().unwrap_or_default();
562                Value::Uid(uid)
563            }
564        }
565
566        Vr::IS => match parse_numeric_tokens::<i64>(tag, vr, values_arr)? {
567            Some(ints) => Value::Ints(ints),
568            None => Value::Empty,
569        },
570
571        Vr::DS => match parse_numeric_tokens::<f64>(tag, vr, values_arr)? {
572            Some(decimals) => Value::Decimals(decimals),
573            None => Value::Empty,
574        },
575
576        Vr::US | Vr::OW => match parse_numeric_tokens::<u16>(tag, vr, values_arr)? {
577            Some(vals) => Value::U16(vals),
578            None => Value::Empty,
579        },
580
581        Vr::SS => match parse_numeric_tokens::<i16>(tag, vr, values_arr)? {
582            Some(vals) => Value::I16(vals),
583            None => Value::Empty,
584        },
585
586        Vr::UL | Vr::OL => match parse_numeric_tokens::<u32>(tag, vr, values_arr)? {
587            Some(vals) => Value::U32(vals),
588            None => Value::Empty,
589        },
590
591        Vr::SL => match parse_numeric_tokens::<i32>(tag, vr, values_arr)? {
592            Some(vals) => Value::I32(vals),
593            None => Value::Empty,
594        },
595
596        Vr::UV | Vr::OV => match parse_numeric_tokens::<u64>(tag, vr, values_arr)? {
597            Some(vals) => Value::U64(vals),
598            None => Value::Empty,
599        },
600
601        Vr::SV => match parse_numeric_tokens::<i64>(tag, vr, values_arr)? {
602            Some(vals) => Value::I64(vals),
603            None => Value::Empty,
604        },
605
606        Vr::FL | Vr::OF => match parse_numeric_tokens::<f32>(tag, vr, values_arr)? {
607            Some(vals) => Value::F32(vals),
608            None => Value::Empty,
609        },
610
611        Vr::FD | Vr::OD => match parse_numeric_tokens::<f64>(tag, vr, values_arr)? {
612            Some(vals) => Value::F64(vals),
613            None => Value::Empty,
614        },
615
616        Vr::AT => {
617            let tags: DcmResult<Vec<Tag>> = values_arr
618                .iter()
619                .filter_map(|v| v.as_str())
620                .map(|s| {
621                    if s.len() != 8 {
622                        return Err(DcmError::Other(format!("invalid AT value: '{s}'")));
623                    }
624                    let g = u16::from_str_radix(&s[0..4], 16)
625                        .map_err(|_| DcmError::Other(format!("bad AT group: {s}")))?;
626                    let e = u16::from_str_radix(&s[4..8], 16)
627                        .map_err(|_| DcmError::Other(format!("bad AT element: {s}")))?;
628                    Ok(Tag::new(g, e))
629                })
630                .collect();
631            Value::Tags(tags?)
632        }
633
634        // Default: string VRs
635        _ => {
636            let strings = json_value_tokens(tag, vr, values_arr)?;
637            Value::Strings(strings)
638        }
639    };
640
641    Ok(Element::new(tag, vr, value))
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647    use dicom_toolkit_dict::tags;
648
649    fn make_dataset() -> DataSet {
650        let mut ds = DataSet::new();
651        ds.set_string(tags::PATIENT_NAME, Vr::PN, "Doe^John");
652        ds.set_string(tags::PATIENT_ID, Vr::LO, "PAT-001");
653        ds.set_uid(tags::SOP_INSTANCE_UID, "1.2.3.4.5");
654        ds.set_u16(tags::SAMPLES_PER_PIXEL, 1);
655        ds.set_u16(tags::ROWS, 512);
656        ds.set_u16(tags::COLUMNS, 512);
657        ds
658    }
659
660    #[test]
661    fn serialize_basic_dataset() {
662        let ds = make_dataset();
663        let json = to_json(&ds).unwrap();
664        // Should contain our tags as 8-hex-char keys
665        assert!(json.contains("00100010"), "should contain PatientName tag");
666        assert!(json.contains("00100020"), "should contain PatientID tag");
667        assert!(
668            json.contains("0020000D") || json.contains("00080018"),
669            "should contain UID tag"
670        );
671    }
672
673    #[test]
674    fn roundtrip_string_element() {
675        let mut ds = DataSet::new();
676        ds.set_string(tags::PATIENT_ID, Vr::LO, "PAT-123");
677
678        let json = to_json(&ds).unwrap();
679        let parsed = from_json(&json).unwrap();
680
681        assert_eq!(parsed.get_string(tags::PATIENT_ID), Some("PAT-123"));
682    }
683
684    #[test]
685    fn roundtrip_uid_element() {
686        let mut ds = DataSet::new();
687        ds.set_uid(tags::SOP_INSTANCE_UID, "1.2.840.10008.5.1.4.1.1.2");
688
689        let json = to_json(&ds).unwrap();
690        let parsed = from_json(&json).unwrap();
691
692        assert_eq!(
693            parsed.get_string(tags::SOP_INSTANCE_UID),
694            Some("1.2.840.10008.5.1.4.1.1.2")
695        );
696    }
697
698    #[test]
699    fn roundtrip_u16_element() {
700        let mut ds = DataSet::new();
701        ds.set_u16(tags::ROWS, 256);
702
703        let json = to_json(&ds).unwrap();
704        let parsed = from_json(&json).unwrap();
705
706        assert_eq!(parsed.get_u16(tags::ROWS), Some(256));
707    }
708
709    #[test]
710    fn roundtrip_sequence() {
711        let mut ds = DataSet::new();
712        let mut item = DataSet::new();
713        item.set_string(tags::PATIENT_ID, Vr::LO, "ITEM-1");
714        ds.set_sequence(tags::REFERENCED_SOP_SEQUENCE, vec![item]);
715
716        let json = to_json(&ds).unwrap();
717        let parsed = from_json(&json).unwrap();
718
719        let items = parsed.get_items(tags::REFERENCED_SOP_SEQUENCE).unwrap();
720        assert_eq!(items.len(), 1);
721        assert_eq!(items[0].get_string(tags::PATIENT_ID), Some("ITEM-1"));
722    }
723
724    #[test]
725    fn roundtrip_person_name() {
726        let mut ds = DataSet::new();
727        ds.set_string(tags::PATIENT_NAME, Vr::PN, "Smith^John^^Dr.");
728
729        let json = to_json(&ds).unwrap();
730        assert!(json.contains("Alphabetic"), "PN should use Alphabetic key");
731
732        let parsed = from_json(&json).unwrap();
733        // PN is stored as PersonName; we get it via the string getter which formats it back
734        assert!(parsed.contains(tags::PATIENT_NAME));
735    }
736
737    #[test]
738    fn invalid_json_returns_error() {
739        assert!(from_json("not json").is_err());
740        assert!(from_json("[]").is_err(), "array at root should fail");
741    }
742
743    #[test]
744    fn invalid_tag_key_returns_error() {
745        // tag key too short
746        assert!(from_json(r#"{"00100": {"vr": "LO"}}"#).is_err());
747        // non-hex chars
748        assert!(from_json(r#"{"GGGGEEEE": {"vr": "LO"}}"#).is_err());
749    }
750
751    #[test]
752    fn pretty_print_is_valid_json() {
753        let ds = make_dataset();
754        let pretty = to_json_pretty(&ds).unwrap();
755        // Should be parseable
756        let reparsed: serde_json::Value = serde_json::from_str(&pretty).unwrap();
757        assert!(reparsed.is_object());
758    }
759
760    #[test]
761    fn bulk_data_uri_mode_uses_uri_for_binary_vrs() {
762        let binary_tag = Tag::new(0x5400, 0x1010);
763        let mut ds = DataSet::new();
764        ds.insert(Element::new(
765            binary_tag,
766            Vr::OB,
767            Value::U8(vec![1, 2, 3, 4]),
768        ));
769
770        let json = to_json_with_binary_mode(
771            &ds,
772            BinaryValueMode::BulkDataUri(&|tag| {
773                (tag == binary_tag).then_some("https://example.test/bulk/54001010".to_string())
774            }),
775        )
776        .unwrap();
777
778        assert!(json.contains("\"BulkDataURI\":\"https://example.test/bulk/54001010\""));
779        assert!(!json.contains("InlineBinary"));
780    }
781
782    #[test]
783    fn bulk_data_uri_mode_uses_uri_for_pixel_data() {
784        let mut ds = DataSet::new();
785        ds.insert(Element::new(
786            tags::PIXEL_DATA,
787            Vr::OB,
788            Value::PixelData(PixelData::Encapsulated {
789                offset_table: vec![0],
790                fragments: vec![vec![1, 2], vec![3, 4]],
791            }),
792        ));
793
794        let json = to_json_with_binary_mode(
795            &ds,
796            BinaryValueMode::BulkDataUri(&|tag| {
797                (tag == tags::PIXEL_DATA).then_some("https://example.test/bulk/pixel".to_string())
798            }),
799        )
800        .unwrap();
801
802        assert!(json.contains("\"BulkDataURI\":\"https://example.test/bulk/pixel\""));
803        assert!(!json.contains("InlineBinary"));
804    }
805
806    #[test]
807    fn bulk_data_uri_mode_rejects_encapsulated_pixel_data_without_uri() {
808        let mut ds = DataSet::new();
809        ds.insert(Element::new(
810            tags::PIXEL_DATA,
811            Vr::OB,
812            Value::PixelData(PixelData::Encapsulated {
813                offset_table: vec![0],
814                fragments: vec![vec![1, 2], vec![3, 4]],
815            }),
816        ));
817
818        let err =
819            to_json_with_binary_mode(&ds, BinaryValueMode::BulkDataUri(&|_| None)).unwrap_err();
820        assert!(err.to_string().contains("requires BulkDataURI"));
821    }
822
823    #[test]
824    fn from_json_accepts_dcmtk_style_scalar_tokens() {
825        let json = r#"{
826            "00100020": {"vr": "LO", "Value": [123]},
827            "00200013": {"vr": "IS", "Value": ["42"]},
828            "00180050": {"vr": "DS", "Value": ["2.5"]},
829            "00280010": {"vr": "US", "Value": ["256"]}
830        }"#;
831
832        let parsed = from_json(json).unwrap();
833
834        assert_eq!(parsed.get_string(tags::PATIENT_ID), Some("123"));
835        assert_eq!(
836            parsed.get(Tag::new(0x0020, 0x0013)).unwrap().value,
837            Value::Ints(vec![42])
838        );
839        match &parsed.get(Tag::new(0x0018, 0x0050)).unwrap().value {
840            Value::Decimals(values) => assert!((values[0] - 2.5).abs() < 1e-9),
841            other => panic!("unexpected value: {:?}", other),
842        }
843        assert_eq!(parsed.get_u16(tags::ROWS), Some(256));
844    }
845
846    #[test]
847    fn from_json_rejects_non_scalar_value_entries() {
848        let err = from_json(r#"{"00100020":{"vr":"LO","Value":[{}]}}"#).unwrap_err();
849        assert!(err.to_string().contains("expected number, string or null"));
850    }
851
852    #[test]
853    fn from_json_rejects_bulk_data_uri_without_loader() {
854        let err =
855            from_json(r#"{"7FE00010":{"vr":"OB","BulkDataURI":"https://example.test/pixel"}}"#)
856                .unwrap_err();
857        assert!(err.to_string().contains("BulkDataURI"));
858    }
859}