1# Copyright 2008-2018 pydicom authors. See LICENSE file for details. 2"""Define the Dataset and FileDataset classes. 3 4The Dataset class represents the DICOM Dataset while the FileDataset class 5adds extra functionality to Dataset when data is read from or written to file. 6 7Overview of DICOM object model 8------------------------------ 9Dataset (dict subclass) 10 Contains DataElement instances, each of which has a tag, VR, VM and value. 11 The DataElement value can be: 12 * A single value, such as a number, string, etc. (i.e. VM = 1) 13 * A list of numbers, strings, etc. (i.e. VM > 1) 14 * A Sequence (list subclass), where each item is a Dataset which 15 contains its own DataElements, and so on in a recursive manner. 16""" 17import copy 18from bisect import bisect_left 19import io 20from importlib.util import find_spec as have_package 21import inspect # for __dir__ 22from itertools import takewhile 23import json 24import os 25import os.path 26import re 27from types import TracebackType 28from typing import ( 29 Optional, Tuple, Union, List, Any, cast, Dict, ValuesView, 30 Iterator, BinaryIO, AnyStr, Callable, TypeVar, Type, overload, 31 MutableSequence, MutableMapping, AbstractSet 32) 33import warnings 34import weakref 35 36try: 37 import numpy 38except ImportError: 39 pass 40 41import pydicom # for dcmwrite 42import pydicom.charset 43import pydicom.config 44from pydicom import jsonrep, config 45from pydicom._version import __version_info__ 46from pydicom.charset import default_encoding, convert_encodings 47from pydicom.config import logger 48from pydicom.datadict import ( 49 dictionary_VR, tag_for_keyword, keyword_for_tag, repeater_has_keyword 50) 51from pydicom.dataelem import DataElement, DataElement_from_raw, RawDataElement 52from pydicom.encaps import encapsulate, encapsulate_extended 53from pydicom.fileutil import path_from_pathlike 54from pydicom.pixel_data_handlers.util import ( 55 convert_color_space, reshape_pixel_array, get_image_pixel_ids 56) 57from pydicom.tag import Tag, BaseTag, tag_in_exception, TagType 58from pydicom.uid import ( 59 ExplicitVRLittleEndian, ImplicitVRLittleEndian, ExplicitVRBigEndian, 60 RLELossless, PYDICOM_IMPLEMENTATION_UID, UID 61) 62from pydicom.waveforms import numpy_handler as wave_handler 63 64 65class PrivateBlock: 66 """Helper class for a private block in the :class:`Dataset`. 67 68 .. versionadded:: 1.3 69 70 See the DICOM Standard, Part 5, 71 :dcm:`Section 7.8.1<part05/sect_7.8.html#sect_7.8.1>` - Private Data 72 Element Tags 73 74 Attributes 75 ---------- 76 group : int 77 The private group where the private block is located as a 32-bit 78 :class:`int`. 79 private_creator : str 80 The private creator string related to the block. 81 dataset : Dataset 82 The parent dataset. 83 block_start : int 84 The start element of the private block as a 32-bit :class:`int`. Note 85 that the 2 low order hex digits of the element are always 0. 86 """ 87 88 def __init__( 89 self, 90 key: Tuple[int, str], 91 dataset: "Dataset", 92 private_creator_element: int 93 ) -> None: 94 """Initializes an object corresponding to a private tag block. 95 96 Parameters 97 ---------- 98 key : tuple 99 The private (tag group, creator) as ``(int, str)``. The group 100 must be an odd number. 101 dataset : Dataset 102 The parent :class:`Dataset`. 103 private_creator_element : int 104 The element of the private creator tag as a 32-bit :class:`int`. 105 """ 106 self.group = key[0] 107 self.private_creator = key[1] 108 self.dataset = dataset 109 self.block_start = private_creator_element << 8 110 111 def get_tag(self, element_offset: int) -> BaseTag: 112 """Return the private tag ID for the given `element_offset`. 113 114 Parameters 115 ---------- 116 element_offset : int 117 The lower 16 bits (e.g. 2 hex numbers) of the element tag. 118 119 Returns 120 ------- 121 The tag ID defined by the private block location and the 122 given element offset. 123 124 Raises 125 ------ 126 ValueError 127 If `element_offset` is too large. 128 """ 129 if element_offset > 0xff: 130 raise ValueError('Element offset must be less than 256') 131 return Tag(self.group, self.block_start + element_offset) 132 133 def __contains__(self, element_offset: int) -> bool: 134 """Return ``True`` if the tag with given `element_offset` is in 135 the parent :class:`Dataset`. 136 """ 137 return self.get_tag(element_offset) in self.dataset 138 139 def __getitem__(self, element_offset: int) -> DataElement: 140 """Return the data element in the parent dataset for the given element 141 offset. 142 143 Parameters 144 ---------- 145 element_offset : int 146 The lower 16 bits (e.g. 2 hex numbers) of the element tag. 147 148 Returns 149 ------- 150 The data element of the tag in the parent dataset defined by the 151 private block location and the given element offset. 152 153 Raises 154 ------ 155 ValueError 156 If `element_offset` is too large. 157 KeyError 158 If no data element exists at that offset. 159 """ 160 return self.dataset.__getitem__(self.get_tag(element_offset)) 161 162 def __delitem__(self, element_offset: int) -> None: 163 """Delete the tag with the given `element_offset` from the dataset. 164 165 Parameters 166 ---------- 167 element_offset : int 168 The lower 16 bits (e.g. 2 hex numbers) of the element tag 169 to be deleted. 170 171 Raises 172 ------ 173 ValueError 174 If `element_offset` is too large. 175 KeyError 176 If no data element exists at that offset. 177 """ 178 del self.dataset[self.get_tag(element_offset)] 179 180 def add_new(self, element_offset: int, VR: str, value: object) -> None: 181 """Add a private element to the parent :class:`Dataset`. 182 183 Adds the private tag with the given `VR` and `value` to the parent 184 :class:`Dataset` at the tag ID defined by the private block and the 185 given `element_offset`. 186 187 Parameters 188 ---------- 189 element_offset : int 190 The lower 16 bits (e.g. 2 hex numbers) of the element tag 191 to be added. 192 VR : str 193 The 2 character DICOM value representation. 194 value 195 The value of the data element. See :meth:`Dataset.add_new()` 196 for a description. 197 """ 198 tag = self.get_tag(element_offset) 199 self.dataset.add_new(tag, VR, value) 200 self.dataset[tag].private_creator = self.private_creator 201 202 203def _dict_equal( 204 a: "Dataset", b: Any, exclude: Optional[List[str]] = None 205) -> bool: 206 """Common method for Dataset.__eq__ and FileDataset.__eq__ 207 208 Uses .keys() as needed because Dataset iter return items not keys 209 `exclude` is used in FileDataset__eq__ ds.__dict__ compare, which 210 would also compare the wrapped _dict member (entire dataset) again. 211 """ 212 return (len(a) == len(b) and 213 all(key in b for key in a.keys()) and 214 all(a[key] == b[key] for key in a.keys() 215 if exclude is None or key not in exclude) 216 ) 217 218 219_DatasetValue = Union[DataElement, RawDataElement] 220_DatasetType = Union["Dataset", MutableMapping[BaseTag, _DatasetValue]] 221 222 223class Dataset: 224 """A DICOM dataset as a mutable mapping of DICOM Data Elements. 225 226 Examples 227 -------- 228 Add an element to the :class:`Dataset` (for elements in the DICOM 229 dictionary): 230 231 >>> ds = Dataset() 232 >>> ds.PatientName = "CITIZEN^Joan" 233 >>> ds.add_new(0x00100020, 'LO', '12345') 234 >>> ds[0x0010, 0x0030] = DataElement(0x00100030, 'DA', '20010101') 235 236 Add a sequence element to the :class:`Dataset` 237 238 >>> ds.BeamSequence = [Dataset(), Dataset(), Dataset()] 239 >>> ds.BeamSequence[0].Manufacturer = "Linac, co." 240 >>> ds.BeamSequence[1].Manufacturer = "Linac and Sons, co." 241 >>> ds.BeamSequence[2].Manufacturer = "Linac and Daughters, co." 242 243 Add private elements to the :class:`Dataset` 244 245 >>> block = ds.private_block(0x0041, 'My Creator', create=True) 246 >>> block.add_new(0x01, 'LO', '12345') 247 248 Updating and retrieving element values: 249 250 >>> ds.PatientName = "CITIZEN^Joan" 251 >>> ds.PatientName 252 'CITIZEN^Joan' 253 >>> ds.PatientName = "CITIZEN^John" 254 >>> ds.PatientName 255 'CITIZEN^John' 256 257 Retrieving an element's value from a Sequence: 258 259 >>> ds.BeamSequence[0].Manufacturer 260 'Linac, co.' 261 >>> ds.BeamSequence[1].Manufacturer 262 'Linac and Sons, co.' 263 264 Accessing the :class:`~pydicom.dataelem.DataElement` items: 265 266 >>> elem = ds['PatientName'] 267 >>> elem 268 (0010, 0010) Patient's Name PN: 'CITIZEN^John' 269 >>> elem = ds[0x00100010] 270 >>> elem 271 (0010, 0010) Patient's Name PN: 'CITIZEN^John' 272 >>> elem = ds.data_element('PatientName') 273 >>> elem 274 (0010, 0010) Patient's Name PN: 'CITIZEN^John' 275 276 Accessing a private :class:`~pydicom.dataelem.DataElement` 277 item: 278 279 >>> block = ds.private_block(0x0041, 'My Creator') 280 >>> elem = block[0x01] 281 >>> elem 282 (0041, 1001) Private tag data LO: '12345' 283 >>> elem.value 284 '12345' 285 286 Alternatively: 287 288 >>> ds.get_private_item(0x0041, 0x01, 'My Creator').value 289 '12345' 290 291 Deleting an element from the :class:`Dataset` 292 293 >>> del ds.PatientID 294 >>> del ds.BeamSequence[1].Manufacturer 295 >>> del ds.BeamSequence[2] 296 297 Deleting a private element from the :class:`Dataset` 298 299 >>> block = ds.private_block(0x0041, 'My Creator') 300 >>> if 0x01 in block: 301 ... del block[0x01] 302 303 Determining if an element is present in the :class:`Dataset` 304 305 >>> 'PatientName' in ds 306 True 307 >>> 'PatientID' in ds 308 False 309 >>> (0x0010, 0x0030) in ds 310 True 311 >>> 'Manufacturer' in ds.BeamSequence[0] 312 True 313 314 Iterating through the top level of a :class:`Dataset` only (excluding 315 Sequences): 316 317 >>> for elem in ds: 318 ... print(elem) 319 (0010, 0010) Patient's Name PN: 'CITIZEN^John' 320 321 Iterating through the entire :class:`Dataset` (including Sequences): 322 323 >>> for elem in ds.iterall(): 324 ... print(elem) 325 (0010, 0010) Patient's Name PN: 'CITIZEN^John' 326 327 Recursively iterate through a :class:`Dataset` (including Sequences): 328 329 >>> def recurse(ds): 330 ... for elem in ds: 331 ... if elem.VR == 'SQ': 332 ... [recurse(item) for item in elem.value] 333 ... else: 334 ... # Do something useful with each DataElement 335 336 Converting the :class:`Dataset` to and from JSON: 337 338 >>> ds = Dataset() 339 >>> ds.PatientName = "Some^Name" 340 >>> jsonmodel = ds.to_json() 341 >>> ds2 = Dataset() 342 >>> ds2.from_json(jsonmodel) 343 (0010, 0010) Patient's Name PN: 'Some^Name' 344 345 Attributes 346 ---------- 347 default_element_format : str 348 The default formatting for string display. 349 default_sequence_element_format : str 350 The default formatting for string display of sequences. 351 indent_chars : str 352 For string display, the characters used to indent nested Sequences. 353 Default is ``" "``. 354 is_little_endian : bool 355 Shall be set before writing with ``write_like_original=False``. 356 The :class:`Dataset` (excluding the pixel data) will be written using 357 the given endianness. 358 is_implicit_VR : bool 359 Shall be set before writing with ``write_like_original=False``. 360 The :class:`Dataset` will be written using the transfer syntax with 361 the given VR handling, e.g *Little Endian Implicit VR* if ``True``, 362 and *Little Endian Explicit VR* or *Big Endian Explicit VR* (depending 363 on ``Dataset.is_little_endian``) if ``False``. 364 """ 365 indent_chars = " " 366 367 def __init__(self, *args: _DatasetType, **kwargs: Any) -> None: 368 """Create a new :class:`Dataset` instance.""" 369 self._parent_encoding: List[str] = kwargs.get( 370 'parent_encoding', default_encoding 371 ) 372 373 self._dict: MutableMapping[BaseTag, _DatasetValue] 374 if not args: 375 self._dict = {} 376 elif isinstance(args[0], Dataset): 377 self._dict = args[0]._dict 378 else: 379 self._dict = args[0] 380 381 self.is_decompressed = False 382 383 # the following read_XXX attributes are used internally to store 384 # the properties of the dataset after read from a file 385 # set depending on the endianness of the read dataset 386 self.read_little_endian: Optional[bool] = None 387 # set depending on the VR handling of the read dataset 388 self.read_implicit_vr: Optional[bool] = None 389 # The dataset's original character set encoding 390 self.read_encoding: Union[None, str, MutableSequence[str]] = None 391 392 self.is_little_endian: Optional[bool] = None 393 self.is_implicit_VR: Optional[bool] = None 394 395 # the parent data set, if this dataset is a sequence item 396 self.parent: "Optional[weakref.ReferenceType[Dataset]]" = None 397 398 # known private creator blocks 399 self._private_blocks: Dict[Tuple[int, str], PrivateBlock] = {} 400 401 self._pixel_array: Optional["numpy.ndarray"] = None 402 self._pixel_id: Dict[str, int] = {} 403 404 self.file_meta: FileMetaDataset 405 406 def __enter__(self) -> "Dataset": 407 """Method invoked on entry to a with statement.""" 408 return self 409 410 def __exit__( 411 self, 412 exc_type: Optional[Type[BaseException]], 413 exc_val: Optional[BaseException], 414 exc_tb: Optional[TracebackType] 415 ) -> Optional[bool]: 416 """Method invoked on exit from a with statement.""" 417 # Returning anything other than True will re-raise any exceptions 418 return None 419 420 def add(self, data_element: DataElement) -> None: 421 """Add an element to the :class:`Dataset`. 422 423 Equivalent to ``ds[data_element.tag] = data_element`` 424 425 Parameters 426 ---------- 427 data_element : dataelem.DataElement 428 The :class:`~pydicom.dataelem.DataElement` to add. 429 """ 430 self[data_element.tag] = data_element 431 432 def add_new(self, tag: TagType, VR: str, value: Any) -> None: 433 """Create a new element and add it to the :class:`Dataset`. 434 435 Parameters 436 ---------- 437 tag 438 The DICOM (group, element) tag in any form accepted by 439 :func:`~pydicom.tag.Tag` such as ``[0x0010, 0x0010]``, 440 ``(0x10, 0x10)``, ``0x00100010``, etc. 441 VR : str 442 The 2 character DICOM value representation (see DICOM Standard, 443 Part 5, :dcm:`Section 6.2<part05/sect_6.2.html>`). 444 value 445 The value of the data element. One of the following: 446 447 * a single string or number 448 * a :class:`list` or :class:`tuple` with all strings or all numbers 449 * a multi-value string with backslash separator 450 * for a sequence element, an empty :class:`list` or ``list`` of 451 :class:`Dataset` 452 """ 453 454 data_element = DataElement(tag, VR, value) 455 # use data_element.tag since DataElement verified it 456 self._dict[data_element.tag] = data_element 457 458 def __array__(self) -> "numpy.ndarray": 459 """Support accessing the dataset from a numpy array.""" 460 return numpy.asarray(self._dict) 461 462 def data_element(self, name: str) -> Optional[DataElement]: 463 """Return the element corresponding to the element keyword `name`. 464 465 Parameters 466 ---------- 467 name : str 468 A DICOM element keyword. 469 470 Returns 471 ------- 472 dataelem.DataElement or None 473 For the given DICOM element `keyword`, return the corresponding 474 :class:`~pydicom.dataelem.DataElement` if present, ``None`` 475 otherwise. 476 """ 477 tag = tag_for_keyword(name) 478 # Test against None as (0000,0000) is a possible tag 479 if tag is not None: 480 return self[tag] 481 return None 482 483 def __contains__(self, name: TagType) -> bool: 484 """Simulate dict.__contains__() to handle DICOM keywords. 485 486 Examples 487 -------- 488 489 >>> ds = Dataset() 490 >>> ds.SliceLocation = '2' 491 >>> 'SliceLocation' in ds 492 True 493 494 Parameters 495 ---------- 496 name : str or int or 2-tuple 497 The element keyword or tag to search for. 498 499 Returns 500 ------- 501 bool 502 ``True`` if the corresponding element is in the :class:`Dataset`, 503 ``False`` otherwise. 504 """ 505 try: 506 return Tag(name) in self._dict 507 except Exception as exc: 508 msg = ( 509 f"Invalid value '{name}' used with the 'in' operator: must be " 510 "an element tag as a 2-tuple or int, or an element keyword" 511 ) 512 if isinstance(exc, OverflowError): 513 msg = ( 514 "Invalid element tag value used with the 'in' operator: " 515 "tags have a maximum value of (0xFFFF, 0xFFFF)" 516 ) 517 518 if config.INVALID_KEY_BEHAVIOR == "WARN": 519 warnings.warn(msg) 520 elif config.INVALID_KEY_BEHAVIOR == "RAISE": 521 raise ValueError(msg) from exc 522 523 return False 524 525 def decode(self) -> None: 526 """Apply character set decoding to the elements in the 527 :class:`Dataset`. 528 529 See DICOM Standard, Part 5, 530 :dcm:`Section 6.1.1<part05/chapter_6.html#sect_6.1.1>`. 531 """ 532 # Find specific character set. 'ISO_IR 6' is default 533 # May be multi-valued, but let pydicom.charset handle all logic on that 534 dicom_character_set = self._character_set 535 536 # Shortcut to the decode function in pydicom.charset 537 decode_data_element = pydicom.charset.decode_element 538 539 # Callback for walk(), to decode the chr strings if necessary 540 # This simply calls the pydicom.charset.decode_element function 541 def decode_callback(ds: "Dataset", data_element: DataElement) -> None: 542 """Callback to decode `data_element`.""" 543 if data_element.VR == 'SQ': 544 for dset in data_element.value: 545 dset._parent_encoding = dicom_character_set 546 dset.decode() 547 else: 548 decode_data_element(data_element, dicom_character_set) 549 550 self.walk(decode_callback, recursive=False) 551 552 def copy(self) -> "Dataset": 553 """Return a shallow copy of the dataset.""" 554 return copy.copy(self) 555 556 def __delattr__(self, name: str) -> None: 557 """Intercept requests to delete an attribute by `name`. 558 559 Examples 560 -------- 561 562 >>> ds = Dataset() 563 >>> ds.PatientName = 'foo' 564 >>> ds.some_attribute = True 565 566 If `name` is a DICOM keyword - delete the corresponding 567 :class:`~pydicom.dataelem.DataElement` 568 569 >>> del ds.PatientName 570 >>> 'PatientName' in ds 571 False 572 573 If `name` is another attribute - delete it 574 575 >>> del ds.some_attribute 576 >>> hasattr(ds, 'some_attribute') 577 False 578 579 Parameters 580 ---------- 581 name : str 582 The keyword for the DICOM element or the class attribute to delete. 583 """ 584 # First check if a valid DICOM keyword and if we have that data element 585 tag = cast(BaseTag, tag_for_keyword(name)) 586 if tag is not None and tag in self._dict: 587 del self._dict[tag] 588 # If not a DICOM name in this dataset, check for regular instance name 589 # can't do delete directly, that will call __delattr__ again 590 elif name in self.__dict__: 591 del self.__dict__[name] 592 # Not found, raise an error in same style as python does 593 else: 594 raise AttributeError(name) 595 596 def __delitem__(self, key: Union[slice, BaseTag, TagType]) -> None: 597 """Intercept requests to delete an attribute by key. 598 599 Examples 600 -------- 601 Indexing using :class:`~pydicom.dataelem.DataElement` tag 602 603 >>> ds = Dataset() 604 >>> ds.CommandGroupLength = 100 605 >>> ds.PatientName = 'CITIZEN^Jan' 606 >>> del ds[0x00000000] 607 >>> ds 608 (0010, 0010) Patient's Name PN: 'CITIZEN^Jan' 609 610 Slicing using :class:`~pydicom.dataelem.DataElement` tag 611 612 >>> ds = Dataset() 613 >>> ds.CommandGroupLength = 100 614 >>> ds.SOPInstanceUID = '1.2.3' 615 >>> ds.PatientName = 'CITIZEN^Jan' 616 >>> del ds[:0x00100000] 617 >>> ds 618 (0010, 0010) Patient's Name PN: 'CITIZEN^Jan' 619 620 Parameters 621 ---------- 622 key 623 The key for the attribute to be deleted. If a ``slice`` is used 624 then the tags matching the slice conditions will be deleted. 625 """ 626 # If passed a slice, delete the corresponding DataElements 627 if isinstance(key, slice): 628 for tag in self._slice_dataset(key.start, key.stop, key.step): 629 del self._dict[tag] 630 # invalidate private blocks in case a private creator is 631 # deleted - will be re-created on next access 632 if self._private_blocks and BaseTag(tag).is_private_creator: 633 self._private_blocks = {} 634 elif isinstance(key, BaseTag): 635 del self._dict[key] 636 if self._private_blocks and key.is_private_creator: 637 self._private_blocks = {} 638 else: 639 # If not a standard tag, than convert to Tag and try again 640 tag = Tag(key) 641 del self._dict[tag] 642 if self._private_blocks and tag.is_private_creator: 643 self._private_blocks = {} 644 645 def __dir__(self) -> List[str]: 646 """Give a list of attributes available in the :class:`Dataset`. 647 648 List of attributes is used, for example, in auto-completion in editors 649 or command-line environments. 650 """ 651 # Force zip object into a list 652 meths = set(list(zip( 653 *inspect.getmembers(self.__class__, inspect.isroutine)))[0]) 654 props = set(list(zip( 655 *inspect.getmembers(self.__class__, inspect.isdatadescriptor)))[0]) 656 dicom_names = set(self.dir()) 657 alldir = sorted(props | meths | dicom_names) 658 return alldir 659 660 def dir(self, *filters: str) -> List[str]: 661 """Return an alphabetical list of element keywords in the 662 :class:`Dataset`. 663 664 Intended mainly for use in interactive Python sessions. Only lists the 665 element keywords in the current level of the :class:`Dataset` (i.e. 666 the contents of any sequence elements are ignored). 667 668 Parameters 669 ---------- 670 filters : str 671 Zero or more string arguments to the function. Used for 672 case-insensitive match to any part of the DICOM keyword. 673 674 Returns 675 ------- 676 list of str 677 The matching element keywords in the dataset. If no 678 filters are used then all element keywords are returned. 679 """ 680 allnames = [keyword_for_tag(tag) for tag in self._dict.keys()] 681 # remove blanks - tags without valid names (e.g. private tags) 682 allnames = [x for x in allnames if x] 683 # Store found names in a dict, so duplicate names appear only once 684 matches = {} 685 for filter_ in filters: 686 filter_ = filter_.lower() 687 match = [x for x in allnames if x.lower().find(filter_) != -1] 688 matches.update({x: 1 for x in match}) 689 690 if filters: 691 return sorted(matches.keys()) 692 693 return sorted(allnames) 694 695 def __eq__(self, other: Any) -> bool: 696 """Compare `self` and `other` for equality. 697 698 Returns 699 ------- 700 bool 701 The result if `self` and `other` are the same class 702 NotImplemented 703 If `other` is not the same class as `self` then returning 704 :class:`NotImplemented` delegates the result to 705 ``superclass.__eq__(subclass)``. 706 """ 707 # When comparing against self this will be faster 708 if other is self: 709 return True 710 711 if isinstance(other, self.__class__): 712 return _dict_equal(self, other) 713 714 return NotImplemented 715 716 @overload 717 def get(self, key: str, default: Optional[Any] = None) -> Any: 718 pass # pragma: no cover 719 720 @overload 721 def get( 722 self, 723 key: Union[int, Tuple[int, int], BaseTag], 724 default: Optional[Any] = None 725 ) -> DataElement: 726 pass # pragma: no cover 727 728 def get( 729 self, 730 key: Union[str, Union[int, Tuple[int, int], BaseTag]], 731 default: Optional[Any] = None 732 ) -> Union[Any, DataElement]: 733 """Simulate ``dict.get()`` to handle element tags and keywords. 734 735 Parameters 736 ---------- 737 key : str or int or Tuple[int, int] or BaseTag 738 The element keyword or tag or the class attribute name to get. 739 default : obj or None, optional 740 If the element or class attribute is not present, return 741 `default` (default ``None``). 742 743 Returns 744 ------- 745 value 746 If `key` is the keyword for an element in the :class:`Dataset` 747 then return the element's value. 748 dataelem.DataElement 749 If `key` is a tag for a element in the :class:`Dataset` then 750 return the :class:`~pydicom.dataelem.DataElement` 751 instance. 752 value 753 If `key` is a class attribute then return its value. 754 """ 755 if isinstance(key, str): 756 try: 757 return getattr(self, key) 758 except AttributeError: 759 return default 760 761 # is not a string, try to make it into a tag and then hand it 762 # off to the underlying dict 763 try: 764 key = Tag(key) 765 except Exception as exc: 766 raise TypeError("Dataset.get key must be a string or tag") from exc 767 768 try: 769 return self.__getitem__(key) 770 except KeyError: 771 return default 772 773 def items(self) -> AbstractSet[Tuple[BaseTag, _DatasetValue]]: 774 """Return the :class:`Dataset` items to simulate :meth:`dict.items`. 775 776 Returns 777 ------- 778 dict_items 779 The top-level (:class:`~pydicom.tag.BaseTag`, 780 :class:`~pydicom.dataelem.DataElement`) items for the 781 :class:`Dataset`. 782 """ 783 return self._dict.items() 784 785 def keys(self) -> AbstractSet[BaseTag]: 786 """Return the :class:`Dataset` keys to simulate :meth:`dict.keys`. 787 788 Returns 789 ------- 790 dict_keys 791 The :class:`~pydicom.tag.BaseTag` of all the elements in 792 the :class:`Dataset`. 793 """ 794 return self._dict.keys() 795 796 def values(self) -> ValuesView[_DatasetValue]: 797 """Return the :class:`Dataset` values to simulate :meth:`dict.values`. 798 799 Returns 800 ------- 801 dict_keys 802 The :class:`DataElements<pydicom.dataelem.DataElement>` that make 803 up the values of the :class:`Dataset`. 804 """ 805 return self._dict.values() 806 807 def __getattr__(self, name: str) -> Any: 808 """Intercept requests for :class:`Dataset` attribute names. 809 810 If `name` matches a DICOM keyword, return the value for the 811 element with the corresponding tag. 812 813 Parameters 814 ---------- 815 name : str 816 An element keyword or a class attribute name. 817 818 Returns 819 ------- 820 value 821 If `name` matches a DICOM keyword, returns the corresponding 822 element's value. Otherwise returns the class attribute's 823 value (if present). 824 """ 825 tag = tag_for_keyword(name) 826 if tag is not None: # `name` isn't a DICOM element keyword 827 tag = Tag(tag) 828 if tag in self._dict: # DICOM DataElement not in the Dataset 829 return self[tag].value 830 831 # no tag or tag not contained in the dataset 832 if name == '_dict': 833 # special handling for contained dict, needed for pickle 834 return {} 835 # Try the base class attribute getter (fix for issue 332) 836 return object.__getattribute__(self, name) 837 838 @property 839 def _character_set(self) -> List[str]: 840 """The character set used to encode text values.""" 841 char_set = self.get(BaseTag(0x00080005), None) 842 if not char_set: 843 return self._parent_encoding 844 845 return convert_encodings(char_set.value) 846 847 @overload 848 def __getitem__(self, key: slice) -> "Dataset": 849 pass # pragma: no cover 850 851 @overload 852 def __getitem__(self, key: TagType) -> DataElement: 853 pass # pragma: no cover 854 855 def __getitem__( 856 self, key: Union[slice, TagType] 857 ) -> Union["Dataset", DataElement]: 858 """Operator for ``Dataset[key]`` request. 859 860 Any deferred data elements will be read in and an attempt will be made 861 to correct any elements with ambiguous VRs. 862 863 Examples 864 -------- 865 Indexing using :class:`~pydicom.dataelem.DataElement` tag 866 867 >>> ds = Dataset() 868 >>> ds.SOPInstanceUID = '1.2.3' 869 >>> ds.PatientName = 'CITIZEN^Jan' 870 >>> ds.PatientID = '12345' 871 >>> ds[0x00100010].value 872 'CITIZEN^Jan' 873 874 Slicing using element tags; all group ``0x0010`` elements in 875 the dataset 876 877 >>> ds[0x00100000:0x00110000] 878 (0010, 0010) Patient's Name PN: 'CITIZEN^Jan' 879 (0010, 0020) Patient ID LO: '12345' 880 881 All group ``0x0002`` elements in the dataset 882 883 >>> ds[(0x0002, 0x0000):(0x0003, 0x0000)] 884 <BLANKLINE> 885 886 Parameters 887 ---------- 888 key 889 The DICOM (group, element) tag in any form accepted by 890 :func:`~pydicom.tag.Tag` such as ``[0x0010, 0x0010]``, 891 ``(0x10, 0x10)``, ``0x00100010``, etc. May also be a :class:`slice` 892 made up of DICOM tags. 893 894 Returns 895 ------- 896 dataelem.DataElement or Dataset 897 If a single DICOM element tag is used then returns the 898 corresponding :class:`~pydicom.dataelem.DataElement`. 899 If a :class:`slice` is used then returns a :class:`Dataset` object 900 containing the corresponding 901 :class:`DataElements<pydicom.dataelem.DataElement>`. 902 """ 903 # If passed a slice, return a Dataset containing the corresponding 904 # DataElements 905 if isinstance(key, slice): 906 return self._dataset_slice(key) 907 908 if isinstance(key, BaseTag): 909 tag = key 910 else: 911 try: 912 tag = Tag(key) 913 except Exception as exc: 914 raise KeyError(f"'{key}'") from exc 915 916 elem = self._dict[tag] 917 if isinstance(elem, DataElement): 918 if elem.VR == 'SQ' and elem.value: 919 # let a sequence know its parent dataset, as sequence items 920 # may need parent dataset tags to resolve ambiguous tags 921 elem.value.parent = self 922 return elem 923 924 if isinstance(elem, RawDataElement): 925 # If a deferred read, then go get the value now 926 if elem.value is None and elem.length != 0: 927 from pydicom.filereader import read_deferred_data_element 928 929 elem = read_deferred_data_element( 930 self.fileobj_type, 931 self.filename, 932 self.timestamp, 933 elem 934 ) 935 936 if tag != BaseTag(0x00080005): 937 character_set = self.read_encoding or self._character_set 938 else: 939 character_set = default_encoding 940 # Not converted from raw form read from file yet; do so now 941 self[tag] = DataElement_from_raw(elem, character_set, self) 942 943 # If the Element has an ambiguous VR, try to correct it 944 if 'or' in self[tag].VR: 945 from pydicom.filewriter import correct_ambiguous_vr_element 946 self[tag] = correct_ambiguous_vr_element( 947 self[tag], self, elem[6] 948 ) 949 950 return cast(DataElement, self._dict.get(tag)) 951 952 def private_block( 953 self, group: int, private_creator: str, create: bool = False 954 ) -> PrivateBlock: 955 """Return the block for the given tag `group` and `private_creator`. 956 957 .. versionadded:: 1.3 958 959 If `create` is ``True`` and the `private_creator` does not exist, 960 the private creator tag is added. 961 962 Notes 963 ----- 964 We ignore the unrealistic case that no free block is available. 965 966 Parameters 967 ---------- 968 group : int 969 The group of the private tag to be found as a 32-bit :class:`int`. 970 Must be an odd number (e.g. a private group). 971 private_creator : str 972 The private creator string associated with the tag. 973 create : bool, optional 974 If ``True`` and `private_creator` does not exist, a new private 975 creator tag is added at the next free block. If ``False`` 976 (the default) and `private_creator` does not exist, 977 :class:`KeyError` is raised instead. 978 979 Returns 980 ------- 981 PrivateBlock 982 The existing or newly created private block. 983 984 Raises 985 ------ 986 ValueError 987 If `group` doesn't belong to a private tag or `private_creator` 988 is empty. 989 KeyError 990 If the private creator tag is not found in the given group and 991 the `create` parameter is ``False``. 992 """ 993 def new_block(element: int) -> PrivateBlock: 994 block = PrivateBlock(key, self, element) 995 self._private_blocks[key] = block 996 return block 997 998 key = (group, private_creator) 999 if key in self._private_blocks: 1000 return self._private_blocks[key] 1001 1002 if not private_creator: 1003 raise ValueError('Private creator must have a value') 1004 1005 if group % 2 == 0: 1006 raise ValueError( 1007 'Tag must be private if private creator is given') 1008 1009 # find block with matching private creator 1010 block = self[(group, 0x10):(group, 0x100)] # type: ignore[misc] 1011 data_el = next( 1012 ( 1013 elem for elem in block if elem.value == private_creator 1014 ), 1015 None 1016 ) 1017 if data_el is not None: 1018 return new_block(data_el.tag.element) 1019 1020 if not create: 1021 # not found and shall not be created - raise 1022 raise KeyError( 1023 "Private creator '{}' not found".format(private_creator)) 1024 1025 # private creator not existing - find first unused private block 1026 # and add the private creator 1027 first_free_el = next( 1028 el for el in range(0x10, 0x100) 1029 if Tag(group, el) not in self._dict 1030 ) 1031 self.add_new(Tag(group, first_free_el), 'LO', private_creator) 1032 return new_block(first_free_el) 1033 1034 def private_creators(self, group: int) -> List[str]: 1035 """Return a list of private creator names in the given group. 1036 1037 .. versionadded:: 1.3 1038 1039 Examples 1040 -------- 1041 This can be used to check if a given private creator exists in 1042 the group of the dataset: 1043 1044 >>> ds = Dataset() 1045 >>> if 'My Creator' in ds.private_creators(0x0041): 1046 ... block = ds.private_block(0x0041, 'My Creator') 1047 1048 Parameters 1049 ---------- 1050 group : int 1051 The private group as a 32-bit :class:`int`. Must be an odd number. 1052 1053 Returns 1054 ------- 1055 list of str 1056 All private creator names for private blocks in the group. 1057 1058 Raises 1059 ------ 1060 ValueError 1061 If `group` is not a private group. 1062 """ 1063 if group % 2 == 0: 1064 raise ValueError('Group must be an odd number') 1065 1066 block = self[(group, 0x10):(group, 0x100)] # type: ignore[misc] 1067 return [x.value for x in block] 1068 1069 def get_private_item( 1070 self, group: int, element_offset: int, private_creator: str 1071 ) -> DataElement: 1072 """Return the data element for the given private tag `group`. 1073 1074 .. versionadded:: 1.3 1075 1076 This is analogous to ``Dataset.__getitem__()``, but only for private 1077 tags. This allows to find the private tag for the correct private 1078 creator without the need to add the tag to the private dictionary 1079 first. 1080 1081 Parameters 1082 ---------- 1083 group : int 1084 The private tag group where the item is located as a 32-bit int. 1085 element_offset : int 1086 The lower 16 bits (e.g. 2 hex numbers) of the element tag. 1087 private_creator : str 1088 The private creator for the tag. Must match the private creator 1089 for the tag to be returned. 1090 1091 Returns 1092 ------- 1093 dataelem.DataElement 1094 The corresponding element. 1095 1096 Raises 1097 ------ 1098 ValueError 1099 If `group` is not part of a private tag or `private_creator` is 1100 empty. 1101 KeyError 1102 If the private creator tag is not found in the given group. 1103 If the private tag is not found. 1104 """ 1105 block = self.private_block(group, private_creator) 1106 return self.__getitem__(block.get_tag(element_offset)) 1107 1108 @overload 1109 def get_item(self, key: slice) -> "Dataset": 1110 pass # pragma: no cover 1111 1112 @overload 1113 def get_item(self, key: TagType) -> DataElement: 1114 pass # pragma: no cover 1115 1116 def get_item( 1117 self, key: Union[slice, TagType] 1118 ) -> Union["Dataset", DataElement, RawDataElement, None]: 1119 """Return the raw data element if possible. 1120 1121 It will be raw if the user has never accessed the value, or set their 1122 own value. Note if the data element is a deferred-read element, 1123 then it is read and converted before being returned. 1124 1125 Parameters 1126 ---------- 1127 key 1128 The DICOM (group, element) tag in any form accepted by 1129 :func:`~pydicom.tag.Tag` such as ``[0x0010, 0x0010]``, 1130 ``(0x10, 0x10)``, ``0x00100010``, etc. May also be a :class:`slice` 1131 made up of DICOM tags. 1132 1133 Returns 1134 ------- 1135 dataelem.DataElement 1136 The corresponding element. 1137 """ 1138 if isinstance(key, slice): 1139 return self._dataset_slice(key) 1140 1141 elem = self._dict.get(Tag(key)) 1142 # If a deferred read, return using __getitem__ to read and convert it 1143 if isinstance(elem, RawDataElement) and elem.value is None: 1144 return self[key] 1145 1146 return elem 1147 1148 def _dataset_slice(self, slce: slice) -> "Dataset": 1149 """Return a slice that has the same properties as the original dataset. 1150 1151 That includes properties related to endianness and VR handling, 1152 and the specific character set. No element conversion is done, e.g. 1153 elements of type ``RawDataElement`` are kept. 1154 """ 1155 tags = self._slice_dataset(slce.start, slce.stop, slce.step) 1156 ds = Dataset({tag: self.get_item(tag) for tag in tags}) 1157 ds.is_little_endian = self.is_little_endian 1158 ds.is_implicit_VR = self.is_implicit_VR 1159 ds.set_original_encoding( 1160 self.read_implicit_vr, self.read_little_endian, self.read_encoding 1161 ) 1162 return ds 1163 1164 @property 1165 def is_original_encoding(self) -> bool: 1166 """Return ``True`` if the encoding to be used for writing is set and 1167 is the same as that used to originally encode the :class:`Dataset`. 1168 1169 .. versionadded:: 1.1 1170 1171 This includes properties related to endianness, VR handling and the 1172 (0008,0005) *Specific Character Set*. 1173 """ 1174 return ( 1175 self.is_implicit_VR is not None 1176 and self.is_little_endian is not None 1177 and self.read_implicit_vr == self.is_implicit_VR 1178 and self.read_little_endian == self.is_little_endian 1179 and self.read_encoding == self._character_set 1180 ) 1181 1182 def set_original_encoding( 1183 self, 1184 is_implicit_vr: Optional[bool], 1185 is_little_endian: Optional[bool], 1186 character_encoding: Union[None, str, MutableSequence[str]] 1187 ) -> None: 1188 """Set the values for the original transfer syntax and encoding. 1189 1190 .. versionadded:: 1.2 1191 1192 Can be used for a :class:`Dataset` with raw data elements to enable 1193 optimized writing (e.g. without decoding the data elements). 1194 """ 1195 self.read_implicit_vr = is_implicit_vr 1196 self.read_little_endian = is_little_endian 1197 self.read_encoding = character_encoding 1198 1199 def group_dataset(self, group: int) -> "Dataset": 1200 """Return a :class:`Dataset` containing only elements of a certain 1201 group. 1202 1203 Parameters 1204 ---------- 1205 group : int 1206 The group part of a DICOM (group, element) tag. 1207 1208 Returns 1209 ------- 1210 Dataset 1211 A :class:`Dataset` containing elements of the group specified. 1212 """ 1213 return self[(group, 0x0000):(group + 1, 0x0000)] # type: ignore[misc] 1214 1215 def __iter__(self) -> Iterator[DataElement]: 1216 """Iterate through the top-level of the Dataset, yielding DataElements. 1217 1218 Examples 1219 -------- 1220 1221 >>> ds = Dataset() 1222 >>> for elem in ds: 1223 ... print(elem) 1224 1225 The :class:`DataElements<pydicom.dataelem.DataElement>` are returned in 1226 increasing tag value order. Sequence items are returned as a single 1227 :class:`~pydicom.dataelem.DataElement`, so it is up 1228 to the calling code to recurse into the Sequence items if desired. 1229 1230 Yields 1231 ------ 1232 dataelem.DataElement 1233 The :class:`Dataset`'s 1234 :class:`DataElements<pydicom.dataelem.DataElement>`, sorted by 1235 increasing tag order. 1236 """ 1237 # Note this is different than the underlying dict class, 1238 # which returns the key of the key:value mapping. 1239 # Here the value is returned (but data_element.tag has the key) 1240 taglist = sorted(self._dict.keys()) 1241 for tag in taglist: 1242 yield self[tag] 1243 1244 def elements(self) -> Iterator[DataElement]: 1245 """Yield the top-level elements of the :class:`Dataset`. 1246 1247 .. versionadded:: 1.1 1248 1249 Examples 1250 -------- 1251 1252 >>> ds = Dataset() 1253 >>> for elem in ds.elements(): 1254 ... print(elem) 1255 1256 The elements are returned in the same way as in 1257 ``Dataset.__getitem__()``. 1258 1259 Yields 1260 ------ 1261 dataelem.DataElement or dataelem.RawDataElement 1262 The unconverted elements sorted by increasing tag order. 1263 """ 1264 taglist = sorted(self._dict.keys()) 1265 for tag in taglist: 1266 yield self.get_item(tag) 1267 1268 def __len__(self) -> int: 1269 """Return the number of elements in the top level of the dataset.""" 1270 return len(self._dict) 1271 1272 def __ne__(self, other: Any) -> bool: 1273 """Compare `self` and `other` for inequality.""" 1274 return not self == other 1275 1276 def clear(self) -> None: 1277 """Delete all the elements from the :class:`Dataset`.""" 1278 self._dict.clear() 1279 1280 def pop(self, key: Union[BaseTag, TagType], *args: Any) -> _DatasetValue: 1281 """Emulate :meth:`dict.pop` with support for tags and keywords. 1282 1283 Removes the element for `key` if it exists and returns it, 1284 otherwise returns a default value if given or raises :class:`KeyError`. 1285 1286 Parameters 1287 ---------- 1288 key : int or str or 2-tuple 1289 1290 * If :class:`tuple` - the group and element number of the DICOM tag 1291 * If :class:`int` - the combined group/element number 1292 * If :class:`str` - the DICOM keyword of the tag 1293 1294 *args : zero or one argument 1295 Defines the behavior if no tag exists for `key`: if given, 1296 it defines the return value, if not given, :class:`KeyError` is 1297 raised 1298 1299 Returns 1300 ------- 1301 RawDataElement or DataElement 1302 The element for `key` if it exists, or the default value if given. 1303 1304 Raises 1305 ------ 1306 KeyError 1307 If the `key` is not a valid tag or keyword. 1308 If the tag does not exist and no default is given. 1309 """ 1310 try: 1311 key = Tag(key) 1312 except Exception: 1313 pass 1314 1315 return self._dict.pop(cast(BaseTag, key), *args) 1316 1317 def popitem(self) -> Tuple[BaseTag, _DatasetValue]: 1318 """Emulate :meth:`dict.popitem`. 1319 1320 Returns 1321 ------- 1322 tuple of (BaseTag, DataElement) 1323 """ 1324 return self._dict.popitem() 1325 1326 def setdefault( 1327 self, key: TagType, default: Optional[Any] = None 1328 ) -> DataElement: 1329 """Emulate :meth:`dict.setdefault` with support for tags and keywords. 1330 1331 Examples 1332 -------- 1333 1334 >>> ds = Dataset() 1335 >>> elem = ds.setdefault((0x0010, 0x0010), "Test") 1336 >>> elem 1337 (0010, 0010) Patient's Name PN: 'Test' 1338 >>> elem.value 1339 'Test' 1340 >>> elem = ds.setdefault('PatientSex', 1341 ... DataElement(0x00100040, 'CS', 'F')) 1342 >>> elem.value 1343 'F' 1344 1345 Parameters 1346 ---------- 1347 key : int, str or 2-tuple of int 1348 1349 * If :class:`tuple` - the group and element number of the DICOM tag 1350 * If :class:`int` - the combined group/element number 1351 * If :class:`str` - the DICOM keyword of the tag 1352 default : pydicom.dataelem.DataElement or object, optional 1353 The :class:`~pydicom.dataelem.DataElement` to use with `key`, or 1354 the value of the :class:`~pydicom.dataelem.DataElement` to use with 1355 `key` (default ``None``). 1356 1357 Returns 1358 ------- 1359 pydicom.dataelem.DataElement or object 1360 The :class:`~pydicom.dataelem.DataElement` for `key`. 1361 1362 Raises 1363 ------ 1364 ValueError 1365 If `key` is not convertible to a valid tag or a known element 1366 keyword. 1367 KeyError 1368 If :attr:`~pydicom.config.enforce_valid_values` is ``True`` and 1369 `key` is an unknown non-private tag. 1370 """ 1371 tag = Tag(key) 1372 if tag in self: 1373 return self[tag] 1374 1375 if not isinstance(default, DataElement): 1376 if tag.is_private: 1377 vr = 'UN' 1378 else: 1379 try: 1380 vr = dictionary_VR(tag) 1381 except KeyError: 1382 if config.enforce_valid_values: 1383 raise KeyError(f"Unknown DICOM tag {tag}") 1384 else: 1385 vr = 'UN' 1386 warnings.warn( 1387 f"Unknown DICOM tag {tag} - setting VR to 'UN'" 1388 ) 1389 1390 default = DataElement(tag, vr, default) 1391 1392 self[key] = default 1393 1394 return default 1395 1396 def convert_pixel_data(self, handler_name: str = '') -> None: 1397 """Convert pixel data to a :class:`numpy.ndarray` internally. 1398 1399 Parameters 1400 ---------- 1401 handler_name : str, optional 1402 The name of the pixel handler that shall be used to 1403 decode the data. Supported names are: ``'gdcm'``, 1404 ``'pillow'``, ``'jpeg_ls'``, ``'rle'``, ``'numpy'`` and 1405 ``'pylibjpeg'``. If not used (the default), a matching handler is 1406 used from the handlers configured in 1407 :attr:`~pydicom.config.pixel_data_handlers`. 1408 1409 Returns 1410 ------- 1411 None 1412 Converted pixel data is stored internally in the dataset. 1413 1414 Raises 1415 ------ 1416 ValueError 1417 If `handler_name` is not a valid handler name. 1418 NotImplementedError 1419 If the given handler or any handler, if none given, is unable to 1420 decompress pixel data with the current transfer syntax 1421 RuntimeError 1422 If the given handler, or the handler that has been selected if 1423 none given, is not available. 1424 1425 Notes 1426 ----- 1427 If the pixel data is in a compressed image format, the data is 1428 decompressed and any related data elements are changed accordingly. 1429 """ 1430 # Check if already have converted to a NumPy array 1431 # Also check if pixel data has changed. If so, get new NumPy array 1432 already_have = True 1433 if not hasattr(self, "_pixel_array"): 1434 already_have = False 1435 elif self._pixel_id != get_image_pixel_ids(self): 1436 already_have = False 1437 1438 if already_have: 1439 return 1440 1441 if handler_name: 1442 self._convert_pixel_data_using_handler(handler_name) 1443 else: 1444 self._convert_pixel_data_without_handler() 1445 1446 def _convert_pixel_data_using_handler(self, name: str) -> None: 1447 """Convert the pixel data using handler with the given name. 1448 See :meth:`~Dataset.convert_pixel_data` for more information. 1449 """ 1450 # handle some variations in name 1451 handler_name = name.lower() 1452 if not handler_name.endswith('_handler'): 1453 handler_name += '_handler' 1454 if handler_name == 'numpy_handler': 1455 handler_name = 'np_handler' 1456 if handler_name == 'jpeg_ls_handler': 1457 # the name in config differs from the actual handler name 1458 # we allow both 1459 handler_name = 'jpegls_handler' 1460 if not hasattr(pydicom.config, handler_name): 1461 raise ValueError(f"'{name}' is not a known handler name") 1462 1463 handler = getattr(pydicom.config, handler_name) 1464 1465 tsyntax = self.file_meta.TransferSyntaxUID 1466 if not handler.supports_transfer_syntax(tsyntax): 1467 raise NotImplementedError( 1468 "Unable to decode pixel data with a transfer syntax UID" 1469 f" of '{tsyntax}' ({tsyntax.name}) using the pixel data " 1470 f"handler '{name}'. Please see the pydicom documentation for " 1471 "information on supported transfer syntaxes." 1472 ) 1473 if not handler.is_available(): 1474 raise RuntimeError( 1475 f"The pixel data handler '{name}' is not available on your " 1476 "system. Please refer to the pydicom documentation for " 1477 "information on installing needed packages." 1478 ) 1479 # if the conversion fails, the exception is propagated up 1480 self._do_pixel_data_conversion(handler) 1481 1482 def _convert_pixel_data_without_handler(self) -> None: 1483 """Convert the pixel data using the first matching handler. 1484 See :meth:`~Dataset.convert_pixel_data` for more information. 1485 """ 1486 # Find all possible handlers that support the transfer syntax 1487 ts = self.file_meta.TransferSyntaxUID 1488 possible_handlers = [ 1489 hh for hh in pydicom.config.pixel_data_handlers 1490 if hh is not None 1491 and hh.supports_transfer_syntax(ts) # type: ignore[attr-defined] 1492 ] 1493 1494 # No handlers support the transfer syntax 1495 if not possible_handlers: 1496 raise NotImplementedError( 1497 "Unable to decode pixel data with a transfer syntax UID of " 1498 f"'{ts}' ({ts.name}) as there are no pixel data " 1499 "handlers available that support it. Please see the pydicom " 1500 "documentation for information on supported transfer syntaxes " 1501 ) 1502 1503 # Handlers that both support the transfer syntax and have their 1504 # dependencies met 1505 available_handlers = [ 1506 hh for hh in possible_handlers 1507 if hh.is_available() # type: ignore[attr-defined] 1508 ] 1509 1510 # There are handlers that support the transfer syntax but none of them 1511 # can be used as missing dependencies 1512 if not available_handlers: 1513 # For each of the possible handlers we want to find which 1514 # dependencies are missing 1515 msg = ( 1516 "The following handlers are available to decode the pixel " 1517 "data however they are missing required dependencies: " 1518 ) 1519 pkg_msg = [] 1520 for hh in possible_handlers: 1521 hh_deps = hh.DEPENDENCIES # type: ignore[attr-defined] 1522 # Missing packages 1523 missing = [dd for dd in hh_deps if have_package(dd) is None] 1524 # Package names 1525 names = [hh_deps[name][1] for name in missing] 1526 pkg_msg.append( 1527 f"{hh.HANDLER_NAME} " # type: ignore[attr-defined] 1528 f"(req. {', '.join(names)})" 1529 ) 1530 1531 raise RuntimeError(msg + ', '.join(pkg_msg)) 1532 1533 last_exception = None 1534 for handler in available_handlers: 1535 try: 1536 self._do_pixel_data_conversion(handler) 1537 return 1538 except Exception as exc: 1539 logger.debug( 1540 "Exception raised by pixel data handler", exc_info=exc 1541 ) 1542 last_exception = exc 1543 1544 # The only way to get to this point is if we failed to get the pixel 1545 # array because all suitable handlers raised exceptions 1546 self._pixel_array = None 1547 self._pixel_id = {} 1548 1549 logger.info( 1550 "Unable to decode the pixel data using the following handlers: {}." 1551 "Please see the list of supported Transfer Syntaxes in the " 1552 "pydicom documentation for alternative packages that might " 1553 "be able to decode the data" 1554 .format(", ".join([str(hh) for hh in available_handlers])) 1555 ) 1556 raise last_exception # type: ignore[misc] 1557 1558 def _do_pixel_data_conversion(self, handler: Any) -> None: 1559 """Do the actual data conversion using the given handler.""" 1560 1561 # Use the handler to get a 1D numpy array of the pixel data 1562 # Will raise an exception if no pixel data element 1563 arr = handler.get_pixeldata(self) 1564 self._pixel_array = reshape_pixel_array(self, arr) 1565 1566 # Some handler/transfer syntax combinations may need to 1567 # convert the color space from YCbCr to RGB 1568 if handler.needs_to_convert_to_RGB(self): 1569 self._pixel_array = convert_color_space( 1570 self._pixel_array, 'YBR_FULL', 'RGB' 1571 ) 1572 1573 self._pixel_id = get_image_pixel_ids(self) 1574 1575 def compress( 1576 self, 1577 transfer_syntax_uid: str, 1578 arr: Optional["numpy.ndarray"] = None, 1579 encoding_plugin: str = '', 1580 decoding_plugin: str = '', 1581 encapsulate_ext: bool = False, 1582 **kwargs: Any, 1583 ) -> None: 1584 """Compress and update an uncompressed dataset in-place with the 1585 resulting :dcm:`encapsulated<part05/sect_A.4.html>` pixel data. 1586 1587 .. versionadded:: 2.2 1588 1589 The dataset must already have the following 1590 :dcm:`Image Pixel<part03/sect_C.7.6.3.html>` module elements present 1591 with correct values that correspond to the resulting compressed 1592 pixel data: 1593 1594 * (0028,0002) *Samples per Pixel* 1595 * (0028,0004) *Photometric Interpretation* 1596 * (0028,0008) *Number of Frames* (if more than 1 frame will be present) 1597 * (0028,0010) *Rows* 1598 * (0028,0011) *Columns* 1599 * (0028,0100) *Bits Allocated* 1600 * (0028,0101) *Bits Stored* 1601 * (0028,0103) *Pixel Representation* 1602 1603 This method will add the file meta dataset if none is present and add 1604 or modify the following elements: 1605 1606 * (0002,0010) *Transfer Syntax UID* 1607 * (7FE0,0010) *Pixel Data* 1608 1609 If *Samples per Pixel* is greater than 1 then the following element 1610 will also be added: 1611 1612 * (0028,0006) *Planar Configuration* 1613 1614 If the compressed pixel data is too large for encapsulation using a 1615 basic offset table then an :dcm:`extended offset table 1616 <part03/sect_C.7.6.3.html>` will also be used, in which case the 1617 following elements will also be added: 1618 1619 * (7FE0,0001) *Extended Offset Table* 1620 * (7FE0,0002) *Extended Offset Table Lengths* 1621 1622 **Supported Transfer Syntax UIDs** 1623 1624 +----------------------+----------+----------------------------------+ 1625 | UID | Plugins | Encoding Guide | 1626 +======================+==========+==================================+ 1627 | *RLE Lossless* - |pydicom, | :doc:`RLE Lossless | 1628 | 1.2.840.10008.1.2.5 |pylibjpeg,| </guides/encoding/rle_lossless>` | 1629 | |gdcm | | 1630 +----------------------+----------+----------------------------------+ 1631 1632 Examples 1633 -------- 1634 1635 Compress the existing uncompressed *Pixel Data* in place: 1636 1637 >>> from pydicom.data import get_testdata_file 1638 >>> from pydicom.uid import RLELossless 1639 >>> ds = get_testdata_file("CT_small.dcm", read=True) 1640 >>> ds.compress(RLELossless) 1641 >>> ds.save_as("CT_small_rle.dcm") 1642 1643 Parameters 1644 ---------- 1645 transfer_syntax_uid : pydicom.uid.UID 1646 The UID of the :dcm:`transfer syntax<part05/chapter_10.html>` to 1647 use when compressing the pixel data. 1648 arr : numpy.ndarray, optional 1649 Compress the uncompressed pixel data in `arr` and use it 1650 to set the *Pixel Data*. If `arr` is not used then the 1651 existing *Pixel Data* in the dataset will be compressed instead. 1652 The :attr:`~numpy.ndarray.shape`, :class:`~numpy.dtype` and 1653 contents of the array should match the dataset. 1654 encoding_plugin : str, optional 1655 Use the `encoding_plugin` to compress the pixel data. See the 1656 :doc:`user guide </old/image_data_compression>` for a list of 1657 plugins available for each UID and their dependencies. If not 1658 specified then all available plugins will be tried (default). 1659 decoding_plugin : str, optional 1660 Placeholder for future functionality. 1661 encapsulate_ext : bool, optional 1662 If ``True`` then force the addition of an extended offset table. 1663 If ``False`` (default) then an extended offset table 1664 will be added if needed for large amounts of compressed *Pixel 1665 Data*, otherwise just the basic offset table will be used. 1666 **kwargs 1667 Optional keyword parameters for the encoding plugin may also be 1668 present. See the :doc:`encoding plugins options 1669 </guides/encoding/encoder_plugin_options>` for more information. 1670 """ 1671 from pydicom.encoders import get_encoder 1672 1673 uid = UID(transfer_syntax_uid) 1674 1675 # Raises NotImplementedError if `uid` is not supported 1676 encoder = get_encoder(uid) 1677 if not encoder.is_available: 1678 missing = "\n".join( 1679 [f" {s}" for s in encoder.missing_dependencies] 1680 ) 1681 raise RuntimeError( 1682 f"The '{uid.name}' encoder is unavailable because its " 1683 f"encoding plugins are missing dependencies:\n" 1684 f"{missing}" 1685 ) 1686 1687 if arr is None: 1688 # Encode the current *Pixel Data* 1689 frame_iterator = encoder.iter_encode( 1690 self, 1691 encoding_plugin=encoding_plugin, 1692 decoding_plugin=decoding_plugin, 1693 **kwargs 1694 ) 1695 else: 1696 # Encode from an uncompressed pixel data array 1697 kwargs.update(encoder.kwargs_from_ds(self)) 1698 frame_iterator = encoder.iter_encode( 1699 arr, 1700 encoding_plugin=encoding_plugin, 1701 **kwargs 1702 ) 1703 1704 # Encode! 1705 encoded = [f for f in frame_iterator] 1706 1707 # Encapsulate the encoded *Pixel Data* 1708 nr_frames = getattr(self, "NumberOfFrames", 1) or 1 1709 total = (nr_frames - 1) * 8 + sum([len(f) for f in encoded[:-1]]) 1710 if encapsulate_ext or total > 2**32 - 1: 1711 (self.PixelData, 1712 self.ExtendedOffsetTable, 1713 self.ExtendedOffsetTableLengths) = encapsulate_extended(encoded) 1714 else: 1715 self.PixelData = encapsulate(encoded) 1716 1717 self['PixelData'].is_undefined_length = True 1718 1719 # Set the correct *Transfer Syntax UID* 1720 if not hasattr(self, 'file_meta'): 1721 self.file_meta = FileMetaDataset() 1722 1723 self.file_meta.TransferSyntaxUID = uid 1724 1725 # Add or update any other required elements 1726 if self.SamplesPerPixel > 1: 1727 self.PlanarConfiguration: int = 1 if uid == RLELossless else 0 1728 1729 def decompress(self, handler_name: str = '') -> None: 1730 """Decompresses *Pixel Data* and modifies the :class:`Dataset` 1731 in-place. 1732 1733 .. versionadded:: 1.4 1734 1735 The `handler_name` keyword argument was added 1736 1737 If not a compressed transfer syntax, then pixel data is converted 1738 to a :class:`numpy.ndarray` internally, but not returned. 1739 1740 If compressed pixel data, then is decompressed using an image handler, 1741 and internal state is updated appropriately: 1742 1743 - ``Dataset.file_meta.TransferSyntaxUID`` is updated to non-compressed 1744 form 1745 - :attr:`~pydicom.dataelem.DataElement.is_undefined_length` 1746 is ``False`` for the (7FE0,0010) *Pixel Data* element. 1747 1748 .. versionchanged:: 1.4 1749 1750 The `handler_name` keyword argument was added 1751 1752 Parameters 1753 ---------- 1754 handler_name : str, optional 1755 The name of the pixel handler that shall be used to 1756 decode the data. Supported names are: ``'gdcm'``, 1757 ``'pillow'``, ``'jpeg_ls'``, ``'rle'``, ``'numpy'`` and 1758 ``'pylibjpeg'``. 1759 If not used (the default), a matching handler is used from the 1760 handlers configured in :attr:`~pydicom.config.pixel_data_handlers`. 1761 1762 Returns 1763 ------- 1764 None 1765 1766 Raises 1767 ------ 1768 NotImplementedError 1769 If the pixel data was originally compressed but file is not 1770 *Explicit VR Little Endian* as required by the DICOM Standard. 1771 """ 1772 self.convert_pixel_data(handler_name) 1773 self.is_decompressed = True 1774 # May have been undefined length pixel data, but won't be now 1775 if 'PixelData' in self: 1776 self[0x7fe00010].is_undefined_length = False 1777 1778 # Make sure correct Transfer Syntax is set 1779 # According to the dicom standard PS3.5 section A.4, 1780 # all compressed files must have been explicit VR, little endian 1781 # First check if was a compressed file 1782 if ( 1783 hasattr(self, 'file_meta') 1784 and self.file_meta.TransferSyntaxUID.is_compressed 1785 ): 1786 # Check that current file as read does match expected 1787 if not self.is_little_endian or self.is_implicit_VR: 1788 msg = ("Current dataset does not match expected ExplicitVR " 1789 "LittleEndian transfer syntax from a compressed " 1790 "transfer syntax") 1791 raise NotImplementedError(msg) 1792 1793 # All is as expected, updated the Transfer Syntax 1794 self.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian 1795 1796 def overlay_array(self, group: int) -> "numpy.ndarray": 1797 """Return the *Overlay Data* in `group` as a :class:`numpy.ndarray`. 1798 1799 .. versionadded:: 1.4 1800 1801 Parameters 1802 ---------- 1803 group : int 1804 The group number of the overlay data. 1805 1806 Returns 1807 ------- 1808 numpy.ndarray 1809 The (`group`,3000) *Overlay Data* converted to a 1810 :class:`numpy.ndarray`. 1811 """ 1812 if group < 0x6000 or group > 0x60FF: 1813 raise ValueError( 1814 "The group part of the 'Overlay Data' element tag must be " 1815 "between 0x6000 and 0x60FF (inclusive)" 1816 ) 1817 1818 from pydicom.config import overlay_data_handlers 1819 1820 available_handlers = [ 1821 hh for hh in overlay_data_handlers 1822 if hh.is_available() # type: ignore[attr-defined] 1823 ] 1824 if not available_handlers: 1825 # For each of the handlers we want to find which 1826 # dependencies are missing 1827 msg = ( 1828 "The following handlers are available to decode the overlay " 1829 "data however they are missing required dependencies: " 1830 ) 1831 pkg_msg = [] 1832 for hh in overlay_data_handlers: 1833 hh_deps = hh.DEPENDENCIES # type: ignore[attr-defined] 1834 # Missing packages 1835 missing = [dd for dd in hh_deps if have_package(dd) is None] 1836 # Package names 1837 names = [hh_deps[name][1] for name in missing] 1838 pkg_msg.append( 1839 f"{hh.HANDLER_NAME} " # type: ignore[attr-defined] 1840 f"(req. {', '.join(names)})" 1841 ) 1842 1843 raise RuntimeError(msg + ', '.join(pkg_msg)) 1844 1845 last_exception = None 1846 for handler in available_handlers: 1847 try: 1848 # Use the handler to get an ndarray of the pixel data 1849 func = handler.get_overlay_array # type: ignore[attr-defined] 1850 return cast("numpy.ndarray", func(self, group)) 1851 except Exception as exc: 1852 logger.debug( 1853 "Exception raised by overlay data handler", exc_info=exc 1854 ) 1855 last_exception = exc 1856 1857 logger.info( 1858 "Unable to decode the overlay data using the following handlers: " 1859 "{}. Please see the list of supported Transfer Syntaxes in the " 1860 "pydicom documentation for alternative packages that might " 1861 "be able to decode the data" 1862 .format(", ".join([str(hh) for hh in available_handlers])) 1863 ) 1864 1865 raise last_exception # type: ignore[misc] 1866 1867 @property 1868 def pixel_array(self) -> "numpy.ndarray": 1869 """Return the pixel data as a :class:`numpy.ndarray`. 1870 1871 .. versionchanged:: 1.4 1872 1873 Added support for *Float Pixel Data* and *Double Float Pixel Data* 1874 1875 Returns 1876 ------- 1877 numpy.ndarray 1878 The (7FE0,0008) *Float Pixel Data*, (7FE0,0009) *Double Float 1879 Pixel Data* or (7FE0,0010) *Pixel Data* converted to a 1880 :class:`numpy.ndarray`. 1881 """ 1882 self.convert_pixel_data() 1883 return cast("numpy.ndarray", self._pixel_array) 1884 1885 def waveform_array(self, index: int) -> "numpy.ndarray": 1886 """Return an :class:`~numpy.ndarray` for the multiplex group at 1887 `index` in the (5400,0100) *Waveform Sequence*. 1888 1889 .. versionadded:: 2.1 1890 1891 Parameters 1892 ---------- 1893 index : int 1894 The index of the multiplex group to return the array for. 1895 1896 Returns 1897 ------ 1898 numpy.ndarray 1899 The *Waveform Data* for the multiplex group as an 1900 :class:`~numpy.ndarray` with shape (samples, channels). If 1901 (003A,0210) *Channel Sensitivity* is present 1902 then the values will be in the units specified by the (003A,0211) 1903 *Channel Sensitivity Units Sequence*. 1904 1905 See Also 1906 -------- 1907 :func:`~pydicom.waveforms.numpy_handler.generate_multiplex` 1908 :func:`~pydicom.waveforms.numpy_handler.multiplex_array` 1909 """ 1910 if not wave_handler.is_available(): 1911 raise RuntimeError("The waveform data handler requires numpy") 1912 1913 return wave_handler.multiplex_array(self, index, as_raw=False) 1914 1915 # Format strings spec'd according to python string formatting options 1916 # See http://docs.python.org/library/stdtypes.html#string-formatting-operations # noqa 1917 default_element_format = "%(tag)s %(name)-35.35s %(VR)s: %(repval)s" 1918 default_sequence_element_format = "%(tag)s %(name)-35.35s %(VR)s: %(repval)s" # noqa 1919 1920 def formatted_lines( 1921 self, 1922 element_format: str = default_element_format, 1923 sequence_element_format: str = default_sequence_element_format, 1924 indent_format: Optional[str] = None 1925 ) -> Iterator[str]: 1926 """Iterate through the :class:`Dataset` yielding formatted :class:`str` 1927 for each element. 1928 1929 Parameters 1930 ---------- 1931 element_format : str 1932 The string format to use for non-sequence elements. Formatting uses 1933 the attributes of 1934 :class:`~pydicom.dataelem.DataElement`. Default is 1935 ``"%(tag)s %(name)-35.35s %(VR)s: %(repval)s"``. 1936 sequence_element_format : str 1937 The string format to use for sequence elements. Formatting uses 1938 the attributes of 1939 :class:`~pydicom.dataelem.DataElement`. Default is 1940 ``"%(tag)s %(name)-35.35s %(VR)s: %(repval)s"`` 1941 indent_format : str or None 1942 Placeholder for future functionality. 1943 1944 Yields 1945 ------ 1946 str 1947 A string representation of an element. 1948 """ 1949 exclusion = ('from_json', 'to_json', 'to_json_dict', 'clear') 1950 for elem in self.iterall(): 1951 # Get all the attributes possible for this data element (e.g. 1952 # gets descriptive text name too) 1953 # This is the dictionary of names that can be used in the format 1954 # string 1955 elem_dict = { 1956 attr: ( 1957 getattr(elem, attr)() if callable(getattr(elem, attr)) 1958 else getattr(elem, attr) 1959 ) 1960 for attr in dir(elem) if not attr.startswith("_") 1961 and attr not in exclusion 1962 } 1963 if elem.VR == "SQ": 1964 yield sequence_element_format % elem_dict 1965 else: 1966 yield element_format % elem_dict 1967 1968 def _pretty_str( 1969 self, indent: int = 0, top_level_only: bool = False 1970 ) -> str: 1971 """Return a string of the DataElements in the Dataset, with indented 1972 levels. 1973 1974 This private method is called by the ``__str__()`` method for handling 1975 print statements or ``str(dataset)``, and the ``__repr__()`` method. 1976 It is also used by ``top()``, therefore the `top_level_only` flag. 1977 This function recurses, with increasing indentation levels. 1978 1979 ..versionchanged:: 2.0 1980 1981 The file meta information is returned in its own section, 1982 if :data:`~pydicom.config.show_file_meta` is ``True`` (default) 1983 1984 Parameters 1985 ---------- 1986 indent : int, optional 1987 The indent level offset (default ``0``). 1988 top_level_only : bool, optional 1989 When True, only create a string for the top level elements, i.e. 1990 exclude elements within any Sequences (default ``False``). 1991 1992 Returns 1993 ------- 1994 str 1995 A string representation of the Dataset. 1996 """ 1997 strings = [] 1998 indent_str = self.indent_chars * indent 1999 nextindent_str = self.indent_chars * (indent + 1) 2000 2001 # Display file meta, if configured to do so, and have a non-empty one 2002 if ( 2003 hasattr(self, "file_meta") and self.file_meta 2004 and pydicom.config.show_file_meta 2005 ): 2006 strings.append(f"{'Dataset.file_meta ':-<49}") 2007 for elem in self.file_meta: 2008 with tag_in_exception(elem.tag): 2009 strings.append(indent_str + repr(elem)) 2010 strings.append(f"{'':-<49}") 2011 2012 for elem in self: 2013 with tag_in_exception(elem.tag): 2014 if elem.VR == "SQ": # a sequence 2015 strings.append( 2016 f"{indent_str}{str(elem.tag)} {elem.description()} " 2017 f"{len(elem.value)} item(s) ---- " 2018 ) 2019 if not top_level_only: 2020 for dataset in elem.value: 2021 strings.append(dataset._pretty_str(indent + 1)) 2022 strings.append(nextindent_str + "---------") 2023 else: 2024 strings.append(indent_str + repr(elem)) 2025 return "\n".join(strings) 2026 2027 def remove_private_tags(self) -> None: 2028 """Remove all private elements from the :class:`Dataset`.""" 2029 2030 def remove_callback(dataset: "Dataset", elem: DataElement) -> None: 2031 """Internal method to use as callback to walk() method.""" 2032 if elem.tag.is_private: 2033 # can't del self[tag] - won't be right dataset on recursion 2034 del dataset[elem.tag] 2035 2036 self.walk(remove_callback) 2037 2038 def save_as( 2039 self, 2040 filename: Union[str, "os.PathLike[AnyStr]", BinaryIO], 2041 write_like_original: bool = True 2042 ) -> None: 2043 """Write the :class:`Dataset` to `filename`. 2044 2045 Wrapper for pydicom.filewriter.dcmwrite, passing this dataset to it. 2046 See documentation for that function for details. 2047 2048 See Also 2049 -------- 2050 pydicom.filewriter.dcmwrite 2051 Write a DICOM file from a :class:`FileDataset` instance. 2052 """ 2053 pydicom.dcmwrite(filename, self, write_like_original) 2054 2055 def ensure_file_meta(self) -> None: 2056 """Create an empty ``Dataset.file_meta`` if none exists. 2057 2058 .. versionadded:: 1.2 2059 """ 2060 # Changed in v2.0 so does not re-assign self.file_meta with getattr() 2061 if not hasattr(self, "file_meta"): 2062 self.file_meta = FileMetaDataset() 2063 2064 def fix_meta_info(self, enforce_standard: bool = True) -> None: 2065 """Ensure the file meta info exists and has the correct values 2066 for transfer syntax and media storage UIDs. 2067 2068 .. versionadded:: 1.2 2069 2070 .. warning:: 2071 2072 The transfer syntax for ``is_implicit_VR = False`` and 2073 ``is_little_endian = True`` is ambiguous and will therefore not 2074 be set. 2075 2076 Parameters 2077 ---------- 2078 enforce_standard : bool, optional 2079 If ``True``, a check for incorrect and missing elements is 2080 performed (see :func:`~validate_file_meta`). 2081 """ 2082 self.ensure_file_meta() 2083 2084 if self.is_little_endian and self.is_implicit_VR: 2085 self.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian 2086 elif not self.is_little_endian and not self.is_implicit_VR: 2087 self.file_meta.TransferSyntaxUID = ExplicitVRBigEndian 2088 elif not self.is_little_endian and self.is_implicit_VR: 2089 raise NotImplementedError("Implicit VR Big Endian is not a " 2090 "supported Transfer Syntax.") 2091 2092 if 'SOPClassUID' in self: 2093 self.file_meta.MediaStorageSOPClassUID = self.SOPClassUID 2094 if 'SOPInstanceUID' in self: 2095 self.file_meta.MediaStorageSOPInstanceUID = self.SOPInstanceUID 2096 if enforce_standard: 2097 validate_file_meta(self.file_meta, enforce_standard=True) 2098 2099 def __setattr__(self, name: str, value: Any) -> None: 2100 """Intercept any attempts to set a value for an instance attribute. 2101 2102 If name is a DICOM keyword, set the corresponding tag and DataElement. 2103 Else, set an instance (python) attribute as any other class would do. 2104 2105 Parameters 2106 ---------- 2107 name : str 2108 The keyword for the element you wish to add/change. If 2109 `name` is not a DICOM element keyword then this will be the 2110 name of the attribute to be added/changed. 2111 value 2112 The value for the attribute to be added/changed. 2113 """ 2114 tag = tag_for_keyword(name) 2115 if tag is not None: # successfully mapped name to a tag 2116 if tag not in self: 2117 # don't have this tag yet->create the data_element instance 2118 VR = dictionary_VR(tag) 2119 data_element = DataElement(tag, VR, value) 2120 if VR == 'SQ': 2121 # let a sequence know its parent dataset to pass it 2122 # to its items, who may need parent dataset tags 2123 # to resolve ambiguous tags 2124 data_element.parent = self 2125 else: 2126 # already have this data_element, just changing its value 2127 data_element = self[tag] 2128 data_element.value = value 2129 # Now have data_element - store it in this dict 2130 self[tag] = data_element 2131 elif repeater_has_keyword(name): 2132 # Check if `name` is repeaters element 2133 raise ValueError( 2134 f"'{name}' is a DICOM repeating group element and must be " 2135 "added using the add() or add_new() methods." 2136 ) 2137 elif name == "file_meta": 2138 self._set_file_meta(value) 2139 else: 2140 # Warn if `name` is camel case but not a keyword 2141 if _RE_CAMEL_CASE.match(name): 2142 msg = ( 2143 f"Camel case attribute '{name}' used which is not in the " 2144 "element keyword data dictionary" 2145 ) 2146 if config.INVALID_KEYWORD_BEHAVIOR == "WARN": 2147 warnings.warn(msg) 2148 elif config.INVALID_KEYWORD_BEHAVIOR == "RAISE": 2149 raise ValueError(msg) 2150 2151 # name not in dicom dictionary - setting a non-dicom instance 2152 # attribute 2153 # XXX note if user mis-spells a dicom data_element - no error!!! 2154 object.__setattr__(self, name, value) 2155 2156 def _set_file_meta(self, value: Optional["Dataset"]) -> None: 2157 if value is not None and not isinstance(value, FileMetaDataset): 2158 if config._use_future: 2159 raise TypeError( 2160 "Pydicom Future: Dataset.file_meta must be an instance " 2161 "of FileMetaDataset" 2162 ) 2163 2164 FileMetaDataset.validate(value) 2165 warnings.warn( 2166 "Starting in pydicom 3.0, Dataset.file_meta must be a " 2167 "FileMetaDataset class instance", 2168 DeprecationWarning 2169 ) 2170 2171 self.__dict__["file_meta"] = value 2172 2173 def __setitem__( 2174 self, key: Union[slice, TagType], elem: _DatasetValue 2175 ) -> None: 2176 """Operator for ``Dataset[key] = elem``. 2177 2178 Parameters 2179 ---------- 2180 key : int or Tuple[int, int] or str 2181 The tag for the element to be added to the :class:`Dataset`. 2182 elem : dataelem.DataElement or dataelem.RawDataElement 2183 The element to add to the :class:`Dataset`. 2184 2185 Raises 2186 ------ 2187 NotImplementedError 2188 If `key` is a :class:`slice`. 2189 ValueError 2190 If the `key` value doesn't match the corresponding 2191 :attr:`DataElement.tag<pydicom.dataelem.tag>`. 2192 """ 2193 if isinstance(key, slice): 2194 raise NotImplementedError( 2195 'Slicing is not supported when setting Dataset items' 2196 ) 2197 2198 try: 2199 key = Tag(key) 2200 except Exception as exc: 2201 raise ValueError( 2202 f"Unable to convert the key '{key}' to an element tag" 2203 ) from exc 2204 2205 if not isinstance(elem, (DataElement, RawDataElement)): 2206 raise TypeError("Dataset items must be 'DataElement' instances") 2207 2208 if isinstance(elem.tag, BaseTag): 2209 elem_tag = elem.tag 2210 else: 2211 elem_tag = Tag(elem.tag) 2212 2213 if key != elem_tag: 2214 raise ValueError( 2215 f"The key '{key}' doesn't match the 'DataElement' tag " 2216 f"'{elem_tag}'" 2217 ) 2218 2219 if elem_tag.is_private: 2220 # See PS 3.5-2008 section 7.8.1 (p. 44) for how blocks are reserved 2221 logger.debug(f"Setting private tag {elem_tag}") 2222 private_block = elem_tag.element >> 8 2223 private_creator_tag = Tag(elem_tag.group, private_block) 2224 if private_creator_tag in self and elem_tag != private_creator_tag: 2225 if isinstance(elem, RawDataElement): 2226 elem = DataElement_from_raw( 2227 elem, self._character_set, self 2228 ) 2229 elem.private_creator = self[private_creator_tag].value 2230 2231 self._dict[elem_tag] = elem 2232 2233 def _slice_dataset( 2234 self, 2235 start: Optional[TagType], 2236 stop: Optional[TagType], 2237 step: Optional[int] 2238 ) -> List[BaseTag]: 2239 """Return the element tags in the Dataset that match the slice. 2240 2241 Parameters 2242 ---------- 2243 start : int or 2-tuple of int or None 2244 The slice's starting element tag value, in any format accepted by 2245 :func:`~pydicom.tag.Tag`. 2246 stop : int or 2-tuple of int or None 2247 The slice's stopping element tag value, in any format accepted by 2248 :func:`~pydicom.tag.Tag`. 2249 step : int or None 2250 The slice's step size. 2251 2252 Returns 2253 ------ 2254 list of BaseTag 2255 The tags in the :class:`Dataset` that meet the conditions of the 2256 slice. 2257 """ 2258 # Check the starting/stopping Tags are valid when used 2259 if start is not None: 2260 start = Tag(start) 2261 if stop is not None: 2262 stop = Tag(stop) 2263 2264 all_tags = sorted(self._dict.keys()) 2265 # If the Dataset is empty, return an empty list 2266 if not all_tags: 2267 return [] 2268 2269 # Special case the common situations: 2270 # - start and/or stop are None 2271 # - step is 1 2272 2273 if start is None: 2274 if stop is None: 2275 # For step=1 avoid copying the list 2276 return all_tags if step == 1 else all_tags[::step] 2277 else: # Have a stop value, get values until that point 2278 step1_list = list(takewhile(lambda x: x < stop, all_tags)) 2279 return step1_list if step == 1 else step1_list[::step] 2280 2281 # Have a non-None start value. Find its index 2282 i_start = bisect_left(all_tags, start) 2283 if stop is None: 2284 return all_tags[i_start::step] 2285 2286 i_stop = bisect_left(all_tags, stop) 2287 return all_tags[i_start:i_stop:step] 2288 2289 def __str__(self) -> str: 2290 """Handle str(dataset). 2291 2292 ..versionchanged:: 2.0 2293 2294 The file meta information was added in its own section, 2295 if :data:`pydicom.config.show_file_meta` is ``True`` 2296 2297 """ 2298 return self._pretty_str() 2299 2300 def top(self) -> str: 2301 """Return a :class:`str` representation of the top level elements. """ 2302 return self._pretty_str(top_level_only=True) 2303 2304 def trait_names(self) -> List[str]: 2305 """Return a :class:`list` of valid names for auto-completion code. 2306 2307 Used in IPython, so that data element names can be found and offered 2308 for autocompletion on the IPython command line. 2309 """ 2310 return dir(self) 2311 2312 def update(self, d: _DatasetType) -> None: 2313 """Extend :meth:`dict.update` to handle DICOM tags and keywords. 2314 2315 Parameters 2316 ---------- 2317 dictionary : dict or Dataset 2318 The :class:`dict` or :class:`Dataset` to use when updating the 2319 current object. 2320 """ 2321 for key, value in list(d.items()): 2322 if isinstance(key, str): 2323 setattr(self, key, value) 2324 else: 2325 self[Tag(cast(int, key))] = value 2326 2327 def iterall(self) -> Iterator[DataElement]: 2328 """Iterate through the :class:`Dataset`, yielding all the elements. 2329 2330 Unlike ``iter(Dataset)``, this *does* recurse into sequences, 2331 and so yields all elements as if dataset were "flattened". 2332 2333 Yields 2334 ------ 2335 dataelem.DataElement 2336 """ 2337 for elem in self: 2338 yield elem 2339 if elem.VR == "SQ": 2340 for ds in elem.value: 2341 yield from ds.iterall() 2342 2343 def walk( 2344 self, 2345 callback: Callable[["Dataset", DataElement], None], 2346 recursive: bool = True 2347 ) -> None: 2348 """Iterate through the :class:`Dataset's<Dataset>` elements and run 2349 `callback` on each. 2350 2351 Visit all elements in the :class:`Dataset`, possibly recursing into 2352 sequences and their items. The `callback` function is called for each 2353 :class:`~pydicom.dataelem.DataElement` (including elements 2354 with a VR of 'SQ'). Can be used to perform an operation on certain 2355 types of elements. 2356 2357 For example, 2358 :meth:`~Dataset.remove_private_tags` finds all elements with private 2359 tags and deletes them. 2360 2361 The elements will be returned in order of increasing tag number within 2362 their current :class:`Dataset`. 2363 2364 Parameters 2365 ---------- 2366 callback 2367 A callable function that takes two arguments: 2368 2369 * a :class:`Dataset` 2370 * a :class:`~pydicom.dataelem.DataElement` belonging 2371 to that :class:`Dataset` 2372 2373 recursive : bool, optional 2374 Flag to indicate whether to recurse into sequences (default 2375 ``True``). 2376 """ 2377 taglist = sorted(self._dict.keys()) 2378 for tag in taglist: 2379 2380 with tag_in_exception(tag): 2381 data_element = self[tag] 2382 callback(self, data_element) # self = this Dataset 2383 # 'tag in self' below needed in case callback deleted 2384 # data_element 2385 if recursive and tag in self and data_element.VR == "SQ": 2386 sequence = data_element.value 2387 for dataset in sequence: 2388 dataset.walk(callback) 2389 2390 @classmethod 2391 def from_json( 2392 cls: Type["Dataset"], 2393 json_dataset: Union[Dict[str, Any], str, bytes, bytearray], 2394 bulk_data_uri_handler: Optional[ 2395 Union[ 2396 Callable[[str, str, str], Union[None, str, int, float, bytes]], 2397 Callable[[str], Union[None, str, int, float, bytes]] 2398 ] 2399 ] = None 2400 ) -> "Dataset": 2401 """Return a :class:`Dataset` from a DICOM JSON Model object. 2402 2403 .. versionadded:: 1.3 2404 2405 See the DICOM Standard, Part 18, :dcm:`Annex F<part18/chapter_F.html>`. 2406 2407 Parameters 2408 ---------- 2409 json_dataset : dict, str, bytes or bytearray 2410 :class:`dict`, :class:`str`, :class:`bytes` or :class:`bytearray` 2411 representing a DICOM Data Set formatted based on the :dcm:`DICOM 2412 JSON Model<part18/chapter_F.html>`. 2413 bulk_data_uri_handler : callable, optional 2414 Callable function that accepts either the tag, vr and 2415 "BulkDataURI" value or just the "BulkDataURI" value of the JSON 2416 representation of a data element and returns the actual value of 2417 that data element (retrieved via DICOMweb WADO-RS). If no 2418 `bulk_data_uri_handler` is specified (default) then the 2419 corresponding element will have an "empty" value such as 2420 ``""``, ``b""`` or ``None`` depending on the `vr` (i.e. the 2421 Value Multiplicity will be 0). 2422 2423 Returns 2424 ------- 2425 Dataset 2426 """ 2427 if isinstance(json_dataset, (str, bytes, bytearray)): 2428 json_dataset = cast(Dict[str, Any], json.loads(json_dataset)) 2429 2430 dataset = cls() 2431 for tag, mapping in json_dataset.items(): 2432 # `tag` is an element tag in uppercase hex format as a str 2433 # `mapping` is Dict[str, Any] and should have keys 'vr' and at most 2434 # one of ('Value', 'BulkDataURI', 'InlineBinary') but may have 2435 # none of those if the element's VM is 0 2436 vr = mapping['vr'] 2437 unique_value_keys = tuple( 2438 set(mapping.keys()) & set(jsonrep.JSON_VALUE_KEYS) 2439 ) 2440 if len(unique_value_keys) == 0: 2441 value_key = None 2442 value = [''] 2443 else: 2444 value_key = unique_value_keys[0] 2445 value = mapping[value_key] 2446 data_element = DataElement.from_json( 2447 cls, tag, vr, value, value_key, bulk_data_uri_handler 2448 ) 2449 dataset.add(data_element) 2450 return dataset 2451 2452 def to_json_dict( 2453 self, 2454 bulk_data_threshold: int = 1024, 2455 bulk_data_element_handler: Optional[Callable[[DataElement], str]] = None, # noqa 2456 suppress_invalid_tags: bool = False, 2457 ) -> Dict[str, Any]: 2458 """Return a dictionary representation of the :class:`Dataset` 2459 conforming to the DICOM JSON Model as described in the DICOM 2460 Standard, Part 18, :dcm:`Annex F<part18/chapter_F.html>`. 2461 2462 .. versionadded:: 1.4 2463 2464 Parameters 2465 ---------- 2466 bulk_data_threshold : int, optional 2467 Threshold for the length of a base64-encoded binary data element 2468 above which the element should be considered bulk data and the 2469 value provided as a URI rather than included inline (default: 2470 ``1024``). Ignored if no bulk data handler is given. 2471 bulk_data_element_handler : callable, optional 2472 Callable function that accepts a bulk data element and returns a 2473 JSON representation of the data element (dictionary including the 2474 "vr" key and either the "InlineBinary" or the "BulkDataURI" key). 2475 suppress_invalid_tags : bool, optional 2476 Flag to specify if errors while serializing tags should be logged 2477 and the tag dropped or if the error should be bubbled up. 2478 2479 Returns 2480 ------- 2481 dict 2482 :class:`Dataset` representation based on the DICOM JSON Model. 2483 """ 2484 json_dataset = {} 2485 for key in self.keys(): 2486 json_key = '{:08X}'.format(key) 2487 data_element = self[key] 2488 try: 2489 json_dataset[json_key] = data_element.to_json_dict( 2490 bulk_data_element_handler=bulk_data_element_handler, 2491 bulk_data_threshold=bulk_data_threshold 2492 ) 2493 except Exception as exc: 2494 logger.error(f"Error while processing tag {json_key}") 2495 if not suppress_invalid_tags: 2496 raise exc 2497 2498 return json_dataset 2499 2500 def to_json( 2501 self, 2502 bulk_data_threshold: int = 1024, 2503 bulk_data_element_handler: Optional[Callable[[DataElement], str]] = None, # noqa 2504 dump_handler: Optional[Callable[[Dict[str, Any]], str]] = None, 2505 suppress_invalid_tags: bool = False, 2506 ) -> str: 2507 """Return a JSON representation of the :class:`Dataset`. 2508 2509 .. versionadded:: 1.3 2510 2511 See the DICOM Standard, Part 18, :dcm:`Annex F<part18/chapter_F.html>`. 2512 2513 Parameters 2514 ---------- 2515 bulk_data_threshold : int, optional 2516 Threshold for the length of a base64-encoded binary data element 2517 above which the element should be considered bulk data and the 2518 value provided as a URI rather than included inline (default: 2519 ``1024``). Ignored if no bulk data handler is given. 2520 bulk_data_element_handler : callable, optional 2521 Callable function that accepts a bulk data element and returns a 2522 JSON representation of the data element (dictionary including the 2523 "vr" key and either the "InlineBinary" or the "BulkDataURI" key). 2524 dump_handler : callable, optional 2525 Callable function that accepts a :class:`dict` and returns the 2526 serialized (dumped) JSON string (by default uses 2527 :func:`json.dumps`). 2528 2529 .. note: 2530 2531 Make sure to use a dump handler that sorts the keys (see 2532 example below) to create DICOM-conformant JSON. 2533 suppress_invalid_tags : bool, optional 2534 Flag to specify if errors while serializing tags should be logged 2535 and the tag dropped or if the error should be bubbled up. 2536 2537 Returns 2538 ------- 2539 str 2540 :class:`Dataset` serialized into a string based on the DICOM JSON 2541 Model. 2542 2543 Examples 2544 -------- 2545 >>> def my_json_dumps(data): 2546 ... return json.dumps(data, indent=4, sort_keys=True) 2547 >>> ds.to_json(dump_handler=my_json_dumps) 2548 """ 2549 if dump_handler is None: 2550 def json_dump(d: Any) -> str: 2551 return json.dumps(d, sort_keys=True) 2552 2553 dump_handler = json_dump 2554 2555 return dump_handler( 2556 self.to_json_dict( 2557 bulk_data_threshold, 2558 bulk_data_element_handler, 2559 suppress_invalid_tags=suppress_invalid_tags 2560 ) 2561 ) 2562 2563 def __getstate__(self) -> Dict[str, Any]: 2564 # pickle cannot handle weakref - remove parent 2565 d = self.__dict__.copy() 2566 del d['parent'] 2567 return d 2568 2569 def __setstate__(self, state: Dict[str, Any]) -> None: 2570 self.__dict__.update(state) 2571 # re-add parent - it will be set to the parent dataset on demand 2572 # if the dataset is in a sequence 2573 self.__dict__['parent'] = None 2574 2575 __repr__ = __str__ 2576 2577 2578_FileDataset = TypeVar("_FileDataset", bound="FileDataset") 2579 2580 2581class FileDataset(Dataset): 2582 """An extension of :class:`Dataset` to make reading and writing to 2583 file-like easier. 2584 2585 Attributes 2586 ---------- 2587 preamble : str or bytes or None 2588 The optional DICOM preamble prepended to the :class:`FileDataset`, if 2589 available. 2590 file_meta : FileMetaDataset or None 2591 The Dataset's file meta information as a :class:`FileMetaDataset`, 2592 if available (``None`` if not present). 2593 Consists of group ``0x0002`` elements. 2594 filename : str or None 2595 The filename that the :class:`FileDataset` was read from (if read from 2596 file) or ``None`` if the filename is not available (if read from a 2597 :class:`io.BytesIO` or similar). 2598 fileobj_type 2599 The object type of the file-like the :class:`FileDataset` was read 2600 from. 2601 is_implicit_VR : bool 2602 ``True`` if the dataset encoding is implicit VR, ``False`` otherwise. 2603 is_little_endian : bool 2604 ``True`` if the dataset encoding is little endian byte ordering, 2605 ``False`` otherwise. 2606 timestamp : float or None 2607 The modification time of the file the :class:`FileDataset` was read 2608 from, ``None`` if the modification time is not available. 2609 """ 2610 2611 def __init__( 2612 self, 2613 filename_or_obj: Union[str, "os.PathLike[AnyStr]", BinaryIO], 2614 dataset: _DatasetType, 2615 preamble: Optional[bytes] = None, 2616 file_meta: Optional["FileMetaDataset"] = None, 2617 is_implicit_VR: bool = True, 2618 is_little_endian: bool = True 2619 ) -> None: 2620 """Initialize a :class:`FileDataset` read from a DICOM file. 2621 2622 Parameters 2623 ---------- 2624 filename_or_obj : str or PathLike or BytesIO or None 2625 Full path and filename to the file, memory buffer object, or 2626 ``None`` if is a :class:`io.BytesIO`. 2627 dataset : Dataset or dict 2628 Some form of dictionary, usually a :class:`Dataset` returned from 2629 :func:`~pydicom.filereader.dcmread`. 2630 preamble : bytes or str, optional 2631 The 128-byte DICOM preamble. 2632 file_meta : FileMetaDataset, optional 2633 The file meta :class:`FileMetaDataset`, such as the one returned by 2634 :func:`~pydicom.filereader.read_file_meta_info`, or an empty 2635 :class:`FileMetaDataset` if no file meta information is in the 2636 file. 2637 is_implicit_VR : bool, optional 2638 ``True`` (default) if implicit VR transfer syntax used; ``False`` 2639 if explicit VR. 2640 is_little_endian : bool 2641 ``True`` (default) if little-endian transfer syntax used; ``False`` 2642 if big-endian. 2643 """ 2644 Dataset.__init__(self, dataset) 2645 self.preamble = preamble 2646 self.file_meta: "FileMetaDataset" = ( 2647 file_meta if file_meta is not None else FileMetaDataset() 2648 ) 2649 self.is_implicit_VR: bool = is_implicit_VR 2650 self.is_little_endian: bool = is_little_endian 2651 2652 filename: Optional[str] = None 2653 filename_or_obj = path_from_pathlike(filename_or_obj) 2654 self.fileobj_type: Any 2655 self.filename: Union[str, BinaryIO] 2656 2657 if isinstance(filename_or_obj, str): 2658 filename = filename_or_obj 2659 self.fileobj_type = open 2660 elif isinstance(filename_or_obj, io.BufferedReader): 2661 filename = filename_or_obj.name 2662 # This is the appropriate constructor for io.BufferedReader 2663 self.fileobj_type = open 2664 else: 2665 # use __class__ python <2.7?; 2666 # http://docs.python.org/reference/datamodel.html 2667 self.fileobj_type = filename_or_obj.__class__ 2668 if hasattr(filename_or_obj, "name"): 2669 filename = filename_or_obj.name 2670 elif hasattr(filename_or_obj, "filename"): 2671 filename = ( 2672 filename_or_obj.filename # type: ignore[attr-defined] 2673 ) 2674 else: 2675 # e.g. came from BytesIO or something file-like 2676 self.filename = filename_or_obj 2677 2678 self.timestamp = None 2679 if filename: 2680 self.filename = filename 2681 if os.path.exists(filename): 2682 statinfo = os.stat(filename) 2683 self.timestamp = statinfo.st_mtime 2684 2685 def _copy_implementation(self, copy_function: Callable) -> "FileDataset": 2686 """Implementation of ``__copy__`` and ``__deepcopy__``. 2687 Sets the filename to ``None`` if it isn't a string, 2688 and copies all other attributes using `copy_function`. 2689 """ 2690 copied = self.__class__( 2691 self.filename, self, self.preamble, self.file_meta, 2692 self.is_implicit_VR, self.is_little_endian 2693 ) 2694 filename = self.filename 2695 if filename is not None and not isinstance(filename, str): 2696 warnings.warn("The 'filename' attribute of the dataset is a " 2697 "file-like object and will be set to None " 2698 "in the copied object") 2699 self.filename = None # type: ignore[assignment] 2700 for (k, v) in self.__dict__.items(): 2701 copied.__dict__[k] = copy_function(v) 2702 2703 self.filename = filename 2704 2705 return copied 2706 2707 def __copy__(self) -> "FileDataset": 2708 """Return a shallow copy of the file dataset. 2709 Make sure that the filename is not copied in case it is a file-like 2710 object. 2711 2712 Returns 2713 ------- 2714 FileDataset 2715 A shallow copy of the file data set. 2716 """ 2717 return self._copy_implementation(copy.copy) 2718 2719 def __deepcopy__(self, _: Optional[Dict[int, Any]]) -> "FileDataset": 2720 """Return a deep copy of the file dataset. 2721 Make sure that the filename is not copied in case it is a file-like 2722 object. 2723 2724 Returns 2725 ------- 2726 FileDataset 2727 A deep copy of the file data set. 2728 """ 2729 return self._copy_implementation(copy.deepcopy) 2730 2731 2732def validate_file_meta( 2733 file_meta: "FileMetaDataset", enforce_standard: bool = True 2734) -> None: 2735 """Validate the *File Meta Information* elements in `file_meta`. 2736 2737 .. versionchanged:: 1.2 2738 2739 Moved from :mod:`pydicom.filewriter`. 2740 2741 Parameters 2742 ---------- 2743 file_meta : Dataset 2744 The *File Meta Information* data elements. 2745 enforce_standard : bool, optional 2746 If ``False``, then only a check for invalid elements is performed. 2747 If ``True`` (default), the following elements will be added if not 2748 already present: 2749 2750 * (0002,0001) *File Meta Information Version* 2751 * (0002,0012) *Implementation Class UID* 2752 * (0002,0013) *Implementation Version Name* 2753 2754 and the following elements will be checked: 2755 2756 * (0002,0002) *Media Storage SOP Class UID* 2757 * (0002,0003) *Media Storage SOP Instance UID* 2758 * (0002,0010) *Transfer Syntax UID* 2759 2760 Raises 2761 ------ 2762 ValueError 2763 If `enforce_standard` is ``True`` and any of the checked *File Meta 2764 Information* elements are missing from `file_meta`. 2765 ValueError 2766 If any non-Group 2 Elements are present in `file_meta`. 2767 """ 2768 # Check that no non-Group 2 Elements are present 2769 for elem in file_meta.elements(): 2770 if elem.tag.group != 0x0002: 2771 raise ValueError("Only File Meta Information Group (0002,eeee) " 2772 "elements must be present in 'file_meta'.") 2773 2774 if enforce_standard: 2775 if 'FileMetaInformationVersion' not in file_meta: 2776 file_meta.FileMetaInformationVersion = b'\x00\x01' 2777 2778 if 'ImplementationClassUID' not in file_meta: 2779 file_meta.ImplementationClassUID = UID(PYDICOM_IMPLEMENTATION_UID) 2780 2781 if 'ImplementationVersionName' not in file_meta: 2782 file_meta.ImplementationVersionName = ( 2783 'PYDICOM ' + ".".join(str(x) for x in __version_info__)) 2784 2785 # Check that required File Meta Information elements are present 2786 missing = [] 2787 for element in [0x0002, 0x0003, 0x0010]: 2788 if Tag(0x0002, element) not in file_meta: 2789 missing.append(Tag(0x0002, element)) 2790 if missing: 2791 msg = ("Missing required File Meta Information elements from " 2792 "'file_meta':\n") 2793 for tag in missing: 2794 msg += '\t{0} {1}\n'.format(tag, keyword_for_tag(tag)) 2795 raise ValueError(msg[:-1]) # Remove final newline 2796 2797 2798class FileMetaDataset(Dataset): 2799 """Contains a collection (dictionary) of group 2 DICOM Data Elements. 2800 2801 .. versionadded:: 2.0 2802 2803 Derived from :class:`~pydicom.dataset.Dataset`, but only allows 2804 Group 2 (File Meta Information) data elements 2805 """ 2806 2807 def __init__(self, *args: _DatasetType, **kwargs: Any) -> None: 2808 """Initialize a FileMetaDataset 2809 2810 Parameters are as per :class:`Dataset`; this overrides the super class 2811 only to check that all are group 2 data elements 2812 2813 Raises 2814 ------ 2815 ValueError 2816 If any data elements are not group 2. 2817 TypeError 2818 If the passed argument is not a :class:`dict` or :class:`Dataset` 2819 """ 2820 super().__init__(*args, **kwargs) 2821 FileMetaDataset.validate(self._dict) 2822 2823 # Set type hints for the possible contents - VR, Type (1|1C|3) 2824 self.FileMetaInformationGroupLength: int # UL, 1 2825 self.FileMetaInformationVersion: bytes # OB, 1 2826 self.MediaStorageSOPClassUID: UID # UI, 1 2827 self.MediaStorageSOPInstanceUID: UID # UI, 1 2828 self.TransferSyntaxUID: UID # UI, 1 2829 self.ImplementationClassUID: UID # UI, 1 2830 self.ImplementationVersionName: Optional[str] # SH, 3 2831 self.SourceApplicationEntityTitle: Optional[str] # AE, 3 2832 self.SendingApplicationEntityTitle: Optional[str] # AE, 3 2833 self.ReceivingApplicationEntityTitle: Optional[str] # AE, 3 2834 self.SourcePresentationAddress: Optional[str] # UR, 3 2835 self.ReceivingPresentationAddress: Optional[str] # UR, 3 2836 self.PrivateInformationCreatorUID: Optional[UID] # UI, 3 2837 self.PrivateInformation: bytes # OB, 1C 2838 2839 @staticmethod 2840 def validate(init_value: _DatasetType) -> None: 2841 """Raise errors if initialization value is not acceptable for file_meta 2842 2843 Parameters 2844 ---------- 2845 init_value: dict or Dataset 2846 The tag:data element pairs to initialize a file meta dataset 2847 2848 Raises 2849 ------ 2850 TypeError 2851 If the passed argument is not a :class:`dict` or :class:`Dataset` 2852 ValueError 2853 If any data elements passed are not group 2. 2854 """ 2855 if init_value is None: 2856 return 2857 2858 if not isinstance(init_value, (Dataset, dict)): 2859 raise TypeError( 2860 "Argument must be a dict or Dataset, not {}".format( 2861 type(init_value) 2862 ) 2863 ) 2864 2865 non_group2 = [ 2866 Tag(tag) for tag in init_value.keys() if Tag(tag).group != 2 2867 ] 2868 if non_group2: 2869 msg = "Attempted to set non-group 2 elements: {}" 2870 raise ValueError(msg.format(non_group2)) 2871 2872 def __setitem__( 2873 self, key: Union[slice, TagType], value: _DatasetValue 2874 ) -> None: 2875 """Override parent class to only allow setting of group 2 elements. 2876 2877 Parameters 2878 ---------- 2879 key : int or Tuple[int, int] or str 2880 The tag for the element to be added to the Dataset. 2881 value : dataelem.DataElement or dataelem.RawDataElement 2882 The element to add to the :class:`FileMetaDataset`. 2883 2884 Raises 2885 ------ 2886 ValueError 2887 If `key` is not a DICOM Group 2 tag. 2888 """ 2889 2890 if isinstance(value.tag, BaseTag): 2891 tag = value.tag 2892 else: 2893 tag = Tag(value.tag) 2894 2895 if tag.group != 2: 2896 raise ValueError( 2897 "Only group 2 data elements are allowed in a FileMetaDataset" 2898 ) 2899 2900 super().__setitem__(key, value) 2901 2902 2903_RE_CAMEL_CASE = re.compile( 2904 # Ensure mix of upper and lowercase and digits, no underscores 2905 # If first character is lowercase ensure at least one uppercase char 2906 "(?P<start>(^[A-Za-z])((?=.+?[A-Z])[A-Za-z0-9]+)|(^[A-Z])([A-Za-z0-9]+))" 2907 "(?P<last>[A-Za-z0-9][^_]$)" # Last character is alphanumeric 2908) 2909