# Copyright 2008-2017 pydicom authors. See LICENSE file for details.
"""Functions for handling DICOM unique identifiers (UIDs)"""
import os
import uuid
import random
import hashlib
import re
import warnings
from pydicom._uid_dict import UID_dictionary
from pydicom import compat
# Many thanks to the Medical Connections for offering free
# valid UIDs (http://www.medicalconnections.co.uk/FreeUID.html)
# Their service was used to obtain the following root UID for pydicom:
PYDICOM_ROOT_UID = '1.2.826.0.1.3680043.8.498.'
PYDICOM_IMPLEMENTATION_UID = PYDICOM_ROOT_UID + '1'
# Regexes for valid UIDs and valid UID prefixes
RE_VALID_UID = r'^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*$'
RE_VALID_UID_PREFIX = r'^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*\.$'
[docs]class UID(str):
"""Subclass python string so have human-friendly UIDs.
Example
-------
>>> from pydicom.uid import UID
>>> uid = UID('1.2.840.10008.1.2.4.50')
>>> uid
'1.2.840.10008.1.2.4.50'
>>> uid.is_implicit_VR
False
>>> uid.is_little_endian
True
>>> uid.is_transfer_syntax
True
>>> uid.name
'JPEG Baseline (Process 1)'
"""
def __new__(cls, val):
"""Setup new instance of the class.
Parameters
----------
val : str or pydicom.uid.UID
The UID string to use to create the UID object.
Returns
-------
pydicom.uid.UID
The UID object.
"""
# Don't repeat if already a UID class then may get the name that
# str(uid) gives rather than the dotted number
if isinstance(val, UID):
return val
if isinstance(val, compat.string_types):
return super(UID, cls).__new__(cls, val.strip())
raise TypeError("UID must be a string")
def __eq__(self, other):
"""Return True if `self` or `self.name` equals `other`."""
# TODO: v1.2 - The __ne__ override is deprecated
if isinstance(other, str) and other and '.' not in other:
msg = "The equality test for \"UID == '{0}'\" is deprecated and " \
"will be removed in pydicom v1.2. In the future use " \
"\"UID.name == '{0}'\"".format(other)
warnings.warn(msg, DeprecationWarning)
if str.__eq__(self, other) is True: # 'is True' needed (issue 96)
return True
if str.__eq__(self.name, other) is True: # 'is True' needed (issue 96)
return True
return False
def __ne__(self, other):
"""Return True if `self` or `self.name` does not equal `other`."""
# TODO: v1.2 - The __ne__ override is deprecated
if isinstance(other, str) and other and '.' not in other:
msg = "The equality test for \"UID != '{0}'\" is deprecated and " \
"will be removed in pydicom v1.2. In the future use " \
"\"UID.name != '{0}'\"".format(other)
warnings.warn(msg, DeprecationWarning)
if str.__eq__(self, other) is True:
return False
if str.__eq__(self.name, other) is True:
return False
return True
def __hash__(self):
"""Return the hash of `self`."""
# TODO: v1.2 - The __hash__ override is deprecated
# For python 3, any override of __cmp__ or __eq__
# immutable requires explicit redirect of hash
# function to the parent class
# See http://docs.python.org/dev/3.0/
# reference/datamodel.html#object.__hash__
return super(UID, self).__hash__()
@property
def is_implicit_VR(self):
"""Return True if an implicit VR transfer syntax UID."""
if self.is_transfer_syntax:
# Implicit VR Little Endian
if self == '1.2.840.10008.1.2':
return True
# Explicit VR Little Endian
# Explicit VR Big Endian
# Deflated Explicit VR Little Endian
# All encapsulated transfer syntaxes
return False
raise ValueError('UID is not a transfer syntax.')
@property
def is_little_endian(self):
"""Return True if a little endian transfer syntax UID."""
if self.is_transfer_syntax:
# Explicit VR Big Endian
if self == '1.2.840.10008.1.2.2':
return False
# Explicit VR Little Endian
# Implicit VR Little Endian
# Deflated Explicit VR Little Endian
# All encapsulated transfer syntaxes
return True
raise ValueError('UID is not a transfer syntax.')
@property
def is_transfer_syntax(self):
"""Return True if a transfer syntax UID."""
if not self.is_private:
return self.type == "Transfer Syntax"
raise ValueError("Can't determine UID type for private UIDs.")
@property
def is_deflated(self):
"""Return True if a deflated transfer syntax UID."""
if self.is_transfer_syntax:
# Deflated Explicit VR Little Endian
if self == '1.2.840.10008.1.2.1.99':
return True
# Explicit VR Little Endian
# Implicit VR Little Endian
# Explicit VR Big Endian
# All encapsulated transfer syntaxes
return False
raise ValueError('UID is not a transfer syntax.')
@property
def is_encapsulated(self):
"""Return True if an encasulated transfer syntax UID."""
return self.is_compressed
@property
def is_compressed(self):
"""Return True if a compressed transfer syntax UID."""
if self.is_transfer_syntax:
# Explicit VR Little Endian
# Implicit VR Little Endian
# Explicit VR Big Endian
# Deflated Explicit VR Little Endian
if self in ['1.2.840.10008.1.2', '1.2.840.10008.1.2.1',
'1.2.840.10008.1.2.2', '1.2.840.10008.1.2.1.99']:
return False
# All encapsulated transfer syntaxes
return True
raise ValueError('UID is not a transfer syntax.')
@property
def name(self):
"""Return the UID name from the UID dictionary."""
uid_string = str.__str__(self)
if uid_string in UID_dictionary:
return UID_dictionary[self][0]
return uid_string
@property
def type(self):
"""Return the UID type from the UID dictionary."""
if str.__str__(self) in UID_dictionary:
return UID_dictionary[self][1]
return ''
@property
def info(self):
"""Return the UID info from the UID dictionary."""
if str.__str__(self) in UID_dictionary:
return UID_dictionary[self][2]
return ''
@property
def is_retired(self):
"""Return True if the UID is retired, False otherwise or if private."""
if str.__str__(self) in UID_dictionary:
return bool(UID_dictionary[self][3])
return False
@property
def is_private(self):
"""Return True if the UID isn't an officially registered DICOM UID."""
if self[:13] == '1.2.840.10008':
return False
return True
@property
def is_valid(self):
"""Return True if `self` is a valid UID, False otherwise."""
if len(self) <= 64 and re.match(RE_VALID_UID, self):
return True
return False
# Pre-defined Transfer Syntax UIDs (for convenience)
ExplicitVRLittleEndian = UID('1.2.840.10008.1.2.1')
ImplicitVRLittleEndian = UID('1.2.840.10008.1.2')
DeflatedExplicitVRLittleEndian = UID('1.2.840.10008.1.2.1.99')
ExplicitVRBigEndian = UID('1.2.840.10008.1.2.2')
JPEGBaseLineLossy8bit = UID('1.2.840.10008.1.2.4.50')
JPEGBaseLineLossy12bit = UID('1.2.840.10008.1.2.4.51')
JPEGLossless = UID('1.2.840.10008.1.2.4.70')
JPEGLSLossless = UID('1.2.840.10008.1.2.4.80')
JPEGLSLossy = UID('1.2.840.10008.1.2.4.81')
JPEG2000Lossless = UID('1.2.840.10008.1.2.4.90')
JPEG2000Lossy = UID('1.2.840.10008.1.2.4.91')
RLELossless = UID('1.2.840.10008.1.2.5')
UncompressedPixelTransferSyntaxes = [
ExplicitVRLittleEndian,
ImplicitVRLittleEndian,
DeflatedExplicitVRLittleEndian,
ExplicitVRBigEndian,
]
JPEGLSSupportedCompressedPixelTransferSyntaxes = [
JPEGLSLossless,
JPEGLSLossy,
]
PILSupportedCompressedPixelTransferSyntaxes = [
JPEGBaseLineLossy8bit,
JPEGLossless,
JPEGBaseLineLossy12bit,
JPEG2000Lossless,
JPEG2000Lossy,
]
JPEG2000CompressedPixelTransferSyntaxes = [
JPEG2000Lossless,
JPEG2000Lossy,
]
JPEGLossyCompressedPixelTransferSyntaxes = [
JPEGBaseLineLossy8bit,
JPEGBaseLineLossy12bit,
]
RLECompressedLosslessSyntaxes = [
RLELossless
]
[docs]def generate_uid(prefix=PYDICOM_ROOT_UID, entropy_srcs=None):
"""Return a 64 character UID which starts with `prefix`.
Parameters
----------
prefix : str or None
The UID prefix to use when creating the UID. Default is the pydicom
root UID '1.2.826.0.1.3680043.8.498.'. If None then a value of '2.25.'
will be used (as described on `David Clunie's website
<http://www.dclunie.com/medical-image-faq/html/part2.html#UID>`_).
entropy_srcs : list of str or None
If a list of str, the prefix will be appended with a SHA512 hash of the
list which means the result is deterministic and should make the
original data unrecoverable. If None random data will be used
(default).
Returns
-------
pydicom.uid.UID
A 64 character DICOM UID.
Raises
------
ValueError
If `prefix` is invalid or greater than 63 characters.
Example
-------
>>> from pydicom.uid import generate_uid
>>> generate_uid()
1.2.826.0.1.3680043.8.498.22463838056059845879389038257786771680
>>> generate_uid(prefix=None)
2.25.12586835699909622925962004639368649121731805922235633382942
>>> generate_uid(entropy_srcs=['lorem', 'ipsum'])
1.2.826.0.1.3680043.8.498.87507166259346337659265156363895084463
>>> generate_uid(entropy_srcs=['lorem', 'ipsum'])
1.2.826.0.1.3680043.8.498.87507166259346337659265156363895084463
"""
max_uid_len = 64
if prefix is None:
prefix = '2.25.'
if len(prefix) > max_uid_len - 1:
raise ValueError("The prefix must be less than 63 chars")
if not re.match(RE_VALID_UID_PREFIX, prefix):
raise ValueError("The prefix is not in a valid format")
avail_digits = max_uid_len - len(prefix)
if entropy_srcs is None:
entropy_srcs = [
str(uuid.uuid1()), # 128-bit from MAC/time/randomness
str(os.getpid()), # Current process ID
hex(random.getrandbits(64)) # 64 bits randomness
]
hash_val = hashlib.sha512(''.join(entropy_srcs).encode('utf-8'))
# Convert this to an int with the maximum available digits
dicom_uid = prefix + str(int(hash_val.hexdigest(), 16))[:avail_digits]
return UID(dicom_uid)