Skip to content
Commits on Source (4)
February 19, 2019 - v0.8.17
Add support for ICC profile raw data.
Fix parsing of negative resolution box exponents.
September 18, 2018 - v0.8.16
Qualify on Python 3.7.
Fix documentation typo.
......
Metadata-Version: 1.1
Name: Glymur
Version: 0.8.16
Version: 0.8.17
Summary: Tools for accessing JPEG2000 files
Home-page: https://github.com/quintusdias/glymur
Author: John Evans
......
Metadata-Version: 1.1
Name: Glymur
Version: 0.8.16
Version: 0.8.17
Summary: Tools for accessing JPEG2000 files
Home-page: https://github.com/quintusdias/glymur
Author: John Evans
......
glymur (0.8.17-1) unstable; urgency=medium
* New upstream release.
-- Antonio Valentino <antonio.valentino@tiscali.it> Thu, 21 Feb 2019 08:44:36 +0000
glymur (0.8.16-1) unstable; urgency=medium
* Initial version (Closes: #917823)
......
......@@ -2,6 +2,16 @@
Changes in glymur 0.8
=====================
Changes in 0.8.17
=================
* Fix parsing of resolution box with negative exponents.
* Add support for ICC profile buffers. The undecoded ICC profile
can be accessed via the "icc_profile_data" member of a
ColourSpecification box. In version 0.9.0, this will change to
"icc_profile" to store the raw data, and the old dictionary
for ICC profile metadata will be accessed as
"icc_profile_header".
Changes in 0.8.16
=================
* Update for Python 3.7.
......
......@@ -790,8 +790,12 @@ class Codestream(object):
self._tile_offset.append(segment.offset)
if segment.psot == 0:
tile_part_length = (self.offset + self.length -
segment.offset - 2)
tile_part_length = (
self.offset
+ self.length
- segment.offset
- 2
)
else:
tile_part_length = segment.psot
self._tile_length.append(tile_part_length)
......
......@@ -51,9 +51,9 @@ def load_openjpeg_library(libname):
if path is not None:
return load_library_handle(libname, path)
if ((('Anaconda' in sys.version) or
('Continuum Analytics, Inc.' in sys.version) or
('packaged by conda-forge' in sys.version))):
if ((('Anaconda' in sys.version)
or ('Continuum Analytics, Inc.' in sys.version)
or ('packaged by conda-forge' in sys.version))):
# If Anaconda, then openjpeg may have been installed via conda.
if platform.system() in ['Linux', 'Darwin']:
suffix = '.so' if platform.system() == 'Linux' else '.dylib'
......
......@@ -311,6 +311,9 @@ class ColourSpecificationBox(Jp2kBox):
colorspace : int or None
Enumerated colorspace, corresponds to one of 'sRGB', 'greyscale', or
'YCC'. If not None, then icc_profile must be None.
icc_profile_data : bytes
Raw ICC profile which may be read by software capable of interpreting
ICC profiles.
icc_profile : dict
ICC profile header according to ICC profile specification. If
colorspace is not None, then icc_profile must be empty.
......@@ -320,7 +323,7 @@ class ColourSpecificationBox(Jp2kBox):
def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0,
approximation=0, colorspace=None, icc_profile=None,
length=0, offset=-1):
icc_profile_data=None, length=0, offset=-1):
Jp2kBox.__init__(self)
self.method = method
......@@ -328,7 +331,14 @@ class ColourSpecificationBox(Jp2kBox):
self.approximation = approximation
self.colorspace = colorspace
self.icc_profile = icc_profile
self.icc_profile_data = icc_profile_data
if self.icc_profile is None and icc_profile_data is not None:
# Form the ordered dict from the raw data.
profile = _ICCProfile(icc_profile_data)
self.icc_profile = profile.header
self.length = length
self.offset = offset
......@@ -402,7 +412,7 @@ class ColourSpecificationBox(Jp2kBox):
text = 'Precedence: {0}'.format(self.precedence)
lst.append(text)
if self.approximation is not 0:
if self.approximation != 0:
try:
dispvalue = _APPROXIMATION_MEASURES[self.approximation]
except KeyError:
......@@ -479,24 +489,26 @@ class ColourSpecificationBox(Jp2kBox):
msg = msg.format(colorspace=colorspace)
warnings.warn(msg, UserWarning)
icc_profile = None
icc_profile_data = None
else:
# ICC profile
colorspace = None
icc_profile = None
if (num_bytes - 3) < 128:
msg = ("ICC profile header is corrupt, length is "
"only {length} when it should be at least 128.")
warnings.warn(msg.format(length=num_bytes - 3), UserWarning)
icc_profile = None
icc_profile_data = None
else:
profile = _ICCProfile(read_buffer[3:])
icc_profile = profile.header
icc_profile_data = read_buffer[3:]
return cls(method=method,
precedence=precedence,
approximation=approximation,
colorspace=colorspace,
icc_profile=icc_profile,
icc_profile_data=icc_profile_data,
length=length,
offset=offset)
......@@ -541,8 +553,8 @@ class ChannelDefinitionBox(Jp2kBox):
def _validate(self, writing=False):
"""Verify that the box obeys the specifications."""
# channel type and association must be specified.
if not ((len(self.index) == len(self.channel_type)) and
(len(self.channel_type) == len(self.association))):
if not ((len(self.index) == len(self.channel_type))
and (len(self.channel_type) == len(self.association))):
msg = ("The length of the index ({index}), channel_type "
"({channel_type}), and association ({association}) inputs "
"must be the same.")
......@@ -1362,8 +1374,8 @@ class FragmentListBox(Jp2kBox):
def _validate(self, writing=False):
"""Validate internal correctness."""
if (((len(self.fragment_offset) != len(self.fragment_length)) or
(len(self.fragment_length) != len(self.data_reference)))):
if (((len(self.fragment_offset) != len(self.fragment_length))
or (len(self.fragment_length) != len(self.data_reference)))):
msg = ("The lengths of the fragment offsets ({len_offsets}), "
"fragment lengths ({len_fragments}), and "
"data reference items ({len_drefs}) must be the same.")
......@@ -2040,8 +2052,8 @@ class PaletteBox(Jp2kBox):
def _validate(self, writing=False):
"""Verify that the box obeys the specifications."""
if ((len(self.bits_per_component) != len(self.signed)) or
(len(self.signed) != self.palette.shape[1])):
if (((len(self.bits_per_component) != len(self.signed))
or (len(self.signed) != self.palette.shape[1]))):
msg = ("The length of the 'bits_per_component' and the 'signed' "
"members must equal the number of columns of the palette.")
self._dispatch_validation_error(msg, writing=writing)
......@@ -2581,7 +2593,7 @@ class CaptureResolutionBox(Jp2kBox):
Instance of the current capture resolution box.
"""
read_buffer = fptr.read(10)
(rn1, rd1, rn2, rd2, re1, re2) = struct.unpack('>HHHHBB', read_buffer)
(rn1, rd1, rn2, rd2, re1, re2) = struct.unpack('>HHHHbb', read_buffer)
vres = rn1 / rd1 * math.pow(10, re1)
hres = rn2 / rd2 * math.pow(10, re2)
......@@ -2657,7 +2669,7 @@ class DisplayResolutionBox(Jp2kBox):
"""
read_buffer = fptr.read(10)
(rn1, rd1, rn2, rd2, re1, re2) = struct.unpack('>HHHHBB', read_buffer)
(rn1, rd1, rn2, rd2, re1, re2) = struct.unpack('>HHHHbb', read_buffer)
vres = rn1 / rd1 * math.pow(10, re1)
hres = rn2 / rd2 * math.pow(10, re2)
......@@ -3452,8 +3464,8 @@ class UUIDBox(Jp2kBox):
# transform the point into georeferenced coordinates
geo_transform = hDataset.GetGeoTransform(can_return_null=True)
dfGeoX = (geo_transform[0] + geo_transform[1] * x +
geo_transform[2] * y)
dfGeoX = (geo_transform[0] + geo_transform[1] * x
+ geo_transform[2] * y)
dfGeoY = geo_transform[3] + geo_transform[4] * x
dfGeoY += geo_transform[5] * y
......
......@@ -307,8 +307,9 @@ class Jp2k(Jp2kBox):
box_length = values[0]
box_id = values[1]
signature = values[2:]
if (((box_length != 12) or (box_id != b'jP ') or
(signature != (13, 10, 135, 10)))):
if (((box_length != 12)
or (box_id != b'jP ')
or (signature != (13, 10, 135, 10)))):
msg = '{filename} is not a JPEG 2000 file.'
msg = msg.format(filename=self.filename)
raise IOError(msg)
......@@ -400,8 +401,8 @@ class Jp2k(Jp2kBox):
"""
other_args = (mct, cratios, psnr, irreversible, cbsize, eph,
grid_offset, modesw, numres, prog, psizes, sop, subsam)
if (((cinema2k is not None or cinema4k is not None) and
(not all([arg is None for arg in other_args])))):
if (((cinema2k is not None or cinema4k is not None)
and (not all([arg is None for arg in other_args])))):
msg = ("Cannot specify cinema2k/cinema4k along with any other "
"options.")
raise IOError(msg)
......@@ -416,8 +417,8 @@ class Jp2k(Jp2kBox):
"argument, it must be in the final position.")
raise IOError(msg)
if (((0 in psnr and np.any(np.diff(psnr[:-1]) < 0)) or
(0 not in psnr and np.any(np.diff(psnr) < 0)))):
if (((0 in psnr and np.any(np.diff(psnr[:-1]) < 0))
or (0 not in psnr and np.any(np.diff(psnr) < 0)))):
msg = ("PSNR values must be increasing, with one exception - "
"zero may be in the final position to indicate a "
"lossless layer.")
......@@ -561,12 +562,16 @@ class Jp2k(Jp2kBox):
# set image offset and reference grid
image.contents.x0 = self._cparams.image_offset_x0
image.contents.y0 = self._cparams.image_offset_y0
image.contents.x1 = (image.contents.x0 +
(numcols - 1) * self._cparams.subsampling_dx +
1)
image.contents.y1 = (image.contents.y0 +
(numrows - 1) * self._cparams.subsampling_dy +
1)
image.contents.x1 = (
image.contents.x0
+ (numcols - 1) * self._cparams.subsampling_dx
+ 1
)
image.contents.y1 = (
image.contents.y0
+ (numrows - 1) * self._cparams.subsampling_dy
+ 1
)
# Stage the image data to the openjpeg data structure.
for k in range(0, numlayers):
......@@ -635,8 +640,8 @@ class Jp2k(Jp2kBox):
msg = msg.format(height=height, width=width,
area=height * width)
raise IOError(msg)
if ((math.log(height, 2) != math.floor(math.log(height, 2)) or
math.log(width, 2) != math.floor(math.log(width, 2)))):
if ((math.log(height, 2) != math.floor(math.log(height, 2))
or math.log(width, 2) != math.floor(math.log(width, 2)))):
msg = ("Bad code block size ({height} x {width}). "
"The dimensions must be powers of 2.")
msg = msg.format(height=height, width=width)
......@@ -668,8 +673,8 @@ class Jp2k(Jp2kBox):
msg = msg.format(prch=prch, prcw=prcw,
cbh=height, cbw=width)
raise IOError(msg)
if ((math.log(prch, 2) != math.floor(math.log(prch, 2)) or
math.log(prcw, 2) != math.floor(math.log(prcw, 2)))):
if ((math.log(prch, 2) != math.floor(math.log(prch, 2))
or math.log(prcw, 2) != math.floor(math.log(prcw, 2)))):
msg = ("Bad precinct size ({height} x {width}). "
"Precinct dimensions must be powers of 2.")
msg = msg.format(height=prch, width=prcw)
......@@ -800,9 +805,9 @@ class Jp2k(Jp2kBox):
msg = "Only JP2 files can currently have boxes appended to them."
raise IOError(msg)
if not ((box.box_id == 'xml ') or
(box.box_id == 'uuid' and
box.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'))):
xmp_uuid = UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
if not ((box.box_id == 'xml ')
or (box.box_id == 'uuid' and box.uuid == xmp_uuid)):
msg = ("Only XML boxes and XMP UUID boxes can currently be "
"appended.")
raise IOError(msg)
......@@ -959,10 +964,11 @@ class Jp2k(Jp2kBox):
"""
Slicing protocol.
"""
if ((isinstance(index, slice) and
(index.start is None and
index.stop is None and
index.step is None)) or (index is Ellipsis)):
if (((isinstance(index, slice)
and (index.start is None
and index.stop is None
and index.step is None))
or (index is Ellipsis))):
# Case of jp2[:] = data, i.e. write the entire image.
#
# Should have a slice object where start = stop = step = None
......@@ -1034,9 +1040,11 @@ class Jp2k(Jp2kBox):
return self._read()
if isinstance(pargs, slice):
if (((pargs.start is None) and
(pargs.stop is None) and
(pargs.step is None))):
if (
pargs.start is None
and pargs.stop is None
and pargs.step is None
):
# Case of jp2[:]
return self._read()
......@@ -1605,10 +1613,16 @@ class Jp2k(Jp2kBox):
# set image offset and reference grid
image.contents.x0 = self._cparams.image_offset_x0
image.contents.y0 = self._cparams.image_offset_y0
image.contents.x1 = (image.contents.x0 +
(numcols - 1) * self._cparams.subsampling_dx + 1)
image.contents.y1 = (image.contents.y0 +
(numrows - 1) * self._cparams.subsampling_dy + 1)
image.contents.x1 = (
image.contents.x0
+ (numcols - 1) * self._cparams.subsampling_dx
+ 1
)
image.contents.y1 = (
image.contents.y0
+ (numrows - 1) * self._cparams.subsampling_dy
+ 1
)
# Stage the image data to the openjpeg data structure.
for k in range(0, num_comps):
......@@ -1782,8 +1796,8 @@ class Jp2k(Jp2kBox):
elif len(cdef_lst) == 1:
cdef = jp2h.box[cdef_lst[0]]
if colr.colorspace == core.SRGB:
if any([chan + 1 not in cdef.association or
cdef.channel_type[chan] != 0 for chan in [0, 1, 2]]):
if any([chan + 1 not in cdef.association
or cdef.channel_type[chan] != 0 for chan in [0, 1, 2]]):
msg = ("All color channels must be defined in the "
"channel definition box.")
raise IOError(msg)
......
......@@ -20,7 +20,7 @@ from .lib import openjpeg as opj, openjp2 as opj2
# Do not change the format of this next line! Doing so risks breaking
# setup.py
version = "0.8.16"
version = "0.8.17"
_sv = LooseVersion(version)
version_tuple = _sv.version
......
[flake8]
exclude = build
ignore = E402, E241
ignore = E402, E241, W503
[nosetests]
exclude = load_tests
......
......@@ -50,7 +50,8 @@ kwargs['classifiers'] = [
version_file = os.path.join('glymur', 'version.py')
with open(version_file, 'rt') as f:
contents = f.read()
match = re.search('version\s*=\s*"(?P<version>\d*.\d*.\d*.*)"\n', contents)
pattern = r'''version\s*=\s*"(?P<version>\d*.\d*.\d*.*)"\n'''
match = re.search(pattern, contents)
kwargs['version'] = match.group('version')
setup(**kwargs)
......@@ -28,7 +28,7 @@ from glymur import Jp2k
from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox
from glymur.jp2box import FileTypeBox, ImageHeaderBox, JP2HeaderBox
from glymur.jp2box import JPEG2000SignatureBox, BitsPerComponentBox
from glymur.jp2box import PaletteBox, UnknownBox
from glymur.jp2box import PaletteBox, UnknownBox, CaptureResolutionBox
from glymur.core import COLOR, OPACITY, SRGB, GREYSCALE
from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE
from .fixtures import WINDOWS_TMP_FILE_MSG, MetadataBase
......@@ -110,9 +110,9 @@ class TestDataEntryURL(unittest.TestCase):
with open(tfile.name, 'rb') as fptr:
fptr.seek(jp22.box[-1].offset + 4 + 4 + 1 + 3)
nbytes = (jp22.box[-1].offset +
jp22.box[-1].length -
fptr.tell())
nbytes = (jp22.box[-1].offset
+ jp22.box[-1].length
- fptr.tell())
read_buffer = fptr.read(nbytes)
read_url = read_buffer.decode('utf-8')
self.assertEqual(url + chr(0), read_url)
......@@ -488,6 +488,25 @@ class TestColourSpecificationBox(unittest.TestCase):
self.assertEqual(colr.colorspace, SRGB)
self.assertIsNone(colr.icc_profile)
def test_icc_profile_data(self):
"""basic colr box with ICC profile"""
relpath = os.path.join('data', 'sgray.icc')
iccfile = pkg.resource_filename(__name__, relpath)
with open(iccfile, mode='rb') as f:
raw_icc_profile = f.read()
colr = ColourSpecificationBox(icc_profile_data=raw_icc_profile)
self.assertEqual(colr.method, glymur.core.ENUMERATED_COLORSPACE)
self.assertEqual(colr.precedence, 0)
self.assertEqual(colr.approximation, 0)
self.assertEqual(colr.icc_profile['Version'], '2.1.0')
self.assertEqual(colr.icc_profile['Color Space'], 'gray')
self.assertIsNone(colr.icc_profile['Datetime'])
self.assertEqual(len(colr.icc_profile_data), 416)
def test_colr_with_bad_color(self):
"""colr must have a valid color, strange as though that may sound."""
colorspace = -1
......@@ -719,6 +738,7 @@ class TestWrap(unittest.TestCase):
self.assertEqual(jp2.box[2].box[1].approximation, 0)
self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB)
self.assertIsNone(jp2.box[2].box[1].icc_profile)
self.assertIsNone(jp2.box[2].box[1].icc_profile_data)
def test_wrap(self):
"""basic test for rewrapping a j2c file, no specified boxes"""
......@@ -986,8 +1006,8 @@ class TestWrap(unittest.TestCase):
"""Rewrap a jpx file."""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
jpx = Jp2k(self.jpxfile)
idx = (list(range(5)) +
list(range(9, 12)) + list(range(6, 9))) + [12]
idx = (list(range(5)) + list(range(9, 12)) + list(range(6, 9))
+ [12])
boxes = [jpx.box[j] for j in idx]
jpx2 = jpx.wrap(tfile1.name, boxes=boxes)
exp_ids = [box.box_id for box in boxes]
......@@ -1005,6 +1025,25 @@ class TestJp2Boxes(unittest.TestCase):
def setUp(self):
self.jpxfile = glymur.data.jpxfile()
def test_capture_resolution_with_negative_exponents(self):
"""
SCENARIO: The cres box has negative exponents and unity for the
numerator and denominator of the resolutions.
EXPECTED RESULT: The resolutions are less than one.
"""
b = BytesIO()
pargs = (18, b'cres', 1, 1, 1, 1, -2, -2)
buffer = struct.pack('>I4s4H2b', *pargs)
b.write(buffer)
b.seek(8)
# Now try to parse
box = CaptureResolutionBox.parse(b, 8, 18)
self.assertEqual(box.vertical_resolution, 0.01)
self.assertEqual(box.horizontal_resolution, 0.01)
def test_default_jp2k(self):
"""Should be able to instantiate a JPEG2000SignatureBox"""
jp2k = glymur.jp2box.JPEG2000SignatureBox()
......@@ -1153,6 +1192,7 @@ class TestRepr(MetadataBase):
self.assertEqual(newbox.approximation, 0)
self.assertEqual(newbox.colorspace, glymur.core.SRGB)
self.assertIsNone(newbox.icc_profile)
self.assertIsNone(newbox.icc_profile_data)
def test_channeldefinition_box(self):
"""Verify __repr__ method on cdef box."""
......@@ -1296,9 +1336,9 @@ class TestRepr(MetadataBase):
tree = ET.ElementTree(elt)
box = glymur.jp2box.XMLBox(xml=tree)
regexp = r"""glymur.jp2box.XMLBox"""
regexp += r"""[(]xml=<lxml.etree._ElementTree\sobject\s"""
regexp += """at\s0x([a-fA-F0-9]*)>[)]"""
regexp = r'''glymur.jp2box.XMLBox'''
regexp += r'''[(]xml=<lxml.etree._ElementTree\sobject\s'''
regexp += r'''at\s0x([a-fA-F0-9]*)>[)]'''
if sys.hexversion < 0x03000000:
self.assertRegexpMatches(repr(box), regexp)
......@@ -1328,10 +1368,10 @@ class TestRepr(MetadataBase):
# Since the raw_data parameter is a sequence of bytes which could be
# quite long, don't bother trying to make it conform to eval(repr()).
regexp = r"""glymur.jp2box.UUIDBox\("""
regexp += """the_uuid="""
regexp += """UUID\('00000000-0000-0000-0000-000000000000'\),\s"""
regexp += """raw_data=<byte\sarray\s10\selements>\)"""
regexp = r'''glymur.jp2box.UUIDBox\('''
regexp += r'''the_uuid='''
regexp += r'''UUID\('00000000-0000-0000-0000-000000000000'\),\s'''
regexp += r'''raw_data=<byte\sarray\s10\selements>\)'''
if sys.hexversion < 0x03000000:
self.assertRegexpMatches(repr(box), regexp)
......@@ -1346,10 +1386,10 @@ class TestRepr(MetadataBase):
# Since the raw_data parameter is a sequence of bytes which could be
# quite long, don't bother trying to make it conform to eval(repr()).
regexp = r"""glymur.jp2box.UUIDBox\("""
regexp += """the_uuid="""
regexp += """UUID\('be7acfcb-97a9-42e8-9c71-999491e3afac'\),\s"""
regexp += """raw_data=<byte\sarray\s3122\selements>\)"""
regexp = r'''glymur.jp2box.UUIDBox\('''
regexp += r'''the_uuid='''
regexp += r'''UUID\('be7acfcb-97a9-42e8-9c71-999491e3afac'\),\s'''
regexp += r'''raw_data=<byte\sarray\s3122\selements>\)'''
if sys.hexversion < 0x03000000:
self.assertRegexpMatches(repr(box), regexp)
......@@ -1363,9 +1403,9 @@ class TestRepr(MetadataBase):
box = jp2.box[-1]
# Difficult to eval(repr()) this, so just match the general pattern.
regexp = "glymur.jp2box.ContiguousCodeStreamBox"
regexp += "[(]codestream=<glymur.codestream.Codestream\sobject\s"
regexp += "at\s0x([a-fA-F0-9]*)>[)]"
regexp = r'''glymur.jp2box.ContiguousCodeStreamBox'''
regexp += r'''[(]codestream=<glymur.codestream.Codestream\sobject\s'''
regexp += r'''at\s0x([a-fA-F0-9]*)>[)]'''
if sys.hexversion < 0x03000000:
self.assertRegexpMatches(repr(box), regexp)
......
......@@ -2099,8 +2099,8 @@ class TestJp2k_1_x(unittest.TestCase):
with self.assertRaises(IOError):
j2k.layer = 1
@unittest.skipIf(((glymur.lib.openjpeg.OPENJPEG is None) or
(glymur.lib.openjpeg.version() < '1.5.0')),
@unittest.skipIf(glymur.lib.openjpeg.OPENJPEG is None
or glymur.lib.openjpeg.version() < '1.5.0',
"OpenJPEG version one must be present")
def test_read_version_15(self):
"""
......
......@@ -170,10 +170,10 @@ class TestOpenJP2(unittest.TestCase):
def tile_encoder(**kwargs):
"""Fixture used by many tests."""
num_tiles = ((kwargs['image_width'] / kwargs['tile_width']) *
(kwargs['image_height'] / kwargs['tile_height']))
tile_size = ((kwargs['tile_width'] * kwargs['tile_height']) *
(kwargs['num_comps'] * kwargs['comp_prec'] / 8))
num_tiles = ((kwargs['image_width'] / kwargs['tile_width'])
* (kwargs['image_height'] / kwargs['tile_height']))
tile_size = ((kwargs['tile_width'] * kwargs['tile_height'])
* (kwargs['num_comps'] * kwargs['comp_prec'] / 8))
data = np.random.random((kwargs['tile_height'],
kwargs['tile_width'],
......
......@@ -1581,6 +1581,6 @@ class TestJp2dump(unittest.TestCase):
# The "CME marker segment" part is the last segment in the codestream
# header.
pattern = 'JPEG\s2000.*?CME\smarker\ssegment.*?UserWarning'
pattern = r'''JPEG\s2000.*?CME\smarker\ssegment.*?UserWarning'''
r = re.compile(pattern, re.DOTALL)
self.assertRegex(actual, r)