Merge branch 'devel' into issue40

This commit is contained in:
jevans 2013-06-15 21:42:37 -04:00
commit 596d502f15
11 changed files with 305 additions and 47 deletions

View file

@ -1,4 +1,5 @@
Jun 11, 2013 - Added reduce=-1 option to get lowest resolution thumbnail
Jun 11, 2013 - v0.1.8 Added reduce=-1 option to get lowest resolution
thumbnail.
Jun 07, 2013 - v0.1.7 Changed Exif dictionary names from ['Exif', 'Photo',
'Iop', 'GPSInfo'] to ['Image', 'Photo', 'Iop', 'GPSInfo'].

View file

@ -1,4 +1,3 @@
include *.txt *.md
recursive-include *.txt *.py
prune build
exclude readthedocs-pip-requirements.txt

View file

@ -78,7 +78,7 @@ copyright = u'2013, John Evans'
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1.7'
release = '0.1.8'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -7,7 +7,7 @@ Read the lowest resolution thumbnail?
=====================================
Printing the Jp2k object should reveal the number of resolutions (look in the
COD segment section), but you can take a shortcut by supplying -1 as the reduce
level.
level. ::
>>> import pkg_resources
>>> import glymur
@ -38,7 +38,7 @@ codestream box, only the main header is printed. It is possible to print
Work with XMP UUIDs?
====================
The example JP2 file shipped with glymur has an XMP UUID::
The example JP2 file shipped with glymur has an XMP UUID. ::
>>> import pkg_resources
>>> from glymur import Jp2k

View file

@ -12,6 +12,7 @@ References
"""
import copy
import datetime
import math
import os
import pprint
@ -153,9 +154,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 : byte array or None
ICC profile according to ICC profile specification. If not None, then
color_space must be None.
icc_profile : _ICCProfile or None
ICC profile header according to ICC profile specification. If not
None, then color_space must be None.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='Colour Specification')
@ -174,8 +175,7 @@ class ColourSpecificationBox(Jp2kBox):
x = _colorspace_map_display[self.colorspace]
msg += '\n Colorspace: {0}'.format(x)
else:
x = len(self.icc_profile)
msg += '\n ICC Profile: {0} bytes'.format(x)
msg += '\n ICC Profile: {0}'.format(self.icc_profile.__str__())
return msg
@ -221,12 +221,169 @@ class ColourSpecificationBox(Jp2kBox):
# ICC profile
kwargs['colorspace'] = None
n = offset + length - f.tell()
kwargs['icc_profile'] = f.read(n)
if n < 128:
msg = "ICC profile header is corrupt, length is "
msg += "only {0} instead of 128."
warnings.warn(msg.format(n), UserWarning)
kwargs['icc_profile'] = None
else:
icc_profile = _ICCProfile(f.read(n))
kwargs['icc_profile'] = icc_profile
box = ColourSpecificationBox(**kwargs)
return box
class _ICCProfile:
"""
"""
profile_class = {b'scnr': 'input device profile',
b'mntr': 'display device profile',
b'prtr': 'output device profile',
b'link': 'devicelink profile',
b'spac': 'colorspace conversion profile',
b'abst': 'abstract profile',
b'nmcl': 'name colour profile'}
colour_space_dict = {b'XYZ ': 'XYZ',
b'Lab ': 'Lab',
b'Luv ': 'Luv',
b'YCbr': 'YCbCr',
b'Yxy ': 'Yxy',
b'RGB ': 'RGB',
b'GRAY': 'gray',
b'HSV ': 'hsv',
b'HLS ': 'hls',
b'CMYK': 'CMYK',
b'CMY ': 'cmy',
b'2CLR': '2colour',
b'3CLR': '3colour',
b'4CLR': '4colour',
b'5CLR': '5colour',
b'6CLR': '6colour',
b'7CLR': '7colour',
b'8CLR': '8colour',
b'9CLR': '9colour',
b'ACLR': '10colour',
b'BCLR': '11colour',
b'CCLR': '12colour',
b'DCLR': '13colour',
b'ECLR': '14colour',
b'FCLR': '15colour'}
rendering_intent_dict = {0: 'perceptual',
1: 'media-relative colorimetric',
2: 'saturation',
3: 'ICC-absolute colorimetric'}
def __init__(self, buffer):
self._raw_buffer = buffer
self.size, = struct.unpack('>I', self._raw_buffer[0:4])
self.preferred_cmm_type, = struct.unpack('>I', self._raw_buffer[4:8])
data = struct.unpack('>BB', self._raw_buffer[8:10])
major = data[0]
minor = (data[1] & 0xf0) >> 4
bugfix = (data[1] & 0x0f)
self.version = '{0}.{1}.{2}'.format(major, minor, bugfix)
self.device_class = self.profile_class[self._raw_buffer[12:16]]
self.colour_space = self.colour_space_dict[self._raw_buffer[16:20]]
self.connection_space = self.colour_space_dict[self._raw_buffer[20:24]]
data = struct.unpack('>HHHHHH', self._raw_buffer[24:36])
self.datetime = datetime.datetime(*data)
self.file_signature = buffer[36:40].decode('utf-8')
if buffer[40:44] == b'\x00\x00\x00\x00':
self.platform = 'unrecognized'
else:
self.platform = buffer[40:44].decode('utf-8')
self.flags, = struct.unpack('>I', buffer[44:48])
self.device_manufacturer = buffer[48:52].decode('utf-8')
if buffer[52:56] == b'\x00\x00\x00\x00':
self.device_model = ''
else:
self.device_model = buffer[52:56].decode('utf-8')
self.device_attributes, = struct.unpack('>Q', buffer[56:64])
self.rendering_intent, = struct.unpack('>I', buffer[64:68])
data = struct.unpack('>iii', buffer[68:80])
self.illuminant = np.array(data, dtype=np.float64) / 65536
if buffer[80:84] == b'\x00\x00\x00\x00':
self.creator = 'unrecognized'
else:
self.creator = buffer[80:84].decode('utf-8')
self.profile_id = buffer[84:100]
self.reserved = buffer[100:127]
def __str__(self):
msg = "\n Size: {0}"
msg += "\n Preferred CMM type: {1:x}"
msg += "\n Version: {2}"
msg += "\n Device class signature: {3}"
msg += "\n Color space: {4}"
msg += "\n Connection space: {5}"
msg += "\n Creation time: {6}"
msg += "\n File signature: {7}"
msg += "\n Platform: {8}"
msg += "\n Flags: {9}"
msg += "\n Device manufacturer: {10}"
msg += "\n Device model: {11}"
msg += "\n Device attributes: {12}"
msg += "\n Rendering intent: {13}"
msg += "\n Illuminant: {14}"
msg += "\n Creator signature: {15}"
if self.flags & 0x01:
flag_string = 'embedded, '
else:
flag_string = 'not embedded, '
if self.flags & 0x02:
flag_string += 'cannot be used independently'
else:
flag_string += 'can be used independently'
if self.device_attributes & 0x01:
attr_string = 'transparency, '
else:
attr_string = 'reflective, '
if self.device_attributes & 0x02:
attr_string += 'matte, '
else:
attr_string += 'glossy, '
if self.device_attributes & 0x04:
attr_string += 'negative media polarity, '
else:
attr_string += 'positive media polarity, '
if self.device_attributes & 0x08:
attr_string += 'black and white media'
else:
attr_string += 'color media'
msg = msg.format(self.size,
self.preferred_cmm_type,
self.version,
self.device_class,
self.colour_space,
self.connection_space,
self.datetime,
self.file_signature,
self.platform,
flag_string,
self.device_manufacturer,
self.device_model,
attr_string,
self.rendering_intent_dict[self.rendering_intent],
self.illuminant,
self.creator)
return(msg)
class ComponentDefinitionBox(Jp2kBox):
"""Container for component definition box information.

View file

@ -138,7 +138,7 @@ class Jp2k(Jp2kBox):
f.seek(0)
self.box = self._parse_superbox(f)
def write(self, data, cratios=None, eph=False, psnr=None, numres=None,
def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None,
cbsize=None, psizes=None, grid_offset=None, sop=False,
subsam=None, tilesize=None, prog=None, modesw=None,
colorspace=None, verbose=False):
@ -150,7 +150,7 @@ class Jp2k(Jp2kBox):
Parameters
----------
data : array
img_array : ndarray
Image data to be written to file.
callbacks : bool, optional
If true, enable default info handler such that INFO messages
@ -307,19 +307,19 @@ class Jp2k(Jp2kBox):
msg = "Cannot specify cratios and psnr together."
raise RuntimeError(msg)
if data.ndim == 2:
numrows, numcols = data.shape
data = data.reshape(numrows, numcols, 1)
elif data.ndim == 3:
if img_array.ndim == 2:
numrows, numcols = img_array.shape
img_array = img_array.reshape(numrows, numcols, 1)
elif img_array.ndim == 3:
pass
else:
msg = "{0}D imagery is not allowed.".format(data.ndim)
msg = "{0}D imagery is not allowed.".format(img_array.ndim)
raise IOError(msg)
numrows, numcols, num_comps = data.shape
numrows, numcols, num_comps = img_array.shape
if colorspace is None:
if data.shape[2] == 1 or data.shape[2] == 2:
if img_array.shape[2] == 1 or img_array.shape[2] == 2:
colorspace = opj2._CLRSPC_GRAY
else:
# No YCC unless specifically told to do so.
@ -331,15 +331,15 @@ class Jp2k(Jp2kBox):
if colorspace not in ('rgb', 'grey', 'gray'):
msg = 'Invalid colorspace "{0}"'.format(colorspace)
raise IOError(msg)
elif colorspace == 'rgb' and data.shape[2] < 3:
elif colorspace == 'rgb' and img_array.shape[2] < 3:
msg = 'RGB colorspace requires at least 3 components.'
raise IOError(msg)
else:
colorspace = _cspace_map[colorspace]
if data.dtype == np.uint8:
if img_array.dtype == np.uint8:
comp_prec = 8
elif data.dtype == np.uint16:
elif img_array.dtype == np.uint16:
comp_prec = 16
else:
raise RuntimeError("unhandled datatype")
@ -368,7 +368,7 @@ class Jp2k(Jp2kBox):
# Stage the image data to the openjpeg data structure.
for k in range(0, num_comps):
layer = np.ascontiguousarray(data[:, :, k], dtype=np.int32)
layer = np.ascontiguousarray(img_array[:, :, k], dtype=np.int32)
dest = image.contents.comps[k].data
src = layer.ctypes.data
ctypes.memmove(dest, src, layer.nbytes)
@ -419,7 +419,7 @@ class Jp2k(Jp2kBox):
Returns
-------
result : array
img_array : ndarray
The image data.
Raises
@ -451,18 +451,18 @@ class Jp2k(Jp2kBox):
msg = "Components must all have the same subsampling factors."
raise IOError(msg)
data = self._read_common(reduce=reduce,
layer=layer,
area=area,
tile=tile,
verbose=verbose,
as_bands=False)
img_array = self._read_common(reduce=reduce,
layer=layer,
area=area,
tile=tile,
verbose=verbose,
as_bands=False)
if data.shape[2] == 1:
data = data.view()
data.shape = data.shape[0:2]
if img_array.shape[2] == 1:
img_array = img_array.view()
img_array.shape = img_array.shape[0:2]
return data
return img_array
def _read_common(self, reduce=0, layer=0, area=None, tile=None,
verbose=False, as_bands=False):
@ -486,7 +486,7 @@ class Jp2k(Jp2kBox):
Returns
-------
data : list or array
img_array : ndarray
The individual image components or a single array.
"""
dparam = opj2._set_default_decoder_parameters()

View file

@ -1,6 +1,7 @@
from .test_callbacks import TestCallbacks as callbacks
from .test_codestream import TestCodestream as codestream
from .test_jp2k import TestJp2k as jp2k
from .test_icc import TestICC as icc
from .test_printing import TestPrinting as printing
from .test_opj_suite import TestSuite as suite
from .test_opj_suite_write import TestSuiteWrite as suitew

74
glymur/test/test_icc.py Normal file
View file

@ -0,0 +1,74 @@
import datetime
import os
import struct
import sys
import tempfile
import unittest
import warnings
from xml.etree import cElementTree as ET
import numpy as np
import pkg_resources
from glymur import Jp2k
import glymur
try:
data_root = os.environ['OPJ_DATA_ROOT']
except KeyError:
data_root = None
except:
raise
@unittest.skipIf(data_root is None,
"OPJ_DATA_ROOT environment variable not set")
class TestICC(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_file5(self):
filename = os.path.join(data_root, 'input/conformance/file5.jp2')
j = Jp2k(filename)
profile = j.box[3].box[1].icc_profile
self.assertEqual(profile.size, 546)
self.assertEqual(profile.preferred_cmm_type, 0)
self.assertEqual(profile.version, '2.2.0')
self.assertEqual(profile.device_class, 'input device profile')
self.assertEqual(profile.colour_space, 'RGB')
self.assertEqual(profile.datetime,
datetime.datetime(2001, 8, 30, 13, 32, 37))
self.assertEqual(profile.file_signature, 'acsp')
self.assertEqual(profile.platform, 'unrecognized')
self.assertTrue(profile.flags & 0x01) # embedded
self.assertFalse(profile.flags & 0x02) # use anywhere
self.assertEqual(profile.device_manufacturer, 'KODA')
self.assertEqual(profile.device_model, 'ROMM')
self.assertFalse(profile.device_attributes & 0x01) # reflective
self.assertFalse(profile.device_attributes & 0x02) # glossy
self.assertFalse(profile.device_attributes & 0x04) # positive
self.assertFalse(profile.device_attributes & 0x08) # colour
self.assertEqual(profile.rendering_intent & 0x00ff, 0) # perceptual
np.testing.assert_almost_equal(profile.illuminant,
(0.964203, 1.000000, 0.824905),
decimal=6)
self.assertEqual(profile.creator, 'JPEG')
@unittest.skipIf(sys.hexversion < 0x03020000,
"Uses features introduced in 3.2.")
def test_invalid_profile_header(self):
jfile = os.path.join(data_root,
'input/nonregression/orb-blue10-lin-jp2.jp2')
with self.assertWarns(UserWarning) as cw:
data = Jp2k(jfile).read()
if __name__ == "__main__":
unittest.main()

View file

@ -3579,7 +3579,7 @@ class TestSuite(unittest.TestCase):
self.assertEqual(jp2.box[3].box[1].method, 2) # enumerated
self.assertEqual(jp2.box[3].box[1].precedence, 0)
self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact
self.assertEqual(len(jp2.box[3].box[1].icc_profile), 546)
self.assertEqual(jp2.box[3].box[1].icc_profile.size, 546)
self.assertIsNone(jp2.box[3].box[1].colorspace)
# Jp2 Header
@ -3674,7 +3674,7 @@ class TestSuite(unittest.TestCase):
self.assertEqual(jp2.box[3].box[1].method, 2)
self.assertEqual(jp2.box[3].box[1].precedence, 0)
self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact
self.assertEqual(len(jp2.box[3].box[1].icc_profile), 13332)
self.assertEqual(jp2.box[3].box[1].icc_profile.size, 13332)
self.assertIsNone(jp2.box[3].box[1].colorspace)
# Jp2 Header
@ -3723,7 +3723,7 @@ class TestSuite(unittest.TestCase):
self.assertEqual(jp2.box[2].box[1].method, 2) # enumerated
self.assertEqual(jp2.box[2].box[1].precedence, 0)
self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact
self.assertEqual(len(jp2.box[2].box[1].icc_profile), 414)
self.assertEqual(jp2.box[2].box[1].icc_profile.size, 414)
self.assertIsNone(jp2.box[2].box[1].colorspace)
# XML box
@ -6394,7 +6394,10 @@ class TestSuite(unittest.TestCase):
def test_NR_orb_blue10_lin_jp2_dump(self):
jfile = os.path.join(data_root,
'input/nonregression/orb-blue10-lin-jp2.jp2')
jp2 = Jp2k(jfile)
with warnings.catch_warnings():
# This file has an invalid ICC profile
warnings.simplefilter("ignore")
jp2 = Jp2k(jfile)
ids = [box.id for box in jp2.box]
self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
@ -6426,7 +6429,7 @@ class TestSuite(unittest.TestCase):
self.assertEqual(jp2.box[2].box[1].method, 2) # res icc
self.assertEqual(jp2.box[2].box[1].precedence, 0)
self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2
self.assertEqual(len(jp2.box[2].box[1].icc_profile), 1)
self.assertIsNone(jp2.box[2].box[1].icc_profile)
self.assertIsNone(jp2.box[2].box[1].colorspace)
c = jp2.box[3].main_header
@ -6490,7 +6493,10 @@ class TestSuite(unittest.TestCase):
def test_NR_orb_blue10_win_jp2_dump(self):
jfile = os.path.join(data_root,
'input/nonregression/orb-blue10-win-jp2.jp2')
jp2 = Jp2k(jfile)
with warnings.catch_warnings():
# This file has an invalid ICC profile
warnings.simplefilter("ignore")
jp2 = Jp2k(jfile)
ids = [box.id for box in jp2.box]
self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
@ -6522,7 +6528,7 @@ class TestSuite(unittest.TestCase):
self.assertEqual(jp2.box[2].box[1].method, 2) # restricted icc
self.assertEqual(jp2.box[2].box[1].precedence, 0)
self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2
self.assertEqual(len(jp2.box[2].box[1].icc_profile), 1)
self.assertIsNone(jp2.box[2].box[1].icc_profile)
self.assertIsNone(jp2.box[2].box[1].colorspace)
c = jp2.box[3].main_header
@ -6624,7 +6630,7 @@ class TestSuite(unittest.TestCase):
self.assertEqual(jp2.box[3].box[1].method, 3) # any icc
self.assertEqual(jp2.box[3].box[1].precedence, 2)
self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact
self.assertEqual(len(jp2.box[3].box[1].icc_profile), 1328)
self.assertEqual(jp2.box[3].box[1].icc_profile.size, 1328)
self.assertIsNone(jp2.box[3].box[1].colorspace)
# UUID boxes. All mentioned in the RREQ box.
@ -6859,7 +6865,10 @@ class TestSuite(unittest.TestCase):
def test_NR_DEC_orb_blue_lin_jp2_25_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/orb-blue10-lin-jp2.jp2')
data = Jp2k(jfile).read()
with warnings.catch_warnings():
# This file has an invalid ICC profile
warnings.simplefilter("ignore")
data = Jp2k(jfile).read()
self.assertTrue(True)
def test_NR_DEC_orb_blue_win_jp2_26_decode(self):

View file

@ -790,7 +790,24 @@ class TestPrinting(unittest.TestCase):
' Precedence: 2',
' Approximation: accurately represents '
+ 'correct colorspace definition',
' ICC Profile: 1328 bytes']
' ICC Profile: ',
' Size: 1328',
' Preferred CMM type: 6170706c',
' Version: 2.2.0',
' Device class signature: display device profile',
' Color space: RGB',
' Connection space: XYZ',
' Creation time: 2009-02-25 11:26:11',
' File signature: acsp',
' Platform: APPL',
' Flags: not embedded, can be used independently',
' Device manufacturer: appl',
' Device model: ',
' Device attributes: '
+ 'reflective, glossy, positive media polarity, color media',
' Rendering intent: perceptual',
' Illuminant: [ 0.96420288 1. 0.8249054 ]',
' Creator signature: appl']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)

View file

@ -1,7 +1,7 @@
from distutils.core import setup
kwargs = {'name': 'Glymur',
'version': '0.1.7',
'version': '0.1.8',
'description': 'Tools for accessing JPEG2000 files',
'long_description': open('README.md').read(),
'author': 'John Evans',