1# Copyright 2008-2018 pydicom authors. See LICENSE file for details.
2# -*- coding: utf-8 -*-
3"""Access dicom dictionary information"""
4
5from typing import Tuple, Optional, Dict
6
7from pydicom.config import logger
8from pydicom.tag import Tag, BaseTag, TagType
9
10# the actual dict of {tag: (VR, VM, name, is_retired, keyword), ...}
11from pydicom._dicom_dict import DicomDictionary
12
13# those with tags like "(50xx, 0005)"
14from pydicom._dicom_dict import RepeatersDictionary
15from pydicom._private_dict import private_dictionaries
16
17
18# Generate mask dict for checking repeating groups etc.
19# Map a true bitwise mask to the DICOM mask with "x"'s in it.
20masks: Dict[str, Tuple[int, int]] = {}
21for mask_x in RepeatersDictionary:
22    # mask1 is XOR'd to see that all non-"x" bits
23    # are identical (XOR result = 0 if bits same)
24    # then AND those out with 0 bits at the "x"
25    # ("we don't care") location using mask2
26    mask1 = int(mask_x.replace("x", "0"), 16)
27    mask2 = int("".join(["F0"[c == "x"] for c in mask_x]), 16)
28    masks[mask_x] = (mask1, mask2)
29
30
31def mask_match(tag: int) -> Optional[str]:
32    """Return the repeaters tag mask for `tag`.
33
34    Parameters
35    ----------
36    tag : int
37        The tag to check.
38
39    Returns
40    -------
41    str or None
42        If the tag is in the repeaters dictionary then returns the
43        corresponding masked tag, otherwise returns ``None``.
44    """
45    for mask_x, (mask1, mask2) in masks.items():
46        if (tag ^ mask1) & mask2 == 0:
47            return mask_x
48    return None
49
50
51def add_dict_entry(
52    tag: int,
53    VR: str,
54    keyword: str,
55    description: str,
56    VM: str = '1',
57    is_retired: str = ''
58) -> None:
59    """Update the DICOM dictionary with a new non-private entry.
60
61    Parameters
62    ----------
63    tag : int
64        The tag number for the new dictionary entry.
65    VR : str
66        DICOM value representation.
67    description : str
68        The descriptive name used in printing the entry. Often the same as the
69        keyword, but with spaces between words.
70    VM : str, optional
71        DICOM value multiplicity. If not specified, then ``'1'`` is used.
72    is_retired : str, optional
73        Usually leave as blank string (default). Set to ``'Retired'`` if is a
74        retired data element.
75
76    Raises
77    ------
78    ValueError
79        If the tag is a private tag.
80
81    Notes
82    -----
83    Does not permanently update the dictionary, but only during run-time.
84    Will replace an existing entry if the tag already exists in the dictionary.
85
86    See Also
87    --------
88    pydicom.examples.add_dict_entry
89        Example file which shows how to use this function
90    add_dict_entries
91        Update multiple values at once.
92
93    Examples
94    --------
95
96    >>> from pydicom import Dataset
97    >>> add_dict_entry(0x10021001, "UL", "TestOne", "Test One")
98    >>> add_dict_entry(0x10021002, "DS", "TestTwo", "Test Two", VM='3')
99    >>> ds = Dataset()
100    >>> ds.TestOne = 'test'
101    >>> ds.TestTwo = ['1', '2', '3']
102
103    """
104    add_dict_entries({tag: (VR, VM, description, is_retired, keyword)})
105
106
107def add_dict_entries(
108    new_entries_dict: Dict[int, Tuple[str, str, str, str, str]]
109) -> None:
110    """Update the DICOM dictionary with new non-private entries.
111
112    Parameters
113    ----------
114    new_entries_dict : dict
115        :class:`dict` of form:
116        ``{tag: (VR, VM, description, is_retired, keyword), ...}``
117        where parameters are as described in :func:`add_dict_entry`.
118
119    Raises
120    ------
121    ValueError
122        If one of the entries is a private tag.
123
124    See Also
125    --------
126    add_dict_entry
127        Add a single entry to the dictionary.
128
129    Examples
130    --------
131
132    >>> from pydicom import Dataset
133    >>> new_dict_items = {
134    ...        0x10021001: ('UL', '1', "Test One", '', 'TestOne'),
135    ...        0x10021002: ('DS', '3', "Test Two", '', 'TestTwo'),
136    ... }
137    >>> add_dict_entries(new_dict_items)
138    >>> ds = Dataset()
139    >>> ds.TestOne = 'test'
140    >>> ds.TestTwo = ['1', '2', '3']
141
142    """
143
144    if any([BaseTag(tag).is_private for tag in new_entries_dict]):
145        raise ValueError(
146            'Private tags cannot be added using "add_dict_entries" - '
147            'use "add_private_dict_entries" instead')
148
149    # Update the dictionary itself
150    DicomDictionary.update(new_entries_dict)
151
152    # Update the reverse mapping from name to tag
153    keyword_dict.update({val[4]: tag for tag, val in new_entries_dict.items()})
154
155
156def add_private_dict_entry(
157    private_creator: str, tag: int, VR: str, description: str, VM: str = '1'
158) -> None:
159    """Update the private DICOM dictionary with a new entry.
160
161    .. versionadded:: 1.3
162
163    Parameters
164    ----------
165    private_creator : str
166        The private creator for the new entry.
167    tag : int
168        The tag number for the new dictionary entry. Note that the
169        2 high bytes of the element part of the tag are ignored.
170    VR : str
171        DICOM value representation.
172    description : str
173        The descriptive name used in printing the entry.
174    VM : str, optional
175        DICOM value multiplicity. If not specified, then ``'1'`` is used.
176
177    Raises
178    ------
179    ValueError
180        If the tag is a non-private tag.
181
182    Notes
183    -----
184    Behaves like :func:`add_dict_entry`, only for a private tag entry.
185
186    See Also
187    --------
188    add_private_dict_entries
189        Add or update multiple entries at once.
190    """
191    new_dict_val = (VR, VM, description, '')
192    add_private_dict_entries(private_creator, {tag: new_dict_val})
193
194
195def add_private_dict_entries(
196    private_creator: str,
197    new_entries_dict: Dict[int, Tuple[str, str, str, str]]
198) -> None:
199    """Update pydicom's private DICOM tag dictionary with new entries.
200
201    .. versionadded:: 1.3
202
203    Parameters
204    ----------
205    private_creator: str
206        The private creator for all entries in `new_entries_dict`.
207    new_entries_dict : dict
208        :class:`dict` of form ``{tag: (VR, VM, description, is_retired), ...}``
209        where parameters are as described in :func:`add_private_dict_entry`.
210
211    Raises
212    ------
213    ValueError
214        If one of the entries is a non-private tag.
215
216    See Also
217    --------
218    add_private_dict_entry
219        Function to add a single entry to the private tag dictionary.
220
221    Examples
222    --------
223    >>> new_dict_items = {
224    ...        0x00410001: ('UL', '1', "Test One"),
225    ...        0x00410002: ('DS', '3', "Test Two", '3'),
226    ... }
227    >>> add_private_dict_entries("ACME LTD 1.2", new_dict_items)
228    >>> add_private_dict_entry("ACME LTD 1.3", 0x00410001, "US", "Test Three")
229    """
230
231    if not all([BaseTag(tag).is_private for tag in new_entries_dict]):
232        raise ValueError(
233            "Non-private tags cannot be added using "
234            "'add_private_dict_entries()' - use 'add_dict_entries()' instead"
235        )
236
237    new_entries = {
238        f"{tag >> 16:04x}xx{tag & 0xff:02x}": value
239        for tag, value in new_entries_dict.items()
240    }
241    private_dictionaries.setdefault(private_creator, {}).update(new_entries)
242
243
244def get_entry(tag: TagType) -> Tuple[str, str, str, str, str]:
245    """Return an entry from the DICOM dictionary as a tuple.
246
247    If the `tag` is not in the main DICOM dictionary, then the repeating
248    group dictionary will also be checked.
249
250    Parameters
251    ----------
252    tag : int
253        The tag for the element whose entry is to be retrieved. Only entries
254        in the official DICOM dictionary will be checked, not entries in the
255        private dictionary.
256
257    Returns
258    -------
259    tuple of str
260        The (VR, VM, name, is_retired, keyword) from the DICOM dictionary.
261
262    Raises
263    ------
264    KeyError
265        If the tag is not present in the DICOM data dictionary.
266
267    See Also
268    --------
269    get_private_entry
270        Return an entry from the private dictionary.
271    """
272    # Note: tried the lookup with 'if tag in DicomDictionary'
273    # and with DicomDictionary.get, instead of try/except
274    # Try/except was fastest using timeit if tag is valid (usual case)
275    # My test had 5.2 usec vs 8.2 for 'contains' test, vs 5.32 for dict.get
276    if not isinstance(tag, BaseTag):
277        tag = Tag(tag)
278    try:
279        return DicomDictionary[tag]
280    except KeyError:
281        if not tag.is_private:
282            mask_x = mask_match(tag)
283            if mask_x:
284                return RepeatersDictionary[mask_x]
285        raise KeyError(f"Tag {tag} not found in DICOM dictionary")
286
287
288def dictionary_is_retired(tag: TagType) -> bool:
289    """Return ``True`` if the element corresponding to `tag` is retired.
290
291    Only performs the lookup for official DICOM elements.
292
293    Parameters
294    ----------
295    tag : int
296        The tag for the element whose retirement status is being checked.
297
298    Returns
299    -------
300    bool
301        ``True`` if the element's retirement status is 'Retired', ``False``
302        otherwise.
303
304    Raises
305    ------
306    KeyError
307        If the tag is not present in the DICOM data dictionary.
308    """
309    if 'retired' in get_entry(tag)[3].lower():
310        return True
311    return False
312
313
314def dictionary_VR(tag: TagType) -> str:
315    """Return the VR of the element corresponding to `tag`.
316
317    Only performs the lookup for official DICOM elements.
318
319    Parameters
320    ----------
321    tag : int
322        The tag for the element whose value representation (VR) is being
323        retrieved.
324
325    Returns
326    -------
327    str
328        The VR of the corresponding element.
329
330    Raises
331    ------
332    KeyError
333        If the tag is not present in the DICOM data dictionary.
334    """
335    return get_entry(tag)[0]
336
337
338def dictionary_VM(tag: TagType) -> str:
339    """Return the VM of the element corresponding to `tag`.
340
341    Only performs the lookup for official DICOM elements.
342
343    Parameters
344    ----------
345    tag : int
346        The tag for the element whose value multiplicity (VM) is being
347        retrieved.
348
349    Returns
350    -------
351    str
352        The VM of the corresponding element.
353
354    Raises
355    ------
356    KeyError
357        If the tag is not present in the DICOM data dictionary.
358    """
359    return get_entry(tag)[1]
360
361
362def dictionary_description(tag: TagType) -> str:
363    """Return the description of the element corresponding to `tag`.
364
365    Only performs the lookup for official DICOM elements.
366
367    Parameters
368    ----------
369    tag : int
370        The tag for the element whose description is being retrieved.
371
372    Returns
373    -------
374    str
375        The description of the corresponding element.
376
377    Raises
378    ------
379    KeyError
380        If the tag is not present in the DICOM data dictionary.
381    """
382    return get_entry(tag)[2]
383
384
385def dictionary_keyword(tag: TagType) -> str:
386    """Return the keyword of the element corresponding to `tag`.
387
388    Only performs the lookup for official DICOM elements.
389
390    Parameters
391    ----------
392    tag : int
393        The tag for the element whose keyword is being retrieved.
394
395    Returns
396    -------
397    str
398        The keyword of the corresponding element.
399
400    Raises
401    ------
402    KeyError
403        If the tag is not present in the DICOM data dictionary.
404    """
405    return get_entry(tag)[4]
406
407
408def dictionary_has_tag(tag: TagType) -> bool:
409    """Return ``True`` if `tag` is in the official DICOM data dictionary.
410
411    Parameters
412    ----------
413    tag : int
414        The tag to check.
415
416    Returns
417    -------
418    bool
419        ``True`` if the tag corresponds to an element present in the official
420        DICOM data dictionary, ``False`` otherwise.
421    """
422    return (tag in DicomDictionary)
423
424
425def keyword_for_tag(tag: TagType) -> str:
426    """Return the keyword of the element corresponding to `tag`.
427
428    Parameters
429    ----------
430    tag : int
431        The tag for the element whose keyword is being retrieved.
432
433    Returns
434    -------
435    str
436        If the element is in the DICOM data dictionary then returns the
437        corresponding element's keyword, otherwise returns ``''``. For
438        group length elements will always return ``'GroupLength'``.
439    """
440    try:
441        return dictionary_keyword(tag)
442    except KeyError:
443        return ""
444
445
446# Provide for the 'reverse' lookup. Given the keyword, what is the tag?
447keyword_dict: Dict[str, int] = {
448    dictionary_keyword(tag): tag for tag in DicomDictionary
449}
450
451
452def tag_for_keyword(keyword: str) -> Optional[int]:
453    """Return the tag of the element corresponding to `keyword`.
454
455    Only performs the lookup for official DICOM elements.
456
457    Parameters
458    ----------
459    keyword : str
460        The keyword for the element whose tag is being retrieved.
461
462    Returns
463    -------
464    int or None
465        If the element is in the DICOM data dictionary then returns the
466        corresponding element's tag, otherwise returns ``None``.
467    """
468    return keyword_dict.get(keyword)
469
470
471def repeater_has_tag(tag: int) -> bool:
472    """Return ``True`` if `tag` is in the DICOM repeaters data dictionary.
473
474    Parameters
475    ----------
476    tag : int
477        The tag to check.
478
479    Returns
480    -------
481    bool
482        ``True`` if the tag is a non-private element tag present in the
483        official DICOM repeaters data dictionary, ``False`` otherwise.
484    """
485    return (mask_match(tag) in RepeatersDictionary)
486
487
488REPEATER_KEYWORDS = [val[4] for val in RepeatersDictionary.values()]
489
490
491def repeater_has_keyword(keyword: str) -> bool:
492    """Return ``True`` if `keyword` is in the DICOM repeaters data dictionary.
493
494    Parameters
495    ----------
496    keyword : str
497        The keyword to check.
498
499    Returns
500    -------
501    bool
502        ``True`` if the keyword corresponding to an element present in the
503        official DICOM repeaters data dictionary, ``False`` otherwise.
504    """
505    return keyword in REPEATER_KEYWORDS
506
507
508# PRIVATE DICTIONARY handling
509# functions in analogy with those of main DICOM dict
510def get_private_entry(
511    tag: TagType, private_creator: str
512) -> Tuple[str, str, str, str]:
513    """Return an entry from the private dictionary corresponding to `tag`.
514
515    Parameters
516    ----------
517    tag : int
518        The tag for the element whose entry is to be retrieved. Only entries
519        in the private dictionary will be checked.
520    private_creator : str
521        The name of the private creator.
522
523    Returns
524    -------
525    tuple of str
526        The (VR, VM, name, is_retired) from the private dictionary.
527
528    Raises
529    ------
530    KeyError
531        If the tag or private creator is not present in the private dictionary.
532
533    See Also
534    --------
535    get_entry
536        Return an entry from the DICOM data dictionary.
537    """
538    if not isinstance(tag, BaseTag):
539        tag = Tag(tag)
540
541    try:
542        private_dict = private_dictionaries[private_creator]
543    except KeyError as exc:
544        raise KeyError(
545            f"Private creator '{private_creator}' not in the private "
546            "dictionary"
547        ) from exc
548
549    # private elements are usually agnostic for
550    # "block" (see PS3.5-2008 7.8.1 p44)
551    # Some elements in _private_dict are explicit;
552    # most have "xx" for high-byte of element
553    #  so here put in the "xx" in the block position for key to look up
554    group_str = f"{tag.group:04x}"
555    elem_str = f"{tag.elem:04x}"
556    keys = [
557        f"{group_str}{elem_str}",
558        f"{group_str}xx{elem_str[-2:]}",
559        f"{group_str[:2]}xxxx{elem_str[-2:]}"
560    ]
561    keys = [k for k in keys if k in private_dict]
562    if not keys:
563        raise KeyError(
564            f"Tag '{tag}' not in private dictionary "
565            f"for private creator '{private_creator}'"
566        )
567    dict_entry = private_dict[keys[0]]
568
569    return dict_entry
570
571
572def private_dictionary_VR(tag: TagType, private_creator: str) -> str:
573    """Return the VR of the private element corresponding to `tag`.
574
575    Parameters
576    ----------
577    tag : int
578        The tag for the element whose value representation (VR) is being
579        retrieved.
580    private_creator : str
581        The name of the private creator.
582
583    Returns
584    -------
585    str
586        The VR of the corresponding element.
587
588    Raises
589    ------
590    KeyError
591        If the tag is not present in the private dictionary.
592    """
593    return get_private_entry(tag, private_creator)[0]
594
595
596def private_dictionary_VM(tag: TagType, private_creator: str) -> str:
597    """Return the VM of the private element corresponding to `tag`.
598
599    Parameters
600    ----------
601    tag : int
602        The tag for the element whose value multiplicity (VM) is being
603        retrieved.
604    private_creator : str
605        The name of the private creator.
606
607    Returns
608    -------
609    str
610        The VM of the corresponding element.
611
612    Raises
613    ------
614    KeyError
615        If the tag is not present in the private dictionary.
616    """
617    return get_private_entry(tag, private_creator)[1]
618
619
620def private_dictionary_description(tag: TagType, private_creator: str) -> str:
621    """Return the description of the private element corresponding to `tag`.
622
623    Parameters
624    ----------
625    tag : int
626        The tag for the element whose description is being retrieved.
627    private_creator : str
628        The name of the private createor.
629
630    Returns
631    -------
632    str
633        The description of the corresponding element.
634
635    Raises
636    ------
637    KeyError
638        If the tag is not present in the private dictionary.
639    """
640    return get_private_entry(tag, private_creator)[2]
641