From 0e302a571000a465d34f7d3632e1c1de03bcbdae Mon Sep 17 00:00:00 2001 From: John Evans Date: Sun, 20 Oct 2013 11:57:51 -0400 Subject: [PATCH 01/26] Adding XMP UUID write support. Still needs tests. #104 --- glymur/jp2box.py | 19 +++++++++++++++++++ glymur/jp2k.py | 9 ++++++--- glymur/test/test_jp2box.py | 16 ++++++++-------- setup.py | 2 +- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index bf88e4d..6f628df 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -2122,6 +2122,7 @@ class UUIDBox(Jp2kBox): text = raw_data.decode('utf-8') elt = ET.fromstring(text) self.data = ET.ElementTree(elt) + self._type = 'XMP' elif the_uuid.bytes == b'JpgTiffExif->JP2': exif_obj = Exif(raw_data) ifds = OrderedDict() @@ -2130,8 +2131,16 @@ class UUIDBox(Jp2kBox): ifds['GPSInfo'] = exif_obj.exif_gpsinfo ifds['Iop'] = exif_obj.exif_iop self.data = ifds + self._type = 'Exif' else: self.data = raw_data + self._type = 'unknown' + + if length == 0: + # Need to compute the length. + # The length is 8 (L and T fields) + 16 (length of UUID identifier) + # + length of uuid data. + length = 24 + len(self.data) self.length = length self.offset = offset @@ -2164,6 +2173,16 @@ class UUIDBox(Jp2kBox): return msg + def write(self, fptr): + """Write a UUID box box to file. + """ + if self._type != 'XMP': + msg = "Only XMP UUID boxes can currently be written." + raise NotImplementedError(msg) + read_buffer = struct.pack('>I4s', self.length, 'uuid') + fptr.write(read_buffer) + fptr.write(self.data) + @staticmethod def parse(fptr, offset, length): """Parse UUID box. diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 1bd36a4..45eaf39 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -514,14 +514,17 @@ class Jp2k(Jp2kBox): Parameters ---------- box : Jp2Box - Instance of a JP2 box. Currently only XML boxes are allowed. + Instance of a JP2 box. Only UUID and XML boxes can currently be + appended. """ if self._codec_format == opj2.CODEC_J2K: msg = "Only JP2 files can currently have boxes appended to them." raise IOError(msg) - if box.box_id != 'xml ': - raise IOError("Only XML boxes can currently be appended.") + if not ((box.box_id == 'xml ') or + (box.box_id == 'uuid' and box._type == 'XMP')): + msg = "Only XML boxes and XMP UUID boxes can currently be appended." + raise IOError(msg) # Check the last box. If the length field is zero, then rewrite # the length field to reflect the true length of the box. diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index ef62460..ff61bc5 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -429,14 +429,14 @@ class TestAppend(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: shutil.copyfile(self.j2kfile, tfile.name) - jp2 = Jp2k(tfile.name) + j2k = Jp2k(tfile.name) - # Make a UUID box. - uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') - data = b'0123456789' - uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data) + # Make an XML box. XML boxes should always be appendable to jp2 + # files. + the_xml = ET.fromstring('0') + xmlbox = glymur.jp2box.XMLBox(xml=the_xml) with self.assertRaises(IOError): - jp2.append(uuidbox) + j2k.append(xmlbox) def test_length_field_is_zero(self): """L=0 (length field in box header) is handled. @@ -473,14 +473,14 @@ class TestAppend(unittest.TestCase): self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), b'0') - def test_only_xml_allowed_to_append(self): + def test_append_allowable_boxes(self): """Only XML boxes are allowed to be appended.""" with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: shutil.copyfile(self.jp2file, tfile.name) jp2 = Jp2k(tfile.name) - # Make a UUID box. + # Make a UUID box. Only XMP UUID boxes can currently be appended. uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') data = b'0123456789' uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data) diff --git a/setup.py b/setup.py index 89e69f7..b780e4d 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ kwargs['classifiers'] = clssfrs # Get the version string. Cannot do this by importing glymur! version_file = os.path.join('glymur', 'version.py') -with open('glymur/version.py', 'rt') as fptr: +with open(version_file, 'rt') as fptr: contents = fptr.read() match = re.search('version\s*=\s*"(?P\d*.\d*.\d*.*)"\n', contents) kwargs['version'] = match.group('version') From 08aaa25fdd03dcad140c1bd6b82b31840d633961 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 24 Oct 2013 19:05:11 -0400 Subject: [PATCH 02/26] Changed nemo.jp2 to have a single XMP UUID. #104 --- glymur/data/nemo.jp2 | Bin 1135423 -> 1135519 bytes glymur/jp2box.py | 16 ++--- glymur/jp2k.py | 2 +- glymur/test/fixtures.py | 87 ++++++++++++++++++++++++ glymur/test/test_jp2box.py | 4 +- glymur/test/test_jp2k.py | 78 ++++++--------------- glymur/test/test_printing.py | 127 ++++++++++++++--------------------- 7 files changed, 169 insertions(+), 145 deletions(-) diff --git a/glymur/data/nemo.jp2 b/glymur/data/nemo.jp2 index 55d199cfe6ebc1630bd87a238c6f4cb4b7333d9d..838583d2f3e22a49d877299178a764436b808e74 100644 GIT binary patch delta 2966 zcma)8&2QsG6c^3%Rmzt*f<(3|C$_OY9w)KYT7t9q!gl{X$|VkgdO4^Oh+`@J{sYu>!Se?R~AkMp0e_YS^Z{=4_t z^1r>u-=BnuYAu2&AgR+_1b&!Uo_l&YOVh|wlxQCNEf!A|&sB&|0urW)qO~;Tu)u=$ z>7nDX7sLWhZloka%YPc5W)i7OvtiQ8by_Y9l$1^;U{ftA;(8=m&`qRKkBnw4Wyy?1 ziuQW8`^}Tsn^?oMi{&qk#~J)L zn~)=9^e~`BgAEb5dd>Rnw1EbgY_V4O_Tl8G1=kU_kFg;m4kl z#Eof`QWo-)PnN$mK9N4?)?M#KFCMUjasUmdw#H4>@C!$D>vl*I=FfA39i$ZaGFL+H zYN2<-Y;8ee*U)c6n%YRXvB8z-E9airUyXYLmH(|}%p*P*Hv7KgQW6)SoSmaG9^qx+ zOvw%P(wU76vpBEBl=gDoQxZ-AsbQln%$2zk`Xx=10f|RW5cwo8w5a`v{2>0sZ{}0G zE6M|3TpJ3*i}mV!p}n#zT-rVz+Dx|wymR3m#ninFQxZpU2A3+YUlH{-Vg-)z{gM#SxI6;Ns9a&$e42=Rt+9m46$??MNLU@@<_ ze(}_lrxCX5?B$`I!<+k@WN}_a6`>+&f-ShxsSDppW)WRHCk1?6jtz-a>9HVUAzv$c zES%8kJQlM~ILf`w{oJ^N)p*wTZ3iF5Upan>hgiyd@mI}D;a^tk;Hvgd!Drr7Hbzg7 z+A`YDL=!^!rAS81nJkbXd&}vjhCXb!Z?F|8Kv-0Rhq>V>%@0bo41vKm9+;k zb4ijwpIni2#ytDk`S|8~_>ENC0)Vzi#%d@u;12VB=z7)jM&6wHPDH$lQ>7iJ3feMS z*eqs?=dM<)mk##6{}9Y!NhlXi7Ig3c@rC(L$M>{*npT0E70%CW=X)JLv>G!XTd)o0 zC&4;s@AG`gUD^Ke=-{WLgV%uefEqxLfL;fB1L#elw}9RT+6Q_Eh~wV{dJpJ*pb!2! JI{5J8{{NWigc<+< delta 2973 zcmcJN&rTCj6vpq&v=pX=q7WBWlVKu}kkFZy63e7CF>NhHE&rO}w%C?5wzO6t)rFHN z3B*;A#I3CK5nQ-nqHV4yH`lCSqW=bn3h=ic<|_ssFPOlMK&-*0bM zOAF17LbX!Kw5ye={3rPFeRv&ao$la6b17RZzO0s8WF9e1|6l#&;lA1C87=MjjB(Ux zR24cr8AWOot_rEsxmQh^lf&`sr-^kvZG%0YU`kJ6=M(%%|yL zd{H$rt*KE>os}XnE%r#8nLamHM12YMJT79s-pXto?i7zro%r#~5rG!swP-1Qv^2Zi)rY%bDgM1G)4=qgDi{j7)JhApP5H!#3O7~*gC`3we0FtJVwal)l@ErTs~7+TU>c+c`Zyy z$UyC2d0hDW^Y~zAEH5V7TD#t?m$!;iyI$LTqqXPcVyUrS*5GMcktL&K>(!k6;`x%4 zYP8ByEF24~Q~2BNMCGI?C0eD5wvv8w_=SrfCjDbWqJ)q2t2Yl{b9eN1v8hDDsv^@p zAhlGhT-<833XMi>PTr`!t#7wvg*H=Ix1^X*PNu_#t55Ilv`dOJ#K!L##mcusY%PCkjT|Tr)krTpZ#nN=L=ivk o2KIIX9K=HcI4s', self.length, 'uuid') + serialized_buffer = b'' + serialized_buffer += ET.tostring(self.data.getroot(), encoding='utf-8') + serialized_buffer += b'' + if self.length == 0: + self.length = 24 + len(serialized_buffer) + read_buffer = struct.pack('>I4s', self.length, b'uuid') fptr.write(read_buffer) - fptr.write(self.data) + fptr.write(self.uuid.bytes) + fptr.write(serialized_buffer) @staticmethod def parse(fptr, offset, length): diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 45eaf39..42eaab5 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -1013,7 +1013,7 @@ class Jp2k(Jp2kBox): >>> jp2 = glymur.Jp2k(jfile) >>> codestream = jp2.get_codestream() >>> print(codestream.segment[1]) - SIZ marker segment @ (3137, 47) + SIZ marker segment @ (3233, 47) Profile: 2 Reference Grid Height, Width: (1456 x 2592) Vertical, Horizontal Reference Grid Offset: (0 x 0) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index b872f1d..68c0706 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -167,3 +167,90 @@ def read_pgx_header(pgx_file): header = header.rstrip() return header, pos + +nemo_xmp_box = """UUID Box (uuid) @ (77, 3146) + UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) + UUID Data: + + + + Google + 2013-02-09T14:47:53 + + + 1 + 72/1 + 72/1 + 2 + HTC + HTC Glacier + 2592 + 1456 + + + 8 + 8 + 8 + + + 2 + 3 + + + 1343036288/4294967295 + 1413044224/4294967295 + + + + + 2748779008/4294967295 + 1417339264/4294967295 + 1288490240/4294967295 + 2576980480/4294967295 + 644245120/4294967295 + 257698032/4294967295 + + + + + 1 + 2528 + 1424 + 353/100 + 0 + 0/1 + WGS-84 + 2013-02-09T14:47:53 + + + 76 + + + 0220 + 0100 + + + 1 + 2 + 3 + 0 + + + 42,20.56N + 71,5.29W + 2013-02-09T19:47:53Z + NETWORK + + + 2013-02-09T14:47:53 + + + + + Glymur + Python XMP Toolkit + + + + + """ diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index ff61bc5..2f6241c 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -419,7 +419,7 @@ class TestAppend(unittest.TestCase): # The sequence of box IDs should be the same as before, but with an # xml box at the end. box_ids = [box.box_id for box in jp2.box] - expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml '] + expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'xml '] self.assertEqual(box_ids, expected) self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), b'0') @@ -468,7 +468,7 @@ class TestAppend(unittest.TestCase): # The sequence of box IDs should be the same as before, but with an # xml box at the end. box_ids = [box.box_id for box in jp2.box] - expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml '] + expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'xml '] self.assertEqual(box_ids, expected) self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), b'0') diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 636ea9a..c9e750a 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -100,7 +100,7 @@ class TestJp2k(unittest.TestCase): jp2k = Jp2k(self.jp2file) # top-level boxes - self.assertEqual(len(jp2k.box), 6) + self.assertEqual(len(jp2k.box), 5) self.assertEqual(jp2k.box[0].box_id, 'jP ') self.assertEqual(jp2k.box[0].offset, 0) @@ -119,15 +119,11 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[3].box_id, 'uuid') self.assertEqual(jp2k.box[3].offset, 77) - self.assertEqual(jp2k.box[3].length, 638) + self.assertEqual(jp2k.box[3].length, 3146) - self.assertEqual(jp2k.box[4].box_id, 'uuid') - self.assertEqual(jp2k.box[4].offset, 715) - self.assertEqual(jp2k.box[4].length, 2412) - - self.assertEqual(jp2k.box[5].box_id, 'jp2c') - self.assertEqual(jp2k.box[5].offset, 3127) - self.assertEqual(jp2k.box[5].length, 1132296) + self.assertEqual(jp2k.box[4].box_id, 'jp2c') + self.assertEqual(jp2k.box[4].offset, 3223) + self.assertEqual(jp2k.box[4].length, 1132296) # jp2h super box self.assertEqual(len(jp2k.box[2].box), 2) @@ -169,7 +165,7 @@ class TestJp2k(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - write_buffer = ifile.read(3127) + write_buffer = ifile.read(3223) tfile.write(write_buffer) # The L field must be 1 in order to signal the presence of the @@ -190,9 +186,9 @@ class TestJp2k(unittest.TestCase): jp2k = Jp2k(tfile.name) - self.assertEqual(jp2k.box[5].box_id, 'jp2c') - self.assertEqual(jp2k.box[5].offset, 3127) - self.assertEqual(jp2k.box[5].length, 1133427 + 8) + self.assertEqual(jp2k.box[4].box_id, 'jp2c') + self.assertEqual(jp2k.box[4].offset, 3223) + self.assertEqual(jp2k.box[4].length, 1133427 + 8) @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") def test_length_field_is_zero(self): @@ -357,45 +353,12 @@ class TestJp2k(unittest.TestCase): def test_xmp_attribute(self): """Verify the XMP packet in the shipping example file can be read.""" j = Jp2k(self.jp2file) - xmp = j.box[4].data + xmp = j.box[3].data ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' - ns1 = '{http://ns.adobe.com/xap/1.0/}' - name = '{0}RDF/{0}Description'.format(ns0) + ns2 = '{http://ns.adobe.com/xap/1.0/}' + name = '{0}RDF/{0}Description/{1}CreatorTool'.format(ns0, ns2) elt = xmp.find(name) - attr_value = elt.attrib['{0}CreatorTool'.format(ns1)] - self.assertEqual(attr_value, 'glymur') - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_unrecognized_exif_tag(self): - """An unrecognized exif tag should be handled gracefully.""" - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - shutil.copyfile(self.jp2file, tfile.name) - - # The Exif UUID starts at byte 77. There are 8 bytes for the L and - # T fields, then 16 bytes for the UUID identifier, then 6 exif - # header bytes, then 8 bytes for the TIFF header, then 2 bytes - # the the Image IFD number of tags, where we finally find the first - # tag, "Make" (271). We'll corrupt it by changing it into 171, - # which does not correspond to any known Exif Image tag. - with open(tfile.name, 'r+b') as fptr: - fptr.seek(117) - write_buffer = struct.pack('', - ' ', - ' ', - ' ', - ' '] - expected = '\n'.join(lst) + expected = nemo_xmp_box self.assertEqual(actual, expected) def test_codestream(self): @@ -657,8 +645,8 @@ class TestPrinting(unittest.TestCase): print(j.get_codestream()) actual = fake_out.getvalue().strip() lst = ['Codestream:', - ' SOC marker segment @ (3135, 0)', - ' SIZ marker segment @ (3137, 47)', + ' SOC marker segment @ (3231, 0)', + ' SIZ marker segment @ (3233, 47)', ' Profile: 2', ' Reference Grid Height, Width: (1456 x 2592)', ' Vertical, Horizontal Reference Grid Offset: (0 x 0)', @@ -668,7 +656,7 @@ class TestPrinting(unittest.TestCase): ' Signed: (False, False, False)', ' Vertical, Horizontal Subsampling: ' + '((1, 1), (1, 1), (1, 1))', - ' COD marker segment @ (3186, 12)', + ' COD marker segment @ (3282, 12)', ' Coding style:', ' Entropy coder, without partitions', ' SOP marker segments: False', @@ -690,11 +678,11 @@ class TestPrinting(unittest.TestCase): ' Vertically stripe causal context: False', ' Predictable termination: False', ' Segmentation symbols: False', - ' QCD marker segment @ (3200, 7)', + ' QCD marker segment @ (3296, 7)', ' Quantization style: no quantization, ' + '2 guard bits', ' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]', - ' CME marker segment @ (3209, 37)', + ' CME marker segment @ (3305, 37)', ' "Created by OpenJPEG version 2.0.0"'] expected = '\n'.join(lst) self.assertEqual(actual, expected) @@ -1046,59 +1034,42 @@ class TestPrinting(unittest.TestCase): "Ordered dicts not printing well in 2.7") def test_exif_uuid(self): """Verify printing of exif information""" - j = glymur.Jp2k(self.jp2file) + with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile: - with patch('sys.stdout', new=StringIO()) as fake_out: - print(j.box[3]) - actual = fake_out.getvalue().strip() + with open(self.jp2file, 'rb') as ifptr: + tfile.write(ifptr.read()) - lines = ["UUID Box (uuid) @ (77, 638)", + # Write L, T, UUID identifier. + tfile.write(struct.pack('>I4s', 76, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = struct.pack(' Date: Thu, 24 Oct 2013 20:38:29 -0400 Subject: [PATCH 03/26] Refactored Exif uuid code to separate subpackage. #104 --- CHANGES.txt | 4 + glymur/_uuid_io/Exif.py | 520 ++++++++++++++++++++++++++++++++ glymur/_uuid_io/__init__.py | 1 + glymur/jp2box.py | 509 +------------------------------ glymur/test/test_jp2box_uuid.py | 78 +++++ 5 files changed, 606 insertions(+), 506 deletions(-) create mode 100644 glymur/_uuid_io/Exif.py create mode 100644 glymur/_uuid_io/__init__.py create mode 100644 glymur/test/test_jp2box_uuid.py diff --git a/CHANGES.txt b/CHANGES.txt index 40846e4..65270bf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +Oct 24, 2013 - v0.6.0 Palette box palette changed to 2D numpy array. Removed + nemo Exif and simple XMP UUIDs in favor of larger XMP UUID. Refactored + Exif UUID code into _uuid_io sub package. + Oct 13, 2013 - v0.5.6 Fixed handling of non-ascii chars in XML boxes. Fixed some docstring errors in jp2box module. diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py new file mode 100644 index 0000000..f70a090 --- /dev/null +++ b/glymur/_uuid_io/Exif.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +""" +Handlers for various UUID types. +""" +import struct +import sys +import warnings + +if sys.hexversion < 0x02070000: + # pylint: disable=F0401,E0611 + from ordereddict import OrderedDict +else: + from collections import OrderedDict + +class _Exif(object): + """ + Attributes + ---------- + read_buffer : bytes + Raw byte stream consisting of the UUID data. + endian : str + Either '<' for big-endian, or '>' for little-endian. + """ + + def __init__(self, read_buffer): + """Interpret raw buffer consisting of Exif IFD. + """ + self.exif_image = None + self.exif_photo = None + self.exif_gpsinfo = None + self.exif_iop = None + + self.read_buffer = read_buffer + + # Ignore the first six bytes. + # Next 8 should be (73, 73, 42, 8) + data = struct.unpack('' for little-endian. + num_tags : int + Number of tags in the IFD. + raw_ifd : dictionary + Maps tag number to "mildly-interpreted" tag value. + processed_ifd : dictionary + Maps tag name to "mildly-interpreted" tag value. + """ + datatype2fmt = {1: ('B', 1), + 2: ('B', 1), + 3: ('H', 2), + 4: ('I', 4), + 5: ('II', 8), + 7: ('B', 1), + 9: ('i', 4), + 10: ('ii', 8)} + + def __init__(self, endian, read_buffer, offset): + self.endian = endian + self.read_buffer = read_buffer + self.processed_ifd = OrderedDict() + + self.num_tags, = struct.unpack(endian + 'H', + read_buffer[offset:offset + 2]) + + fmt = self.endian + 'HHII' * self.num_tags + ifd_buffer = read_buffer[offset + 2:offset + 2 + self.num_tags * 12] + data = struct.unpack(fmt, ifd_buffer) + self.raw_ifd = OrderedDict() + for j, tag in enumerate(data[0::4]): + # The offset to the tag offset/payload is the offset to the IFD + # plus 2 bytes for the number of tags plus 12 bytes for each + # tag entry plus 8 bytes to the offset/payload itself. + toffp = read_buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4] + tag_data = self.parse_tag(data[j * 4 + 1], + data[j * 4 + 2], + toffp) + self.raw_ifd[tag] = tag_data + + def parse_tag(self, dtype, count, offset_buf): + """Interpret an Exif image tag data payload. + """ + fmt = self.datatype2fmt[dtype][0] * count + payload_size = self.datatype2fmt[dtype][1] * count + + if payload_size <= 4: + # Interpret the payload from the 4 bytes in the tag entry. + target_buffer = offset_buf[:payload_size] + else: + # Interpret the payload at the offset specified by the 4 bytes in + # the tag entry. + offset, = struct.unpack(self.endian + 'I', offset_buf) + target_buffer = self.read_buffer[offset:offset + payload_size] + + if dtype == 2: + # ASCII + if sys.hexversion < 0x03000000: + payload = target_buffer.rstrip('\x00') + else: + payload = target_buffer.decode('utf-8').rstrip('\x00') + + else: + payload = struct.unpack(self.endian + fmt, target_buffer) + if dtype == 5 or dtype == 10: + # Rational or Signed Rational. Construct the list of values. + rational_payload = [] + for j in range(count): + value = float(payload[j * 2]) / float(payload[j * 2 + 1]) + rational_payload.append(value) + payload = rational_payload + if count == 1: + # If just a single value, then return a scalar instead of a + # tuple. + payload = payload[0] + + return payload + + def post_process(self, tagnum2name): + """Map the tag name instead of tag number to the tag value. + """ + for tag, value in self.raw_ifd.items(): + try: + tag_name = tagnum2name[tag] + except KeyError: + # Ok, we don't recognize this tag. Just use the numeric id. + msg = 'Unrecognized Exif tag "{0}".'.format(tag) + warnings.warn(msg, UserWarning) + tag_name = tag + self.processed_ifd[tag_name] = value + + +class _ExifImageIfd(_Ifd): + """ + Attributes + ---------- + tagnum2name : dict + Maps Exif image tag numbers to the tag names. + ifd : dict + Maps tag names to tag values. + """ + tagnum2name = {11: 'ProcessingSoftware', + 254: 'NewSubfileType', + 255: 'SubfileType', + 256: 'ImageWidth', + 257: 'ImageLength', + 258: 'BitsPerSample', + 259: 'Compression', + 262: 'PhotometricInterpretation', + 263: 'Threshholding', + 264: 'CellWidth', + 265: 'CellLength', + 266: 'FillOrder', + 269: 'DocumentName', + 270: 'ImageDescription', + 271: 'Make', + 272: 'Model', + 273: 'StripOffsets', + 274: 'Orientation', + 277: 'SamplesPerPixel', + 278: 'RowsPerStrip', + 279: 'StripByteCounts', + 282: 'XResolution', + 283: 'YResolution', + 284: 'PlanarConfiguration', + 290: 'GrayResponseUnit', + 291: 'GrayResponseCurve', + 292: 'T4Options', + 293: 'T6Options', + 296: 'ResolutionUnit', + 301: 'TransferFunction', + 305: 'Software', + 306: 'DateTime', + 315: 'Artist', + 316: 'HostComputer', + 317: 'Predictor', + 318: 'WhitePoint', + 319: 'PrimaryChromaticities', + 320: 'ColorMap', + 321: 'HalftoneHints', + 322: 'TileWidth', + 323: 'TileLength', + 324: 'TileOffsets', + 325: 'TileByteCounts', + 330: 'SubIFDs', + 332: 'InkSet', + 333: 'InkNames', + 334: 'NumberOfInks', + 336: 'DotRange', + 337: 'TargetPrinter', + 338: 'ExtraSamples', + 339: 'SampleFormat', + 340: 'SMinSampleValue', + 341: 'SMaxSampleValue', + 342: 'TransferRange', + 343: 'ClipPath', + 344: 'XClipPathUnits', + 345: 'YClipPathUnits', + 346: 'Indexed', + 347: 'JPEGTables', + 351: 'OPIProxy', + 512: 'JPEGProc', + 513: 'JPEGInterchangeFormat', + 514: 'JPEGInterchangeFormatLength', + 515: 'JPEGRestartInterval', + 517: 'JPEGLosslessPredictors', + 518: 'JPEGPointTransforms', + 519: 'JPEGQTables', + 520: 'JPEGDCTables', + 521: 'JPEGACTables', + 529: 'YCbCrCoefficients', + 530: 'YCbCrSubSampling', + 531: 'YCbCrPositioning', + 532: 'ReferenceBlackWhite', + 700: 'XMLPacket', + 18246: 'Rating', + 18249: 'RatingPercent', + 32781: 'ImageID', + 33421: 'CFARepeatPatternDim', + 33422: 'CFAPattern', + 33423: 'BatteryLevel', + 33432: 'Copyright', + 33434: 'ExposureTime', + 33437: 'FNumber', + 33723: 'IPTCNAA', + 34377: 'ImageResources', + 34665: 'ExifTag', + 34675: 'InterColorProfile', + 34850: 'ExposureProgram', + 34852: 'SpectralSensitivity', + 34853: 'GPSTag', + 34855: 'ISOSpeedRatings', + 34856: 'OECF', + 34857: 'Interlace', + 34858: 'TimeZoneOffset', + 34859: 'SelfTimerMode', + 36867: 'DateTimeOriginal', + 37122: 'CompressedBitsPerPixel', + 37377: 'ShutterSpeedValue', + 37378: 'ApertureValue', + 37379: 'BrightnessValue', + 37380: 'ExposureBiasValue', + 37381: 'MaxApertureValue', + 37382: 'SubjectDistance', + 37383: 'MeteringMode', + 37384: 'LightSource', + 37385: 'Flash', + 37386: 'FocalLength', + 37387: 'FlashEnergy', + 37388: 'SpatialFrequencyResponse', + 37389: 'Noise', + 37390: 'FocalPlaneXResolution', + 37391: 'FocalPlaneYResolution', + 37392: 'FocalPlaneResolutionUnit', + 37393: 'ImageNumber', + 37394: 'SecurityClassification', + 37395: 'ImageHistory', + 37396: 'SubjectLocation', + 37397: 'ExposureIndex', + 37398: 'TIFFEPStandardID', + 37399: 'SensingMethod', + 40091: 'XPTitle', + 40092: 'XPComment', + 40093: 'XPAuthor', + 40094: 'XPKeywords', + 40095: 'XPSubject', + 50341: 'PrintImageMatching', + 50706: 'DNGVersion', + 50707: 'DNGBackwardVersion', + 50708: 'UniqueCameraModel', + 50709: 'LocalizedCameraModel', + 50710: 'CFAPlaneColor', + 50711: 'CFALayout', + 50712: 'LinearizationTable', + 50713: 'BlackLevelRepeatDim', + 50714: 'BlackLevel', + 50715: 'BlackLevelDeltaH', + 50716: 'BlackLevelDeltaV', + 50717: 'WhiteLevel', + 50718: 'DefaultScale', + 50719: 'DefaultCropOrigin', + 50720: 'DefaultCropSize', + 50721: 'ColorMatrix1', + 50722: 'ColorMatrix2', + 50723: 'CameraCalibration1', + 50724: 'CameraCalibration2', + 50725: 'ReductionMatrix1', + 50726: 'ReductionMatrix2', + 50727: 'AnalogBalance', + 50728: 'AsShotNeutral', + 50729: 'AsShotWhiteXY', + 50730: 'BaselineExposure', + 50731: 'BaselineNoise', + 50732: 'BaselineSharpness', + 50733: 'BayerGreenSplit', + 50734: 'LinearResponseLimit', + 50735: 'CameraSerialNumber', + 50736: 'LensInfo', + 50737: 'ChromaBlurRadius', + 50738: 'AntiAliasStrength', + 50739: 'ShadowScale', + 50740: 'DNGPrivateData', + 50741: 'MakerNoteSafety', + 50778: 'CalibrationIlluminant1', + 50779: 'CalibrationIlluminant2', + 50780: 'BestQualityScale', + 50781: 'RawDataUniqueID', + 50827: 'OriginalRawFileName', + 50828: 'OriginalRawFileData', + 50829: 'ActiveArea', + 50830: 'MaskedAreas', + 50831: 'AsShotICCProfile', + 50832: 'AsShotPreProfileMatrix', + 50833: 'CurrentICCProfile', + 50834: 'CurrentPreProfileMatrix', + 50879: 'ColorimetricReference', + 50931: 'CameraCalibrationSignature', + 50932: 'ProfileCalibrationSignature', + 50934: 'AsShotProfileName', + 50935: 'NoiseReductionApplied', + 50936: 'ProfileName', + 50937: 'ProfileHueSatMapDims', + 50938: 'ProfileHueSatMapData1', + 50939: 'ProfileHueSatMapData2', + 50940: 'ProfileToneCurve', + 50941: 'ProfileEmbedPolicy', + 50942: 'ProfileCopyright', + 50964: 'ForwardMatrix1', + 50965: 'ForwardMatrix2', + 50966: 'PreviewApplicationName', + 50967: 'PreviewApplicationVersion', + 50968: 'PreviewSettingsName', + 50969: 'PreviewSettingsDigest', + 50970: 'PreviewColorSpace', + 50971: 'PreviewDateTime', + 50972: 'RawImageDigest', + 50973: 'OriginalRawFileDigest', + 50974: 'SubTileBlockSize', + 50975: 'RowInterleaveFactor', + 50981: 'ProfileLookTableDims', + 50982: 'ProfileLookTableData', + 51008: 'OpcodeList1', + 51009: 'OpcodeList2', + 51022: 'OpcodeList3', + 51041: 'NoiseProfile'} + + def __init__(self, endian, read_buffer, offset): + _Ifd.__init__(self, endian, read_buffer, offset) + self.post_process(self.tagnum2name) + + +class _ExifPhotoIfd(_Ifd): + """Represents tags found in the Exif sub ifd. + """ + tagnum2name = {33434: 'ExposureTime', + 33437: 'FNumber', + 34850: 'ExposureProgram', + 34852: 'SpectralSensitivity', + 34855: 'ISOSpeedRatings', + 34856: 'OECF', + 34864: 'SensitivityType', + 34865: 'StandardOutputSensitivity', + 34866: 'RecommendedExposureIndex', + 34867: 'ISOSpeed', + 34868: 'ISOSpeedLatitudeyyy', + 34869: 'ISOSpeedLatitudezzz', + 36864: 'ExifVersion', + 36867: 'DateTimeOriginal', + 36868: 'DateTimeDigitized', + 37121: 'ComponentsConfiguration', + 37122: 'CompressedBitsPerPixel', + 37377: 'ShutterSpeedValue', + 37378: 'ApertureValue', + 37379: 'BrightnessValue', + 37380: 'ExposureBiasValue', + 37381: 'MaxApertureValue', + 37382: 'SubjectDistance', + 37383: 'MeteringMode', + 37384: 'LightSource', + 37385: 'Flash', + 37386: 'FocalLength', + 37396: 'SubjectArea', + 37500: 'MakerNote', + 37510: 'UserComment', + 37520: 'SubSecTime', + 37521: 'SubSecTimeOriginal', + 37522: 'SubSecTimeDigitized', + 40960: 'FlashpixVersion', + 40961: 'ColorSpace', + 40962: 'PixelXDimension', + 40963: 'PixelYDimension', + 40964: 'RelatedSoundFile', + 40965: 'InteroperabilityTag', + 41483: 'FlashEnergy', + 41484: 'SpatialFrequencyResponse', + 41486: 'FocalPlaneXResolution', + 41487: 'FocalPlaneYResolution', + 41488: 'FocalPlaneResolutionUnit', + 41492: 'SubjectLocation', + 41493: 'ExposureIndex', + 41495: 'SensingMethod', + 41728: 'FileSource', + 41729: 'SceneType', + 41730: 'CFAPattern', + 41985: 'CustomRendered', + 41986: 'ExposureMode', + 41987: 'WhiteBalance', + 41988: 'DigitalZoomRatio', + 41989: 'FocalLengthIn35mmFilm', + 41990: 'SceneCaptureType', + 41991: 'GainControl', + 41992: 'Contrast', + 41993: 'Saturation', + 41994: 'Sharpness', + 41995: 'DeviceSettingDescription', + 41996: 'SubjectDistanceRange', + 42016: 'ImageUniqueID', + 42032: 'CameraOwnerName', + 42033: 'BodySerialNumber', + 42034: 'LensSpecification', + 42035: 'LensMake', + 42036: 'LensModel', + 42037: 'LensSerialNumber'} + + def __init__(self, endian, read_buffer, offset): + _Ifd.__init__(self, endian, read_buffer, offset) + self.post_process(self.tagnum2name) + + +class _ExifGPSInfoIfd(_Ifd): + """Represents information found in the GPSInfo sub IFD. + """ + tagnum2name = {0: 'GPSVersionID', + 1: 'GPSLatitudeRef', + 2: 'GPSLatitude', + 3: 'GPSLongitudeRef', + 4: 'GPSLongitude', + 5: 'GPSAltitudeRef', + 6: 'GPSAltitude', + 7: 'GPSTimeStamp', + 8: 'GPSSatellites', + 9: 'GPSStatus', + 10: 'GPSMeasureMode', + 11: 'GPSDOP', + 12: 'GPSSpeedRef', + 13: 'GPSSpeed', + 14: 'GPSTrackRef', + 15: 'GPSTrack', + 16: 'GPSImgDirectionRef', + 17: 'GPSImgDirection', + 18: 'GPSMapDatum', + 19: 'GPSDestLatitudeRef', + 20: 'GPSDestLatitude', + 21: 'GPSDestLongitudeRef', + 22: 'GPSDestLongitude', + 23: 'GPSDestBearingRef', + 24: 'GPSDestBearing', + 25: 'GPSDestDistanceRef', + 26: 'GPSDestDistance', + 27: 'GPSProcessingMethod', + 28: 'GPSAreaInformation', + 29: 'GPSDateStamp', + 30: 'GPSDifferential'} + + def __init__(self, endian, read_buffer, offset): + _Ifd.__init__(self, endian, read_buffer, offset) + self.post_process(self.tagnum2name) + + +class _ExifInteroperabilityIfd(_Ifd): + """Represents tags found in the Interoperability sub IFD. + """ + tagnum2name = {1: 'InteroperabilityIndex', + 2: 'InteroperabilityVersion', + 4096: 'RelatedImageFileFormat', + 4097: 'RelatedImageWidth', + 4098: 'RelatedImageLength'} + + def __init__(self, endian, read_buffer, offset): + _Ifd.__init__(self, endian, read_buffer, offset) + self.post_process(self.tagnum2name) + + + diff --git a/glymur/_uuid_io/__init__.py b/glymur/_uuid_io/__init__.py new file mode 100644 index 0000000..721ee36 --- /dev/null +++ b/glymur/_uuid_io/__init__.py @@ -0,0 +1 @@ +from .Exif import _Exif diff --git a/glymur/jp2box.py b/glymur/jp2box.py index a270424..3ea911e 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -39,6 +39,8 @@ from .core import _COLOR_TYPE_MAP_DISPLAY from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD +from ._uuid_io import _Exif + _METHOD_DISPLAY = { ENUMERATED_COLORSPACE: 'enumerated colorspace', RESTRICTED_ICC_PROFILE: 'restricted ICC profile', @@ -2084,7 +2086,7 @@ class UUIDBox(Jp2kBox): self.data = ET.ElementTree(elt) self._type = 'XMP' elif the_uuid.bytes == b'JpgTiffExif->JP2': - exif_obj = Exif(raw_data) + exif_obj = _Exif(raw_data) ifds = OrderedDict() ifds['Image'] = exif_obj.exif_image ifds['Photo'] = exif_obj.exif_photo @@ -2170,511 +2172,6 @@ class UUIDBox(Jp2kBox): return box -class Exif(object): - """ - Attributes - ---------- - read_buffer : bytes - Raw byte stream consisting of the UUID data. - endian : str - Either '<' for big-endian, or '>' for little-endian. - """ - - def __init__(self, read_buffer): - """Interpret raw buffer consisting of Exif IFD. - """ - self.exif_image = None - self.exif_photo = None - self.exif_gpsinfo = None - self.exif_iop = None - - self.read_buffer = read_buffer - - # Ignore the first six bytes. - # Next 8 should be (73, 73, 42, 8) - data = struct.unpack('' for little-endian. - num_tags : int - Number of tags in the IFD. - raw_ifd : dictionary - Maps tag number to "mildly-interpreted" tag value. - processed_ifd : dictionary - Maps tag name to "mildly-interpreted" tag value. - """ - datatype2fmt = {1: ('B', 1), - 2: ('B', 1), - 3: ('H', 2), - 4: ('I', 4), - 5: ('II', 8), - 7: ('B', 1), - 9: ('i', 4), - 10: ('ii', 8)} - - def __init__(self, endian, read_buffer, offset): - self.endian = endian - self.read_buffer = read_buffer - self.processed_ifd = OrderedDict() - - self.num_tags, = struct.unpack(endian + 'H', - read_buffer[offset:offset + 2]) - - fmt = self.endian + 'HHII' * self.num_tags - ifd_buffer = read_buffer[offset + 2:offset + 2 + self.num_tags * 12] - data = struct.unpack(fmt, ifd_buffer) - self.raw_ifd = OrderedDict() - for j, tag in enumerate(data[0::4]): - # The offset to the tag offset/payload is the offset to the IFD - # plus 2 bytes for the number of tags plus 12 bytes for each - # tag entry plus 8 bytes to the offset/payload itself. - toffp = read_buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4] - tag_data = self.parse_tag(data[j * 4 + 1], - data[j * 4 + 2], - toffp) - self.raw_ifd[tag] = tag_data - - def parse_tag(self, dtype, count, offset_buf): - """Interpret an Exif image tag data payload. - """ - fmt = self.datatype2fmt[dtype][0] * count - payload_size = self.datatype2fmt[dtype][1] * count - - if payload_size <= 4: - # Interpret the payload from the 4 bytes in the tag entry. - target_buffer = offset_buf[:payload_size] - else: - # Interpret the payload at the offset specified by the 4 bytes in - # the tag entry. - offset, = struct.unpack(self.endian + 'I', offset_buf) - target_buffer = self.read_buffer[offset:offset + payload_size] - - if dtype == 2: - # ASCII - if sys.hexversion < 0x03000000: - payload = target_buffer.rstrip('\x00') - else: - payload = target_buffer.decode('utf-8').rstrip('\x00') - - else: - payload = struct.unpack(self.endian + fmt, target_buffer) - if dtype == 5 or dtype == 10: - # Rational or Signed Rational. Construct the list of values. - rational_payload = [] - for j in range(count): - value = float(payload[j * 2]) / float(payload[j * 2 + 1]) - rational_payload.append(value) - payload = rational_payload - if count == 1: - # If just a single value, then return a scalar instead of a - # tuple. - payload = payload[0] - - return payload - - def post_process(self, tagnum2name): - """Map the tag name instead of tag number to the tag value. - """ - for tag, value in self.raw_ifd.items(): - try: - tag_name = tagnum2name[tag] - except KeyError: - # Ok, we don't recognize this tag. Just use the numeric id. - msg = 'Unrecognized Exif tag "{0}".'.format(tag) - warnings.warn(msg, UserWarning) - tag_name = tag - self.processed_ifd[tag_name] = value - - -class _ExifImageIfd(_Ifd): - """ - Attributes - ---------- - tagnum2name : dict - Maps Exif image tag numbers to the tag names. - ifd : dict - Maps tag names to tag values. - """ - tagnum2name = {11: 'ProcessingSoftware', - 254: 'NewSubfileType', - 255: 'SubfileType', - 256: 'ImageWidth', - 257: 'ImageLength', - 258: 'BitsPerSample', - 259: 'Compression', - 262: 'PhotometricInterpretation', - 263: 'Threshholding', - 264: 'CellWidth', - 265: 'CellLength', - 266: 'FillOrder', - 269: 'DocumentName', - 270: 'ImageDescription', - 271: 'Make', - 272: 'Model', - 273: 'StripOffsets', - 274: 'Orientation', - 277: 'SamplesPerPixel', - 278: 'RowsPerStrip', - 279: 'StripByteCounts', - 282: 'XResolution', - 283: 'YResolution', - 284: 'PlanarConfiguration', - 290: 'GrayResponseUnit', - 291: 'GrayResponseCurve', - 292: 'T4Options', - 293: 'T6Options', - 296: 'ResolutionUnit', - 301: 'TransferFunction', - 305: 'Software', - 306: 'DateTime', - 315: 'Artist', - 316: 'HostComputer', - 317: 'Predictor', - 318: 'WhitePoint', - 319: 'PrimaryChromaticities', - 320: 'ColorMap', - 321: 'HalftoneHints', - 322: 'TileWidth', - 323: 'TileLength', - 324: 'TileOffsets', - 325: 'TileByteCounts', - 330: 'SubIFDs', - 332: 'InkSet', - 333: 'InkNames', - 334: 'NumberOfInks', - 336: 'DotRange', - 337: 'TargetPrinter', - 338: 'ExtraSamples', - 339: 'SampleFormat', - 340: 'SMinSampleValue', - 341: 'SMaxSampleValue', - 342: 'TransferRange', - 343: 'ClipPath', - 344: 'XClipPathUnits', - 345: 'YClipPathUnits', - 346: 'Indexed', - 347: 'JPEGTables', - 351: 'OPIProxy', - 512: 'JPEGProc', - 513: 'JPEGInterchangeFormat', - 514: 'JPEGInterchangeFormatLength', - 515: 'JPEGRestartInterval', - 517: 'JPEGLosslessPredictors', - 518: 'JPEGPointTransforms', - 519: 'JPEGQTables', - 520: 'JPEGDCTables', - 521: 'JPEGACTables', - 529: 'YCbCrCoefficients', - 530: 'YCbCrSubSampling', - 531: 'YCbCrPositioning', - 532: 'ReferenceBlackWhite', - 700: 'XMLPacket', - 18246: 'Rating', - 18249: 'RatingPercent', - 32781: 'ImageID', - 33421: 'CFARepeatPatternDim', - 33422: 'CFAPattern', - 33423: 'BatteryLevel', - 33432: 'Copyright', - 33434: 'ExposureTime', - 33437: 'FNumber', - 33723: 'IPTCNAA', - 34377: 'ImageResources', - 34665: 'ExifTag', - 34675: 'InterColorProfile', - 34850: 'ExposureProgram', - 34852: 'SpectralSensitivity', - 34853: 'GPSTag', - 34855: 'ISOSpeedRatings', - 34856: 'OECF', - 34857: 'Interlace', - 34858: 'TimeZoneOffset', - 34859: 'SelfTimerMode', - 36867: 'DateTimeOriginal', - 37122: 'CompressedBitsPerPixel', - 37377: 'ShutterSpeedValue', - 37378: 'ApertureValue', - 37379: 'BrightnessValue', - 37380: 'ExposureBiasValue', - 37381: 'MaxApertureValue', - 37382: 'SubjectDistance', - 37383: 'MeteringMode', - 37384: 'LightSource', - 37385: 'Flash', - 37386: 'FocalLength', - 37387: 'FlashEnergy', - 37388: 'SpatialFrequencyResponse', - 37389: 'Noise', - 37390: 'FocalPlaneXResolution', - 37391: 'FocalPlaneYResolution', - 37392: 'FocalPlaneResolutionUnit', - 37393: 'ImageNumber', - 37394: 'SecurityClassification', - 37395: 'ImageHistory', - 37396: 'SubjectLocation', - 37397: 'ExposureIndex', - 37398: 'TIFFEPStandardID', - 37399: 'SensingMethod', - 40091: 'XPTitle', - 40092: 'XPComment', - 40093: 'XPAuthor', - 40094: 'XPKeywords', - 40095: 'XPSubject', - 50341: 'PrintImageMatching', - 50706: 'DNGVersion', - 50707: 'DNGBackwardVersion', - 50708: 'UniqueCameraModel', - 50709: 'LocalizedCameraModel', - 50710: 'CFAPlaneColor', - 50711: 'CFALayout', - 50712: 'LinearizationTable', - 50713: 'BlackLevelRepeatDim', - 50714: 'BlackLevel', - 50715: 'BlackLevelDeltaH', - 50716: 'BlackLevelDeltaV', - 50717: 'WhiteLevel', - 50718: 'DefaultScale', - 50719: 'DefaultCropOrigin', - 50720: 'DefaultCropSize', - 50721: 'ColorMatrix1', - 50722: 'ColorMatrix2', - 50723: 'CameraCalibration1', - 50724: 'CameraCalibration2', - 50725: 'ReductionMatrix1', - 50726: 'ReductionMatrix2', - 50727: 'AnalogBalance', - 50728: 'AsShotNeutral', - 50729: 'AsShotWhiteXY', - 50730: 'BaselineExposure', - 50731: 'BaselineNoise', - 50732: 'BaselineSharpness', - 50733: 'BayerGreenSplit', - 50734: 'LinearResponseLimit', - 50735: 'CameraSerialNumber', - 50736: 'LensInfo', - 50737: 'ChromaBlurRadius', - 50738: 'AntiAliasStrength', - 50739: 'ShadowScale', - 50740: 'DNGPrivateData', - 50741: 'MakerNoteSafety', - 50778: 'CalibrationIlluminant1', - 50779: 'CalibrationIlluminant2', - 50780: 'BestQualityScale', - 50781: 'RawDataUniqueID', - 50827: 'OriginalRawFileName', - 50828: 'OriginalRawFileData', - 50829: 'ActiveArea', - 50830: 'MaskedAreas', - 50831: 'AsShotICCProfile', - 50832: 'AsShotPreProfileMatrix', - 50833: 'CurrentICCProfile', - 50834: 'CurrentPreProfileMatrix', - 50879: 'ColorimetricReference', - 50931: 'CameraCalibrationSignature', - 50932: 'ProfileCalibrationSignature', - 50934: 'AsShotProfileName', - 50935: 'NoiseReductionApplied', - 50936: 'ProfileName', - 50937: 'ProfileHueSatMapDims', - 50938: 'ProfileHueSatMapData1', - 50939: 'ProfileHueSatMapData2', - 50940: 'ProfileToneCurve', - 50941: 'ProfileEmbedPolicy', - 50942: 'ProfileCopyright', - 50964: 'ForwardMatrix1', - 50965: 'ForwardMatrix2', - 50966: 'PreviewApplicationName', - 50967: 'PreviewApplicationVersion', - 50968: 'PreviewSettingsName', - 50969: 'PreviewSettingsDigest', - 50970: 'PreviewColorSpace', - 50971: 'PreviewDateTime', - 50972: 'RawImageDigest', - 50973: 'OriginalRawFileDigest', - 50974: 'SubTileBlockSize', - 50975: 'RowInterleaveFactor', - 50981: 'ProfileLookTableDims', - 50982: 'ProfileLookTableData', - 51008: 'OpcodeList1', - 51009: 'OpcodeList2', - 51022: 'OpcodeList3', - 51041: 'NoiseProfile'} - - def __init__(self, endian, read_buffer, offset): - _Ifd.__init__(self, endian, read_buffer, offset) - self.post_process(self.tagnum2name) - - -class _ExifPhotoIfd(_Ifd): - """Represents tags found in the Exif sub ifd. - """ - tagnum2name = {33434: 'ExposureTime', - 33437: 'FNumber', - 34850: 'ExposureProgram', - 34852: 'SpectralSensitivity', - 34855: 'ISOSpeedRatings', - 34856: 'OECF', - 34864: 'SensitivityType', - 34865: 'StandardOutputSensitivity', - 34866: 'RecommendedExposureIndex', - 34867: 'ISOSpeed', - 34868: 'ISOSpeedLatitudeyyy', - 34869: 'ISOSpeedLatitudezzz', - 36864: 'ExifVersion', - 36867: 'DateTimeOriginal', - 36868: 'DateTimeDigitized', - 37121: 'ComponentsConfiguration', - 37122: 'CompressedBitsPerPixel', - 37377: 'ShutterSpeedValue', - 37378: 'ApertureValue', - 37379: 'BrightnessValue', - 37380: 'ExposureBiasValue', - 37381: 'MaxApertureValue', - 37382: 'SubjectDistance', - 37383: 'MeteringMode', - 37384: 'LightSource', - 37385: 'Flash', - 37386: 'FocalLength', - 37396: 'SubjectArea', - 37500: 'MakerNote', - 37510: 'UserComment', - 37520: 'SubSecTime', - 37521: 'SubSecTimeOriginal', - 37522: 'SubSecTimeDigitized', - 40960: 'FlashpixVersion', - 40961: 'ColorSpace', - 40962: 'PixelXDimension', - 40963: 'PixelYDimension', - 40964: 'RelatedSoundFile', - 40965: 'InteroperabilityTag', - 41483: 'FlashEnergy', - 41484: 'SpatialFrequencyResponse', - 41486: 'FocalPlaneXResolution', - 41487: 'FocalPlaneYResolution', - 41488: 'FocalPlaneResolutionUnit', - 41492: 'SubjectLocation', - 41493: 'ExposureIndex', - 41495: 'SensingMethod', - 41728: 'FileSource', - 41729: 'SceneType', - 41730: 'CFAPattern', - 41985: 'CustomRendered', - 41986: 'ExposureMode', - 41987: 'WhiteBalance', - 41988: 'DigitalZoomRatio', - 41989: 'FocalLengthIn35mmFilm', - 41990: 'SceneCaptureType', - 41991: 'GainControl', - 41992: 'Contrast', - 41993: 'Saturation', - 41994: 'Sharpness', - 41995: 'DeviceSettingDescription', - 41996: 'SubjectDistanceRange', - 42016: 'ImageUniqueID', - 42032: 'CameraOwnerName', - 42033: 'BodySerialNumber', - 42034: 'LensSpecification', - 42035: 'LensMake', - 42036: 'LensModel', - 42037: 'LensSerialNumber'} - - def __init__(self, endian, read_buffer, offset): - _Ifd.__init__(self, endian, read_buffer, offset) - self.post_process(self.tagnum2name) - - -class _ExifGPSInfoIfd(_Ifd): - """Represents information found in the GPSInfo sub IFD. - """ - tagnum2name = {0: 'GPSVersionID', - 1: 'GPSLatitudeRef', - 2: 'GPSLatitude', - 3: 'GPSLongitudeRef', - 4: 'GPSLongitude', - 5: 'GPSAltitudeRef', - 6: 'GPSAltitude', - 7: 'GPSTimeStamp', - 8: 'GPSSatellites', - 9: 'GPSStatus', - 10: 'GPSMeasureMode', - 11: 'GPSDOP', - 12: 'GPSSpeedRef', - 13: 'GPSSpeed', - 14: 'GPSTrackRef', - 15: 'GPSTrack', - 16: 'GPSImgDirectionRef', - 17: 'GPSImgDirection', - 18: 'GPSMapDatum', - 19: 'GPSDestLatitudeRef', - 20: 'GPSDestLatitude', - 21: 'GPSDestLongitudeRef', - 22: 'GPSDestLongitude', - 23: 'GPSDestBearingRef', - 24: 'GPSDestBearing', - 25: 'GPSDestDistanceRef', - 26: 'GPSDestDistance', - 27: 'GPSProcessingMethod', - 28: 'GPSAreaInformation', - 29: 'GPSDateStamp', - 30: 'GPSDifferential'} - - def __init__(self, endian, read_buffer, offset): - _Ifd.__init__(self, endian, read_buffer, offset) - self.post_process(self.tagnum2name) - - -class _ExifInteroperabilityIfd(_Ifd): - """Represents tags found in the Interoperability sub IFD. - """ - tagnum2name = {1: 'InteroperabilityIndex', - 2: 'InteroperabilityVersion', - 4096: 'RelatedImageFileFormat', - 4097: 'RelatedImageWidth', - 4098: 'RelatedImageLength'} - - def __init__(self, endian, read_buffer, offset): - _Ifd.__init__(self, endian, read_buffer, offset) - self.post_process(self.tagnum2name) - - # Map each box ID to the corresponding class. _BOX_WITH_ID = { 'asoc': AssociationBox, diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py new file mode 100644 index 0000000..6278f57 --- /dev/null +++ b/glymur/test/test_jp2box_uuid.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Test suite for printing. +""" +# C0302: don't care too much about having too many lines in a test module +# pylint: disable=C0302 + +# E061: unittest.mock introduced in 3.3 (python-2.7/pylint issue) +# pylint: disable=E0611,F0401 + +# R0904: Not too many methods in unittest. +# pylint: disable=R0904 + +import os +import re +import struct +import sys +import tempfile +import warnings +from xml.etree import cElementTree as ET + +if sys.hexversion < 0x02070000: + import unittest2 as unittest +else: + import unittest + +if sys.hexversion < 0x03000000: + from StringIO import StringIO +else: + from io import StringIO + +if sys.hexversion <= 0x03030000: + from mock import patch +else: + from unittest.mock import patch + +import glymur +from glymur import Jp2k +from .fixtures import OPJ_DATA_ROOT, opj_data_file, nemo_xmp_box + + +class TestUUIDExif(unittest.TestCase): + """Tests for UUIDs of Exif type.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + + def tearDown(self): + pass + + @unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+") + def test_unrecognized_exif_tag(self): + """Verify warning in case of unrecognized tag.""" + with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile: + + with open(self.jp2file, 'rb') as ifptr: + tfile.write(ifptr.read()) + + # Write L, T, UUID identifier. + tfile.write(struct.pack('>I4s', 52, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = struct.pack(' Date: Sat, 26 Oct 2013 16:51:07 -0400 Subject: [PATCH 04/26] Refactored UUID handling. #104 New classes for each type in _uuid_io sub package. --- glymur/_uuid_io/Exif.py | 50 ++++++++++++------- glymur/_uuid_io/XMP.py | 46 +++++++++++++++++ glymur/_uuid_io/__init__.py | 5 +- glymur/_uuid_io/generic.py | 27 ++++++++++ glymur/core.py | 45 +++++++++++++++++ glymur/jp2box.py | 95 ++++++------------------------------ glymur/test/test_jp2k.py | 2 +- glymur/test/test_printing.py | 3 +- 8 files changed, 173 insertions(+), 100 deletions(-) create mode 100644 glymur/_uuid_io/XMP.py create mode 100644 glymur/_uuid_io/generic.py diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index f70a090..3fb24b1 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """ -Handlers for various UUID types. +Handlers for Exif UUIDs. Be nice if we would find a standard for this. """ +import pprint import struct import sys import warnings @@ -12,7 +13,7 @@ if sys.hexversion < 0x02070000: else: from collections import OrderedDict -class _Exif(object): +class UUIDExif(object): """ Attributes ---------- @@ -25,10 +26,10 @@ class _Exif(object): def __init__(self, read_buffer): """Interpret raw buffer consisting of Exif IFD. """ - self.exif_image = None - self.exif_photo = None - self.exif_gpsinfo = None - self.exif_iop = None + exif_image = None + exif_photo = None + exif_gpsinfo = None + exif_iop = None self.read_buffer = read_buffer @@ -45,24 +46,39 @@ class _Exif(object): # This is the 'Exif Image' portion. exif = _ExifImageIfd(self.endian, read_buffer[6:], offset) - self.exif_image = exif.processed_ifd + exif_image = exif.processed_ifd - if 'ExifTag' in self.exif_image.keys(): - offset = self.exif_image['ExifTag'] - photo = _ExifPhotoIfd(self.endian, read_buffer[6:], offset) - self.exif_photo = photo.processed_ifd + if 'ExifTag' in exif_image.keys(): + offset = exif_image['ExifTag'] + photo_ifd = _ExifPhotoIfd(self.endian, read_buffer[6:], offset) + exif_photo = photo_ifd.processed_ifd - if 'InteroperabilityTag' in self.exif_photo.keys(): - offset = self.exif_photo['InteroperabilityTag'] + if 'InteroperabilityTag' in exif_photo.keys(): + offset = exif_photo['InteroperabilityTag'] interop = _ExifInteroperabilityIfd(self.endian, read_buffer[6:], offset) - self.iop = interop.processed_ifd + iop = interop.processed_ifd - if 'GPSTag' in self.exif_image.keys(): - offset = self.exif_image['GPSTag'] + if 'GPSTag' in exif_image.keys(): + offset = exif_image['GPSTag'] gps = _ExifGPSInfoIfd(self.endian, read_buffer[6:], offset) - self.exif_gpsinfo = gps.processed_ifd + exif_gpsinfo = gps.processed_ifd + + self.ifds = OrderedDict() + self.ifds['Image'] = exif_image + self.ifds['Photo'] = exif_photo + self.ifds['GPSInfo'] = exif_gpsinfo + self.ifds['Iop'] = exif_iop + + def __str__(self): + # 2.7 has trouble pretty-printing ordered dicts, so print them + # as regular dicts. Not ideal, but at least it's good on 3.3+. + if sys.hexversion < 0x03000000: + data = dict(self.ifds) + else: + data = self.ifds + return '\n' + pprint.pformat(data) class _Ifd(object): diff --git a/glymur/_uuid_io/XMP.py b/glymur/_uuid_io/XMP.py new file mode 100644 index 0000000..0451a92 --- /dev/null +++ b/glymur/_uuid_io/XMP.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +""" +Handler for a UUID for XMP. +""" + +import sys +from xml.etree import cElementTree as ET + +from ..core import _pretty_print_xml + +class UUIDXMP(object): + """ + Handler for a UUID for XMP. + + Attributes + ---------- + packet : ElementTree + XML conforming to the XMP specifications. + + References + ---------- + .. [XMP] International Organization for Standardication. ISO/IEC + 16684-1:2012 - Graphic technology -- Extensible metadata platform (XMP) + specification -- Part 1: Data model, serialization and core properties + """ + def __init__(self, read_buffer): + """ + Parameters + ---------- + read_buffer : byte array + sequence of bytes that can be decoded into an XMP packet. + """ + + # XMP data. Parse as XML. + if sys.hexversion < 0x03000000: + # 2.x strings same as bytes + elt = ET.fromstring(read_buffer) + else: + # 3.x takes strings, not bytes. + text = read_buffer.decode('utf-8') + elt = ET.fromstring(text) + self.packet = ET.ElementTree(elt) + + def __str__(self): + return _pretty_print_xml(self.packet) diff --git a/glymur/_uuid_io/__init__.py b/glymur/_uuid_io/__init__.py index 721ee36..5545351 100644 --- a/glymur/_uuid_io/__init__.py +++ b/glymur/_uuid_io/__init__.py @@ -1 +1,4 @@ -from .Exif import _Exif +from .Exif import UUIDExif +from .XMP import UUIDXMP +from .generic import UUIDGeneric + diff --git a/glymur/_uuid_io/generic.py b/glymur/_uuid_io/generic.py new file mode 100644 index 0000000..bad68a2 --- /dev/null +++ b/glymur/_uuid_io/generic.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +""" +Handler for a generic UUID. +""" + +class UUIDGeneric(object): + """ + Handler for a generic UUID that is not currently recognized. + + Attributes + ---------- + data : byte array + Sequence of uninterpreted bytes as read from the file. + """ + def __init__(self, read_buffer): + """ + Parameters + ---------- + read_buffer : byte array + sequence of bytes as read from the file. + """ + self.data = read_buffer + + def __str__(self): + return '{0} bytes'.format(len(self.data)) + diff --git a/glymur/core.py b/glymur/core.py index 22b5a19..620d0e3 100644 --- a/glymur/core.py +++ b/glymur/core.py @@ -1,5 +1,8 @@ """Core definitions to be shared amongst the modules. """ +import copy +import xml.etree.cElementTree as ET + # Progression order LRCP = 0 RLCP = 1 @@ -73,3 +76,45 @@ _CAPABILITIES_DISPLAY = { 1: '0', 2: '1', 3: '3'} + + +def _pretty_print_xml(xml, level=0): + """Pretty print XML data. + """ + xml = copy.deepcopy(xml) + _indent(xml.getroot(), level=level) + xmltext = ET.tostring(xml.getroot(), encoding='utf-8').decode('utf-8') + + # Indent it a bit. + lst = [(' ' + x) for x in xmltext.split('\n')] + try: + xml = '\n'.join(lst) + return '\n{0}'.format(xml) + except UnicodeEncodeError: + # This can happen on python 2.x if the character set contains certain + # non-ascii characters. Just print out the corresponding xml char + # entities instead. + xml = u'\n'.join(lst) + text = u'\n{0}'.format(xml) + text = text.encode('ascii', 'xmlcharrefreplace') + return text + + +def _indent(elem, level=0): + """Recipe for pretty printing XML. Please see + + http://effbot.org/zone/element-lib.htm#prettyprint + """ + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + _indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 3ea911e..681cf51 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -13,7 +13,6 @@ References # pylint: disable=C0302,R0903,R0913 -import copy import datetime import math import os @@ -38,8 +37,9 @@ from .core import _COLORSPACE_MAP_DISPLAY from .core import _COLOR_TYPE_MAP_DISPLAY from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD +from .core import _pretty_print_xml -from ._uuid_io import _Exif +from . import _uuid_io _METHOD_DISPLAY = { ENUMERATED_COLORSPACE: 'enumerated colorspace', @@ -2065,8 +2065,11 @@ class UUIDBox(Jp2kBox): ---------- the_uuid : uuid.UUID Identifies the type of UUID box. + data : object + Specific to each type of UUID. There are handlers for XMP, Exif, + and unknown UUIDs. raw_data : byte array - This is the "payload" of data for the specified UUID. + Sequence of uninterpreted bytes as read from the file. length : int length of the box in bytes. offset : int @@ -2076,59 +2079,33 @@ class UUIDBox(Jp2kBox): self.uuid = the_uuid if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - # XMP data. Parse as XML. Seems to be a difference between - # ElementTree in version 2.7 and 3.3. - if sys.hexversion < 0x03000000: - elt = ET.fromstring(raw_data) - else: - text = raw_data.decode('utf-8') - elt = ET.fromstring(text) - self.data = ET.ElementTree(elt) + self.data = _uuid_io.UUIDXMP(raw_data) self._type = 'XMP' elif the_uuid.bytes == b'JpgTiffExif->JP2': - exif_obj = _Exif(raw_data) - ifds = OrderedDict() - ifds['Image'] = exif_obj.exif_image - ifds['Photo'] = exif_obj.exif_photo - ifds['GPSInfo'] = exif_obj.exif_gpsinfo - ifds['Iop'] = exif_obj.exif_iop - self.data = ifds + self.data = _uuid_io.UUIDExif(raw_data) self._type = 'Exif' else: - self.data = raw_data + self.data = _uuid_io.UUIDGeneric(raw_data) self._type = 'unknown' + + self.raw_data = raw_data self.length = length self.offset = offset def __str__(self): msg = '{0}\n' - msg += ' UUID: {1}{2}\n' + msg += ' UUID: {1} ({2})\n' msg += ' UUID Data: {3}' - if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - uuid_type = ' (XMP)' - uuid_data = _pretty_print_xml(self.data) - elif self.uuid.bytes == b'JpgTiffExif->JP2': - uuid_type = ' (Exif)' - # 2.7 has trouble pretty-printing ordered dicts, so print them - # as regular dicts. Not ideal, but at least it's good on 3.3+. - if sys.hexversion < 0x03000000: - data = dict(self.data) - else: - data = self.data - uuid_data = '\n' + pprint.pformat(data) - else: - uuid_type = '' - uuid_data = '{0} bytes'.format(len(self.data)) - msg = msg.format(Jp2kBox.__str__(self), self.uuid, - uuid_type, - uuid_data) + self._type, + str(self.data)) return msg + def write(self, fptr): """Write a UUID box box to file. """ @@ -2196,45 +2173,3 @@ _BOX_WITH_ID = { 'url ': DataEntryURLBox, 'uuid': UUIDBox, 'xml ': XMLBox} - - -def _indent(elem, level=0): - """Recipe for pretty printing XML. Please see - - http://effbot.org/zone/element-lib.htm#prettyprint - """ - i = "\n" + level * " " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - _indent(elem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - - -def _pretty_print_xml(xml, level=0): - """Pretty print XML data. - """ - xml = copy.deepcopy(xml) - _indent(xml.getroot(), level=level) - xmltext = ET.tostring(xml.getroot(), encoding='utf-8').decode('utf-8') - - # Indent it a bit. - lst = [(' ' + x) for x in xmltext.split('\n')] - try: - xml = '\n'.join(lst) - return '\n{0}'.format(xml) - except UnicodeEncodeError: - # This can happen on python 2.x if the character set contains certain - # non-ascii characters. Just print out the corresponding xml char - # entities instead. - xml = u'\n'.join(lst) - text = u'\n{0}'.format(xml) - text = text.encode('ascii', 'xmlcharrefreplace') - return text diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index c9e750a..026b129 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -353,7 +353,7 @@ class TestJp2k(unittest.TestCase): def test_xmp_attribute(self): """Verify the XMP packet in the shipping example file can be read.""" j = Jp2k(self.jp2file) - xmp = j.box[3].data + xmp = j.box[3].data.packet ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' ns2 = '{http://ns.adobe.com/xap/1.0/}' name = '{0}RDF/{0}Description/{1}CreatorTool'.format(ns0, ns2) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 5824e0e..a1f0c55 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -636,6 +636,7 @@ class TestPrinting(unittest.TestCase): actual = fake_out.getvalue().strip() expected = nemo_xmp_box + self.maxDiff = None self.assertEqual(actual, expected) def test_codestream(self): @@ -1024,7 +1025,7 @@ class TestPrinting(unittest.TestCase): print(jp2.box[4]) actual = fake_out.getvalue().strip() lines = ['UUID Box (uuid) @ (1544, 25)', - ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71', + ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)', ' UUID Data: 1 bytes'] expected = '\n'.join(lines) From 7e717b50370f56dd0895922196098ef6c72c347c Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 26 Oct 2013 18:05:44 -0400 Subject: [PATCH 05/26] Made Exif handling more resilient. #104 --- glymur/_uuid_io/Exif.py | 6 +++- glymur/jp2box.py | 23 +++++++++---- glymur/test/test_jp2box_uuid.py | 57 ++++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index 3fb24b1..0a86c18 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -39,9 +39,13 @@ class UUIDExif(object): if data[0] == 73 and data[1] == 73: # little endian self.endian = '<' - else: + elif data[0] == 77 and data[1] == 77: # big endian self.endian = '>' + else: + msg = "Bad byte order indication: {0}".format(read_buffer[6:8]) + raise RuntimeError(msg) + offset = data[3] # This is the 'Exif Image' portion. diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 681cf51..074d18e 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -19,6 +19,7 @@ import os import pprint import struct import sys +import traceback import uuid import warnings import xml.etree.cElementTree as ET @@ -2078,15 +2079,23 @@ class UUIDBox(Jp2kBox): Jp2kBox.__init__(self, box_id='uuid', longname='UUID') self.uuid = the_uuid - if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - self.data = _uuid_io.UUIDXMP(raw_data) - self._type = 'XMP' - elif the_uuid.bytes == b'JpgTiffExif->JP2': - self.data = _uuid_io.UUIDExif(raw_data) - self._type = 'Exif' - else: + try: + if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + self.data = _uuid_io.UUIDXMP(raw_data) + self._type = 'XMP' + elif the_uuid.bytes == b'JpgTiffExif->JP2': + self.data = _uuid_io.UUIDExif(raw_data) + self._type = 'Exif' + else: + self.data = _uuid_io.UUIDGeneric(raw_data) + self._type = 'unknown' + except Exception as err: + # In case of any exception, create the generic UUID. self.data = _uuid_io.UUIDGeneric(raw_data) self._type = 'unknown' + msg = "Error encountered during UUID processing, " + msg += "the UUID will be treated as generic.\n\n{0}" + warnings.warn(msg.format(traceback.format_exc())) self.raw_data = raw_data diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py index 6278f57..a1d93a4 100644 --- a/glymur/test/test_jp2box_uuid.py +++ b/glymur/test/test_jp2box_uuid.py @@ -63,7 +63,7 @@ class TestUUIDExif(unittest.TestCase): xbuffer = struct.pack('I4s', 52, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = struct.pack('I4s', 52, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = struct.pack(' Date: Sat, 26 Oct 2013 18:21:24 -0400 Subject: [PATCH 06/26] Pylint issues, #104 --- glymur/_uuid_io/Exif.py | 2 +- glymur/_uuid_io/__init__.py | 4 +++- glymur/jp2box.py | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index 0a86c18..ccc03dd 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -62,7 +62,7 @@ class UUIDExif(object): interop = _ExifInteroperabilityIfd(self.endian, read_buffer[6:], offset) - iop = interop.processed_ifd + exif_iop = interop.processed_ifd if 'GPSTag' in exif_image.keys(): offset = exif_image['GPSTag'] diff --git a/glymur/_uuid_io/__init__.py b/glymur/_uuid_io/__init__.py index 5545351..a23c2ce 100644 --- a/glymur/_uuid_io/__init__.py +++ b/glymur/_uuid_io/__init__.py @@ -1,4 +1,6 @@ +""" +Sub package for handling various UUIDs. +""" from .Exif import UUIDExif from .XMP import UUIDXMP from .generic import UUIDGeneric - diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 074d18e..34eeeb8 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -2089,7 +2089,7 @@ class UUIDBox(Jp2kBox): else: self.data = _uuid_io.UUIDGeneric(raw_data) self._type = 'unknown' - except Exception as err: + except Exception: # In case of any exception, create the generic UUID. self.data = _uuid_io.UUIDGeneric(raw_data) self._type = 'unknown' @@ -2121,15 +2121,15 @@ class UUIDBox(Jp2kBox): if self._type != 'XMP': msg = "Only XMP UUID boxes can currently be written." raise NotImplementedError(msg) - serialized_buffer = b'' - serialized_buffer += ET.tostring(self.data.getroot(), encoding='utf-8') - serialized_buffer += b'' + serialized = b'' + serialized += ET.tostring(self.data.packet.getroot(), encoding='utf-8') + serialized += b'' if self.length == 0: - self.length = 24 + len(serialized_buffer) + self.length = 24 + len(serialized) read_buffer = struct.pack('>I4s', self.length, b'uuid') fptr.write(read_buffer) fptr.write(self.uuid.bytes) - fptr.write(serialized_buffer) + fptr.write(serialized) @staticmethod def parse(fptr, offset, length): From c4060f23c88ece87a5e97daccf1c7c37cbe80b88 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 26 Oct 2013 19:23:33 -0400 Subject: [PATCH 07/26] Added big endian Exif support. #104 --- glymur/_uuid_io/Exif.py | 6 +++--- glymur/test/test_jp2box_uuid.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index ccc03dd..fb5e2a1 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -34,8 +34,8 @@ class UUIDExif(object): self.read_buffer = read_buffer # Ignore the first six bytes. - # Next 8 should be (73, 73, 42, 8) - data = struct.unpack('I4s', 52, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = struct.pack('>BBHI', 77, 77, 42, 8) + tfile.write(xbuffer) + + # We will write just a single tag. + tfile.write(struct.pack('>H', 1)) + + # The "Make" tag is tag no. 271. + tfile.write(struct.pack('>HHI4s', 271, 2, 3, b'HTC\x00')) + tfile.flush() + + jp2 = glymur.Jp2k(tfile.name) + self.assertEqual(jp2.box[-1].data.ifds['Image']['Make'], "HTC") + if __name__ == "__main__": unittest.main() From 119dc0dfcd124332213270bea3dfcef19d5d9aa6 Mon Sep 17 00:00:00 2001 From: John Evans Date: Sun, 27 Oct 2013 14:28:22 -0400 Subject: [PATCH 08/26] minor documentation tweaks. #104 --- glymur/jp2box.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index c02f497..ec1b349 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1209,7 +1209,7 @@ class JPEG2000SignatureBox(Jp2kBox): offset of the box from the start of the file. longname : str more verbose description of the box. - signature : byte + signature : tuple Four-byte tuple identifying the file as JPEG 2000. """ def __init__(self, signature=(13, 10, 135, 10), length=0, offset=-1): @@ -1807,7 +1807,7 @@ class LabelBox(Jp2kBox): longname : str more verbose description of the box. label : str - Label + Textual label. """ def __init__(self, label, length=0, offset=-1): Jp2kBox.__init__(self, box_id='lbl ', longname='Label') @@ -2179,9 +2179,12 @@ class UUIDBox(Jp2kBox): more verbose description of the box. uuid : uuid.UUID 16-byte UUID - data : bytes or dict or ElementTree.Element - Vendor-specific data. Exif UUIDs are interpreted as dictionaries. - XMP UUIDs are interpreted as standard XML. + raw_data : byte array + Sequence of uninterpreted bytes as read from the file. + data : object + Specific to each type of UUID. There are handlers for XMP, Exif, and + generic (unknown) UUIDs. In the case of XMP and Exif UUIDs, this is + the interpreted version of raw_data. References ---------- @@ -2195,9 +2198,6 @@ class UUIDBox(Jp2kBox): ---------- the_uuid : uuid.UUID Identifies the type of UUID box. - data : object - Specific to each type of UUID. There are handlers for XMP, Exif, - and unknown UUIDs. raw_data : byte array Sequence of uninterpreted bytes as read from the file. length : int From 727c4dd2ea357a5dc57d551a318cf08f0dc60ee7 Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 27 Oct 2013 18:13:42 -0400 Subject: [PATCH 09/26] Verifying that we can write XMP UUIDs. --- glymur/test/fixtures.py | 16 ++++++++++++++++ glymur/test/test_jp2box_uuid.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 68c0706..9c543bf 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -254,3 +254,19 @@ nemo_xmp_box = """UUID Box (uuid) @ (77, 3146) """ + +SimpleRDF = """ + + + Simple value + + + + Suse + Fedora + + + + +""" diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py index e8cb7be..c17a6ab 100644 --- a/glymur/test/test_jp2box_uuid.py +++ b/glymur/test/test_jp2box_uuid.py @@ -12,9 +12,11 @@ import os import re +import shutil import struct import sys import tempfile +import uuid import warnings from xml.etree import cElementTree as ET @@ -35,9 +37,38 @@ else: import glymur from glymur import Jp2k -from .fixtures import OPJ_DATA_ROOT, opj_data_file, nemo_xmp_box +from .fixtures import OPJ_DATA_ROOT, opj_data_file, SimpleRDF +class TestUUIDXMP(unittest.TestCase): + """Tests for UUIDs of XMP type.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + + def tearDown(self): + pass + + def test_append(self): + """Should be able to append an XMP UUID box.""" + the_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac') + raw_data = SimpleRDF.encode('utf-8') + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + shutil.copyfile(self.jp2file, tfile.name) + jp2 = Jp2k(tfile.name) + ubox = glymur.jp2box.UUIDBox(the_uuid=the_uuid, raw_data=raw_data) + jp2.append(ubox) + + # Should be two UUID boxes now. + expected_ids = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'uuid'] + actual_ids = [b.box_id for b in jp2.box] + self.assertEqual(actual_ids, expected_ids) + + # The data should be an XMP packet, which gets interpreted as + # an ElementTree. + self.assertTrue(isinstance(jp2.box[-1].data.packet, + ET.ElementTree)) + class TestUUIDExif(unittest.TestCase): """Tests for UUIDs of Exif type.""" From 32760a6ecc6322ecd746a15b5adbbad73f8608aa Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 27 Oct 2013 22:09:15 -0400 Subject: [PATCH 10/26] Doc updates for writing XMP UUIDs. #104 --- docs/source/how_do_i.rst | 84 ++++++++++++++++++++++-------------- docs/source/introduction.rst | 12 +++--- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 212862d..443e8db 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -4,7 +4,7 @@ How do I...? ... 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 resolution level. :: @@ -15,7 +15,7 @@ resolution level. :: >>> thumbnail = j.read(rlevel=-1) ... display metadata? -================= +===================== There are two ways. From the unix command line, the script *jp2dump* is available. :: @@ -35,7 +35,7 @@ codestream box, only the main header is printed. It is possible to print >>> print(j.get_codestream()) ... add XML metadata? -================= +===================== You can append any number of XML boxes to a JP2 file (not to a raw codestream). Consider the following XML file `data.xml` : :: @@ -66,12 +66,12 @@ The **append** method can add an XML box as shown below:: >>> jp2.append(xmlbox) >>> print(jp2) -... add metadata in a more general fashion? -======================================= +... add even more metadata? +=========================== An existing raw codestream (or JP2 file) can be wrapped (re-wrapped) in a user-defined set of JP2 boxes. To get just a minimal JP2 jacket on the codestream provided by `goodstuff.j2k` (a file consisting of a raw codestream), -you can use the **wrap** method with no box argument: :: +you can use the :py:meth:`wrap` method with no box argument: :: >>> import glymur >>> jfile = glymur.data.goodstuff() @@ -108,8 +108,9 @@ JP2 header superbox. XML boxes are not in the minimal set of box requirements for the JP2 format, so in order to add an XML box into the mix before the codestream box, we'll need to -re-specify all of the boxes. If you already have a JP2 jacket in place, you can just reuse that, -though. Take the following example content in an XML file `favorites.xml` : :: +re-specify all of the boxes. If you already have a JP2 jacket in place, +you can just reuse that, though. Take the following example content in +an XML file `favorites.xml` : :: @@ -152,13 +153,13 @@ the following will work. :: . (truncated) . -As to the question of which method you should use, **append** or **wrap**, -to add metadata, you should keep in mind that **wrap** produces a new JP2 file, -while **append** modifies an existing file and is currently limited to XML -boxes. +As to the question of which method you should use, :py:meth:`append` or +:py:meth:`wrap`, to add metadata, you should keep in mind that :py:meth:`wrap` +produces a new JP2 file, while :py:meth:`append` modifies an existing file and +is currently limited to XML and UUID boxes. ... create an image with an alpha layer? -==================================== +======================================== OpenJPEG can create JP2 files with more than 3 components (requires the development version of OpenJPEG), but by default, any extra components are @@ -219,32 +220,49 @@ Here's how the Preview application on the mac shows the RGBA image. .. image:: goodstuff_alpha.png -work with XMP UUIDs? -==================== +... work with XMP UUIDs? +======================== The example JP2 file shipped with glymur has an XMP UUID. :: >>> import glymur >>> j = glymur.Jp2k(glymur.data.nemo()) - >>> print(j.box[4]) - UUID Box (uuid) @ (715, 2412) - UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) - UUID Data: - + >>> print(j.box[3]) # formatting added to the XML below + - - - + . + . + . + -Since the UUID data in this case is returned as an ElementTree instance, one can -use ElementTree to access the data. For example, to extract the -**CreatorTool** attribute value, the following would work:: +Since the UUID data in this case is returned as an ElementTree instance, +one can use ElementTree from the standard library to access the data. +For example, to extract the **CreatorTool** attribute value, the following +would work:: - >>> xmp = j.box[4].data - >>> ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' - >>> ns1 = '{http://ns.adobe.com/xap/1.0/}' - >>> name = '{0}RDF/{0}Description'.format(ns0) + >>> xmp = j.box[3].data.packet + >>> rdf = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' + >>> ns2 = '{http://ns.adobe.com/xap/1.0/}' + >>> name = '{0}RDF/{0}Description/{1}CreatorTool'.format(rdf, ns2) >>> elt = xmp.find(name) >>> elt - - >>> elt.attrib['{0}CreatorTool'.format(ns1)] - 'glymur' + + >>> elt.text + 'Google' + +Yes, that's painful. A better solution is to install the Python XMP Toolkit +(developer branch):: + + >>> from libxmp import XMPMeta + >>> from libxmp.consts import XMP_NS_XMP as NS_XAP + >>> meta = XMPMeta() + >>> meta.parse_from_str(j.box[3].raw_data.decode('utf-8')) + >>> meta.get_property(NS_XAP, 'CreatorTool') + 'Google' + diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index e1a3b3b..0a88902 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -7,11 +7,9 @@ which allows one to read and write JPEG 2000 files from within Python. Glymur supports both reading and writing of JPEG 2000 images, but writing JPEG 2000 images is currently limited to images that can fit in memory -Of particular focus is retrieval of metadata. Reading Exif UUIDs is supported, -as is reading XMP UUIDs as the XMP data packet is just XML. There is -some very limited support for reading JPX metadata. For instance, -**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can -be retrieved from such JPX files. +In regards to metadata, most JP2 boxes are properly interpreted. +Certain optional JP2 boxes can also be written, including XML boxes and +XMP UUIDs. There is some very limited support for reading JPX metadata. Glymur works on Python 2.6, 2.7, and 3.3. @@ -20,8 +18,8 @@ OpenJPEG Installation Glymur will read JPEG 2000 images with versions 1.3, 1.4, 1.5, 2.0, and the trunk/development version of OpenJPEG. Writing images is only supported with the 1.5 or better, however, and the trunk/development -version is strongly recommended. For more information about OpenJPEG, -please consult http://www.openjpeg.org. +version of OpenJPEG is strongly recommended. For more information about +OpenJPEG, please consult http://www.openjpeg.org. If you use MacPorts or if you have a sufficiently recent version of Linux, your package manager should already provide you with a version of From ffe17f12cbb9545e0d5abcc4f57ffe48dd6ebdba Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 23 Jan 2014 21:50:48 -0500 Subject: [PATCH 11/26] Introducing python-xmp-toolkit requirement. #104 Down to 3 failures and 1 error. --- glymur/_uuid_io/Exif.py | 83 +++++++-------------------------- glymur/_uuid_io/XMP.py | 46 ------------------ glymur/_uuid_io/__init__.py | 6 +-- glymur/_uuid_io/generic.py | 27 ----------- glymur/jp2box.py | 77 ++++++++++++++---------------- glymur/jp2k.py | 4 +- glymur/test/test_jp2box_uuid.py | 5 +- glymur/test/test_jp2k.py | 12 ++--- setup.py | 2 +- 9 files changed, 67 insertions(+), 195 deletions(-) delete mode 100644 glymur/_uuid_io/XMP.py delete mode 100644 glymur/_uuid_io/generic.py diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index fb5e2a1..c1b30f0 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -13,76 +13,27 @@ if sys.hexversion < 0x02070000: else: from collections import OrderedDict -class UUIDExif(object): +def tiff_header(read_buffer): """ - Attributes - ---------- - read_buffer : bytes - Raw byte stream consisting of the UUID data. - endian : str - Either '<' for big-endian, or '>' for little-endian. """ + # Ignore the first six bytes. + # Next 8 should be (73, 73, 42, 8) or (77, 77, 42, 8) + data = struct.unpack('JP2': - self.data = _uuid_io.UUIDExif(raw_data) - self._type = 'Exif' - else: - self.data = _uuid_io.UUIDGeneric(raw_data) - self._type = 'unknown' - except Exception: - # In case of any exception, create the generic UUID. - self.data = _uuid_io.UUIDGeneric(raw_data) - self._type = 'unknown' - msg = "Error encountered during UUID processing, " - msg += "the UUID will be treated as generic.\n\n{0}" - warnings.warn(msg.format(traceback.format_exc())) - - self.raw_data = raw_data - self.length = length self.offset = offset + self.data = None + + try: + self._parse_raw_data() + except Exception as e: + warnings.warn(str(e)) + + def _parse_raw_data(self): + """ + Private function for parsing UUID payloads if possible. + """ + if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + xmp = XMPMeta() + xmp.parse_from_str(self.raw_data.decode('utf-8'), + xmpmeta_wrap=False) + self.data = xmp + elif self.uuid.bytes == b'JpgTiffExif->JP2': + self.data = _uuid_io.tiff_header(self.raw_data) + else: + self.data = self.raw_data def __repr__(self): msg = "glymur.jp2box.UUIDBox(the_uuid={0}, " msg += "raw_data=)" - return msg.format(repr(self.uuid), len(self.raw_data)) - + return msg.format(repr(self.uuid), len(self.data)) def __str__(self): msg = '{0}\n' - msg += ' UUID: {1} ({2})\n' - msg += ' UUID Data: {3}' + msg += ' UUID: {1}\n' + msg += ' UUID Data: {2}' - msg = msg.format(Jp2kBox.__str__(self), - self.uuid, - self._type, - str(self.data)) + msg = msg.format(Jp2kBox.__str__(self), self.uuid, str(self.data)) return msg - def write(self, fptr): - """Write a UUID box box to file. + """Write a UUID box to file. """ - if self._type != 'XMP': + if self.uuid != uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): msg = "Only XMP UUID boxes can currently be written." raise NotImplementedError(msg) - serialized = b'' - serialized += ET.tostring(self.data.packet.getroot(), encoding='utf-8') - serialized += b'' - if self.length == 0: - self.length = 24 + len(serialized) - read_buffer = struct.pack('>I4s', self.length, b'uuid') - fptr.write(read_buffer) + write_buffer = struct.pack('>I4s', self.length, b'uuid') + fptr.write(write_buffer) fptr.write(self.uuid.bytes) - fptr.write(serialized) + fptr.write(self.raw_data) @staticmethod def parse(fptr, offset, length): diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 4d7071e..a337ae7 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -20,6 +20,7 @@ import ctypes import math import os import struct +from uuid import UUID import warnings import numpy as np @@ -526,7 +527,8 @@ class Jp2k(Jp2kBox): raise IOError(msg) if not ((box.box_id == 'xml ') or - (box.box_id == 'uuid' and box._type == 'XMP')): + (box.box_id == 'uuid' and + box.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'))): msg = "Only XML boxes and XMP UUID boxes can currently be appended." raise IOError(msg) diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py index c17a6ab..8f24f74 100644 --- a/glymur/test/test_jp2box_uuid.py +++ b/glymur/test/test_jp2box_uuid.py @@ -35,6 +35,8 @@ if sys.hexversion <= 0x03030000: else: from unittest.mock import patch +from libxmp import XMPMeta + import glymur from glymur import Jp2k from .fixtures import OPJ_DATA_ROOT, opj_data_file, SimpleRDF @@ -66,8 +68,7 @@ class TestUUIDXMP(unittest.TestCase): # The data should be an XMP packet, which gets interpreted as # an ElementTree. - self.assertTrue(isinstance(jp2.box[-1].data.packet, - ET.ElementTree)) + self.assertTrue(isinstance(jp2.box[-1].data, XMPMeta)) class TestUUIDExif(unittest.TestCase): """Tests for UUIDs of Exif type.""" diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index c38cacd..c055aba 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -30,6 +30,8 @@ import warnings import numpy as np import pkg_resources +import libxmp + import glymur from glymur import Jp2k @@ -362,13 +364,9 @@ class TestJp2k(unittest.TestCase): def test_xmp_attribute(self): """Verify the XMP packet in the shipping example file can be read.""" j = Jp2k(self.jp2file) - xmp = j.box[3].data.packet - ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' - ns2 = '{http://ns.adobe.com/xap/1.0/}' - name = '{0}RDF/{0}Description/{1}CreatorTool'.format(ns0, ns2) - elt = xmp.find(name) - self.assertEqual(elt.text, 'Google') - + xmp = j.box[3].data + creator_tool = xmp.get_property(libxmp.consts.XMP_NS_XMP, 'CreatorTool') + self.assertEqual(creator_tool, 'Google') @unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version), "Requires at least version 1.5") diff --git a/setup.py b/setup.py index b780e4d..b4fdc2f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ kwargs = {'name': 'Glymur', 'license': 'MIT', 'test_suite': 'glymur.test'} -instllrqrs = ['numpy>=1.4.1'] +instllrqrs = ['numpy>=1.4.1', 'python-xmp-toolkit>=2.0.0'] if sys.hexversion < 0x03030000: instllrqrs.append('contextlib2>=0.4') instllrqrs.append('mock>=1.0.1') From 553b36d40ee1bf3a043db39bdc4f68fa902c7565 Mon Sep 17 00:00:00 2001 From: John Evans Date: Fri, 24 Jan 2014 11:08:23 -0500 Subject: [PATCH 12/26] Passing on Anaconda platform with python-xmp-toolkit 2.0. #104 --- glymur/_uuid_io/Exif.py | 32 ++++++ glymur/_uuid_io/__init__.py | 2 +- glymur/jp2box.py | 29 +++--- glymur/test/fixtures.py | 174 +++++++++++++++++--------------- glymur/test/test_jp2box_uuid.py | 2 +- 5 files changed, 138 insertions(+), 101 deletions(-) diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index c1b30f0..a7c04aa 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -3,6 +3,7 @@ Handlers for Exif UUIDs. Be nice if we would find a standard for this. """ import pprint +import re import struct import sys import warnings @@ -13,8 +14,39 @@ if sys.hexversion < 0x02070000: else: from collections import OrderedDict +# The Python XMP Toolkit may be used for XMP UUIDs, but only if available and +# if the version is at least 2.0.0. +try: + import libxmp + if hasattr(libxmp, 'version') and re.match('[2-9].\d*.\d*', libxmp.version.VERSION): + from libxmp import XMPMeta + _HAS_PYTHON_XMP_TOOLKIT = True + else: + _HAS_PYTHON_XMP_TOOLKIT = False +except ImportError: + _HAS_PYTHON_XMP_TOOLKIT = False + +def xmp(read_buffer): + """ + If libxmp 2.0+ is installed, use it to describe the XMP data. + """ + if not _HAS_PYTHON_XMP_TOOLKIT: + # If the python xmp toolkit is not available or is not advanced enough, + # then issue a warning and just make available the raw data. + msg = "An XMP UUID was detected, but the Python XMP Toolkit package " + msg += "is either not available or is too old (must be at least 2.0). " + msg += "The UUID data field will consist only of the raw uninterpreted " + msg += "bytes." + warnings.warn(msg, UserWarning) + return read_buffer + + xmp = XMPMeta() + xmp.parse_from_str(read_buffer.decode('utf-8'), xmpmeta_wrap=False) + return xmp + def tiff_header(read_buffer): """ + Interpret the UUID data as a TIFF header. """ # Ignore the first six bytes. # Next 8 should be (73, 73, 42, 8) or (77, 77, 42, 8) diff --git a/glymur/_uuid_io/__init__.py b/glymur/_uuid_io/__init__.py index d12d20c..89bbf21 100644 --- a/glymur/_uuid_io/__init__.py +++ b/glymur/_uuid_io/__init__.py @@ -1,4 +1,4 @@ """ Sub package for handling various types of UUIDs. """ -from .Exif import tiff_header +from .Exif import tiff_header, xmp diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 300ac12..8b0f6f3 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -33,12 +33,6 @@ else: import numpy as np -try: - from libxmp import XMPMeta - _HAS_PYTHON_XMP_TOOLKIT = True -except ImportError: - _HAS_PYTHON_XMP_TOOLKIT = False - from .codestream import Codestream from .core import _COLORSPACE_MAP_DISPLAY from .core import _COLOR_TYPE_MAP_DISPLAY @@ -2228,10 +2222,7 @@ class UUIDBox(Jp2kBox): Private function for parsing UUID payloads if possible. """ if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - xmp = XMPMeta() - xmp.parse_from_str(self.raw_data.decode('utf-8'), - xmpmeta_wrap=False) - self.data = xmp + self.data = _uuid_io.xmp(self.raw_data) elif self.uuid.bytes == b'JpgTiffExif->JP2': self.data = _uuid_io.tiff_header(self.raw_data) else: @@ -2243,11 +2234,19 @@ class UUIDBox(Jp2kBox): return msg.format(repr(self.uuid), len(self.data)) def __str__(self): - msg = '{0}\n' - msg += ' UUID: {1}\n' - msg += ' UUID Data: {2}' - - msg = msg.format(Jp2kBox.__str__(self), self.uuid, str(self.data)) + if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + utype = "XMP" + elif self.uuid.bytes == b'JpgTiffExif->JP2': + utype = "EXIF" + else: + utype = "unknown" + msg = '{0}\n UUID: {1} ({2})\n'.format(Jp2kBox.__str__(self), + self.uuid, + utype) + if utype == 'unknown': + msg += ' UUID Data: {0} bytes'.format(len(self.raw_data)) + else: + msg += ' UUID Data: {0}'.format(str(self.data)) return msg diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 9c543bf..a33d1d6 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -170,90 +170,96 @@ def read_pgx_header(pgx_file): nemo_xmp_box = """UUID Box (uuid) @ (77, 3146) UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) - UUID Data: - - - - Google - 2013-02-09T14:47:53 - - - 1 - 72/1 - 72/1 - 2 - HTC - HTC Glacier - 2592 - 1456 - - - 8 - 8 - 8 - - - 2 - 3 - - - 1343036288/4294967295 - 1413044224/4294967295 - - - - - 2748779008/4294967295 - 1417339264/4294967295 - 1288490240/4294967295 - 2576980480/4294967295 - 644245120/4294967295 - 257698032/4294967295 - - - - - 1 - 2528 - 1424 - 353/100 - 0 - 0/1 - WGS-84 - 2013-02-09T14:47:53 - - - 76 - - - 0220 - 0100 - - - 1 - 2 - 3 - 0 - - - 42,20.56N - 71,5.29W - 2013-02-09T19:47:53Z - NETWORK - - - 2013-02-09T14:47:53 - - - - - Glymur - Python XMP Toolkit - - - - - """ + UUID Data: + + + + Google + 2013-02-09T14:47:53 + + + 1 + 72/1 + 72/1 + 2 + HTC + HTC Glacier + 2592 + 1456 + + + 8 + 8 + 8 + + + 2 + 3 + + + 1343036288/4294967295 + 1413044224/4294967295 + + + + + 2748779008/4294967295 + 1417339264/4294967295 + 1288490240/4294967295 + 2576980480/4294967295 + 644245120/4294967295 + 257698032/4294967295 + + + + + 1 + 2528 + 1424 + 353/100 + 0 + 0/1 + WGS-84 + 2013-02-09T14:47:53 + + + 76 + + + 0220 + 0100 + + + 1 + 2 + 3 + 0 + + + 42,20.56N + 71,5.29W + 2013-02-09T19:47:53Z + NETWORK + + + 2013-02-09T14:47:53 + + + + + Glymur + Python XMP Toolkit + + + + + +""" SimpleRDF = """ Date: Fri, 24 Jan 2014 15:35:23 -0500 Subject: [PATCH 13/26] Tests passing on bare 2.6.6 python. #104 --- glymur/test/fixtures.py | 12 ++++++++++++ glymur/test/test_jp2box_uuid.py | 14 ++++++++++---- glymur/test/test_jp2k.py | 9 ++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index a33d1d6..164f93d 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -11,6 +11,18 @@ import numpy as np import glymur +# The Python XMP Toolkit may be used for XMP UUIDs, but only if available and +# if the version is at least 2.0.0. +try: + import libxmp + if hasattr(libxmp, 'version') and re.match('[2-9].\d*.\d*', libxmp.version.VERSION): + from libxmp import XMPMeta + HAS_PYTHON_XMP_TOOLKIT = True + else: + HAS_PYTHON_XMP_TOOLKIT = False +except ImportError: + HAS_PYTHON_XMP_TOOLKIT = False + # Need to know of the libopenjp2 version is the official 2.0.0 release and NOT # the 2.0+ development version. OPENJP2_IS_V2_OFFICIAL = False diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py index 3e7f196..1415777 100644 --- a/glymur/test/test_jp2box_uuid.py +++ b/glymur/test/test_jp2box_uuid.py @@ -35,7 +35,9 @@ if sys.hexversion <= 0x03030000: else: from unittest.mock import patch -from libxmp import XMPMeta +from .fixtures import HAS_PYTHON_XMP_TOOLKIT, OPJ_DATA_ROOT +if HAS_PYTHON_XMP_TOOLKIT: + from libxmp import XMPMeta import glymur from glymur import Jp2k @@ -66,9 +68,13 @@ class TestUUIDXMP(unittest.TestCase): actual_ids = [b.box_id for b in jp2.box] self.assertEqual(actual_ids, expected_ids) - # The data should be an XMP packet, which gets interpreted as - # an ElementTree. - self.assertTrue(isinstance(jp2.box[-1].data, XMPMeta)) + # The data should be an XMP packet + if HAS_PYTHON_XMP_TOOLKIT: + # when python xmp toolkit is available. + self.assertTrue(isinstance(jp2.box[-1].data, XMPMeta)) + else: + # when python xmp toolkit is not available. + self.assertTrue(isinstance(jp2.box[-1].data, str)) class TestUUIDExif(unittest.TestCase): """Tests for UUIDs of Exif type.""" diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index c055aba..cf5596e 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -30,12 +30,13 @@ import warnings import numpy as np import pkg_resources -import libxmp - import glymur from glymur import Jp2k -from .fixtures import OPENJP2_IS_V2_OFFICIAL +from .fixtures import HAS_PYTHON_XMP_TOOLKIT, OPENJP2_IS_V2_OFFICIAL +if HAS_PYTHON_XMP_TOOLKIT: + import libxmp + from .fixtures import OPJ_DATA_ROOT, opj_data_file @@ -361,6 +362,8 @@ class TestJp2k(unittest.TestCase): self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()), b'this is a test') + @unittest.skipIf(not HAS_PYTHON_XMP_TOOLKIT, + "Requires Python XMP Toolkit >= 2.0") def test_xmp_attribute(self): """Verify the XMP packet in the shipping example file can be read.""" j = Jp2k(self.jp2file) From c8f6aff4d82c8782af745c8f4b5182aae597387c Mon Sep 17 00:00:00 2001 From: John Evans Date: Fri, 24 Jan 2014 15:56:50 -0500 Subject: [PATCH 14/26] pylint work. #104 --- glymur/_uuid_io/Exif.py | 3 +- glymur/codestream.py | 4 +- glymur/jp2box.py | 29 ++++---- glymur/jp2k.py | 12 ++-- glymur/lib/openjpeg.py | 124 ++++++++++++++++---------------- glymur/lib/test/test_openjp2.py | 16 ++--- glymur/test/fixtures.py | 3 +- glymur/test/test_jp2box.py | 12 ++-- glymur/version.py | 16 +++-- 9 files changed, 111 insertions(+), 108 deletions(-) diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index a7c04aa..cc8254e 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -18,7 +18,8 @@ else: # if the version is at least 2.0.0. try: import libxmp - if hasattr(libxmp, 'version') and re.match('[2-9].\d*.\d*', libxmp.version.VERSION): + if hasattr(libxmp, 'version') and re.match(r"""[2-9].\d*.\d*""", + libxmp.version.VERSION): from libxmp import XMPMeta _HAS_PYTHON_XMP_TOOLKIT = True else: diff --git a/glymur/codestream.py b/glymur/codestream.py index d01795f..67b2d56 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -663,7 +663,7 @@ class Codestream(object): bitdepth = tuple(((x & 0x7f) + 1) for x in data[0::3]) signed = tuple(((x & 0xb0) > 0) for x in data[0::3]) - + xrsiz = data[1::3] yrsiz = data[2::3] @@ -1529,7 +1529,7 @@ class SIZsegment(Segment): signed=self.signed, xyrsiz=(self.xrsiz, self.yrsiz)) return msg - + def __str__(self): msg = Segment.__str__(self) msg += '\n ' diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 8b0f6f3..85555e4 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -19,7 +19,6 @@ import os import pprint import struct import sys -import traceback import uuid import warnings import xml.etree.cElementTree as ET @@ -563,11 +562,11 @@ class CodestreamHeaderBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, box=[], length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jpch', longname='Codestream Header') self.length = length self.offset = offset - self.box = box + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.CodestreamHeaderBox(box={0})".format(self.box) @@ -625,12 +624,12 @@ class CompositingLayerHeaderBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, box=[], length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jplh', longname='Compositing Layer Header') self.length = length self.offset = offset - self.box = [] + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.CompositingLayerHeaderBox(box={0})" @@ -1073,11 +1072,11 @@ class AssociationBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, box=[], length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='asoc', longname='Association') self.length = length self.offset = offset - self.box = box + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.AssociationBox(box={0})".format(self.box) @@ -1135,11 +1134,11 @@ class JP2HeaderBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, box=[], length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jp2h', longname='JP2 Header') self.length = length self.offset = offset - self.box = box + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.JP2HeaderBox(box={0})".format(self.box) @@ -1621,11 +1620,11 @@ class ResolutionBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, box=[], length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='res ', longname='Resolution') self.length = length self.offset = offset - self.box = box + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.ResolutionBox(box={0})" @@ -2040,11 +2039,11 @@ class UUIDInfoBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, box=[], length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='uinf', longname='UUIDInfo') self.length = length self.offset = offset - self.box = box + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.UUIDInfoBox(box={0})".format(self.box) @@ -2214,8 +2213,8 @@ class UUIDBox(Jp2kBox): try: self._parse_raw_data() - except Exception as e: - warnings.warn(str(e)) + except RuntimeError as error: + warnings.warn(str(error)) def _parse_raw_data(self): """ diff --git a/glymur/jp2k.py b/glymur/jp2k.py index a337ae7..4eb8b2a 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -484,17 +484,17 @@ class Jp2k(Jp2kBox): stack.callback(opj2.image_destroy, image) _populate_image_struct(cparams, image, img_array) - + codec = opj2.create_compress(cparams.codec_fmt) stack.callback(opj2.destroy_codec, codec) - + info_handler = _INFO_CALLBACK if verbose else None opj2.set_info_handler(codec, info_handler) opj2.set_warning_handler(codec, _WARNING_CALLBACK) opj2.set_error_handler(codec, _ERROR_CALLBACK) - + opj2.setup_encoder(codec, cparams, image) - + if _OPENJP2_IS_OFFICIAL_V2: fptr = libc.fopen(self.filename, 'wb') strm = opj2.stream_create_default_file_stream(fptr, False) @@ -505,11 +505,11 @@ class Jp2k(Jp2kBox): strm = opj2.stream_create_default_file_stream_v3(self.filename, False) stack.callback(opj2.stream_destroy_v3, strm) - + opj2.start_compress(codec, image, strm) opj2.encode(codec, strm) opj2.end_compress(codec, strm) - + # Refresh the metadata. self.parse() diff --git a/glymur/lib/openjpeg.py b/glymur/lib/openjpeg.py index 5b1183f..418e9df 100644 --- a/glymur/lib/openjpeg.py +++ b/glymur/lib/openjpeg.py @@ -11,8 +11,8 @@ import numpy as np from .config import glymur_config _, OPENJPEG = glymur_config() -# Maximum number of tile parts expected by JPWL: increase at your will -JPWL_MAX_NO_TILESPECS = 16 +# Maximum number of tile parts expected by JPWL: increase at your will +JPWL_MAX_NO_TILESPECS = 16 J2K_MAXRLVLS = 33 # Number of maximum resolution level authorized PATH_LEN = 4096 # maximum allowed size for filenames @@ -58,7 +58,7 @@ class CommonStructType(ctypes.Structure): ("mj2_handle", ctypes.c_void_p)] -STREAM_READ = 0x0001 # The stream was opened for reading. +STREAM_READ = 0x0001 # The stream was opened for reading. STREAM_WRITE = 0x0002 # The stream was opened for writing. class CioType(ctypes.Structure): """Byte input-output stream (CIO) @@ -81,7 +81,7 @@ class CioType(ctypes.Structure): class CompressionInfoType(CommonStructType): - """Common fields between JPEG-2000 compression and decompression contexts. + """Common fields between JPEG-2000 compression and decompression contexts. This is for compression contexts. Corresponds to common_struct_t. """ pass @@ -91,68 +91,68 @@ class PocType(ctypes.Structure): """Progression order changes.""" _fields_ = [("resno", ctypes.c_int), # Resolution num start, Component num start, given by POC - ("compno0", ctypes.c_int), + ("compno0", ctypes.c_int), # Layer num end,Resolution num end, Component num end, given by POC - ("layno1", ctypes.c_int), - ("resno1", ctypes.c_int), - ("compno1", ctypes.c_int), + ("layno1", ctypes.c_int), + ("resno1", ctypes.c_int), + ("compno1", ctypes.c_int), - # Layer num start,Precinct num start, Precinct num end - ("layno0", ctypes.c_int), - ("precno0", ctypes.c_int), - ("precno1", ctypes.c_int), + # Layer num start,Precinct num start, Precinct num end + ("layno0", ctypes.c_int), + ("precno0", ctypes.c_int), + ("precno1", ctypes.c_int), # Progression order enum # OPJ_PROG_ORDER prg1,prg; - ("prg1", ctypes.c_int), - ("prg", ctypes.c_int), + ("prg1", ctypes.c_int), + ("prg", ctypes.c_int), - # Progression order string + # Progression order string # char progorder[5]; ("progorder", ctypes.c_char * 5), - # Tile number + # Tile number # int tile; - ("tile", ctypes.c_int), + ("tile", ctypes.c_int), # /** Start and end values for Tile width and height*/ # int tx0,tx1,ty0,ty1; - ("tx0", ctypes.c_int), - ("tx1", ctypes.c_int), - ("ty0", ctypes.c_int), - ("ty1", ctypes.c_int), + ("tx0", ctypes.c_int), + ("tx1", ctypes.c_int), + ("ty0", ctypes.c_int), + ("ty1", ctypes.c_int), # /** Start value, initialised in pi_initialise_encode*/ # int layS, resS, compS, prcS; - ("layS", ctypes.c_int), - ("resS", ctypes.c_int), - ("compS", ctypes.c_int), + ("layS", ctypes.c_int), + ("resS", ctypes.c_int), + ("compS", ctypes.c_int), ("prcS", ctypes.c_int), # /** End value, initialised in pi_initialise_encode */ # int layE, resE, compE, prcE; - ("layE", ctypes.c_int), - ("resE", ctypes.c_int), - ("compE", ctypes.c_int), - ("prcE", ctypes.c_int), + ("layE", ctypes.c_int), + ("resE", ctypes.c_int), + ("compE", ctypes.c_int), + ("prcE", ctypes.c_int), # Start and end values of Tile width and height, initialised in # pi_initialise_encode int txS,txE,tyS,tyE,dx,dy; - ("txS", ctypes.c_int), - ("txE", ctypes.c_int), - ("tyS", ctypes.c_int), - ("tyE", ctypes.c_int), - ("dx", ctypes.c_int), - ("dy", ctypes.c_int), + ("txS", ctypes.c_int), + ("txE", ctypes.c_int), + ("tyS", ctypes.c_int), + ("tyE", ctypes.c_int), + ("dx", ctypes.c_int), + ("dy", ctypes.c_int), - # Temporary values for Tile parts, initialised in pi_create_encode + # Temporary values for Tile parts, initialised in pi_create_encode # int lay_t, res_t, comp_t, prc_t,tx0_t,ty0_t; - ("lay_t", ctypes.c_int), - ("res_t", ctypes.c_int), - ("comp_t", ctypes.c_int), - ("prc_t", ctypes.c_int), - ("tx0_t", ctypes.c_int), + ("lay_t", ctypes.c_int), + ("res_t", ctypes.c_int), + ("comp_t", ctypes.c_int), + ("prc_t", ctypes.c_int), + ("tx0_t", ctypes.c_int), ("ty0_t", ctypes.c_int)] @@ -374,23 +374,23 @@ class DecompressionParametersType(ctypes.Structure): class ImageComptParmType(ctypes.Structure): """Component parameters structure used by the opj_image_create function. """ - _fields_ = [ - # XRsiz: horizontal separation of a sample of ith component with - # respect to the reference grid - ("dx", ctypes.c_int), + _fields_ = [ + # XRsiz: horizontal separation of a sample of ith component with + # respect to the reference grid + ("dx", ctypes.c_int), - # YRsiz: vertical separation of a sample of ith component with + # YRsiz: vertical separation of a sample of ith component with # respect to the reference grid */ - ("dy", ctypes.c_int), - - # data width, height - ("w", ctypes.c_int), - ("h", ctypes.c_int), + ("dy", ctypes.c_int), - # x component offset compared to the whole image - # y component offset compared to the whole image - ("x0", ctypes.c_int), - ("y0", ctypes.c_int), + # data width, height + ("w", ctypes.c_int), + ("h", ctypes.c_int), + + # x component offset compared to the whole image + # y component offset compared to the whole image + ("x0", ctypes.c_int), + ("y0", ctypes.c_int), # precision ('prec', ctypes.c_int), @@ -398,7 +398,7 @@ class ImageComptParmType(ctypes.Structure): # image depth in bits ('bpp', ctypes.c_int), - # signed (1) / unsigned (0) + # signed (1) / unsigned (0) ('sgnd', ctypes.c_int)] @@ -511,7 +511,7 @@ def destroy_compress(cinfo): def encode(cinfo, cio, image): """Wrapper for openjpeg library function opj_encode. - Encodes an image into a JPEG-2000 codestream. + Encodes an image into a JPEG-2000 codestream. Parameters ---------- @@ -540,7 +540,7 @@ def destroy_decompress(dinfo): def image_cmptparm_t_from_np(np_image): """Return appropriate image_cmptparm_t based on given numpy array. """ - try: + try: num_comps = np_image.shape[2] except IndexError: num_comps = 1 @@ -557,17 +557,17 @@ def image_cmptparm_t_from_np(np_image): bpp = 8 sgnd = 1 elif np_image.dtype == np.uint16: - prec = 16 - bpp = 16 + prec = 16 + bpp = 16 sgnd = 0 elif np_image.dtype == np.int16: - prec = 16 - bpp = 16 + prec = 16 + bpp = 16 sgnd = 1 else: raise(TypeError("unhandled")) - for j in range(0, num_comps): + for j in range(0, num_comps): tarr[j].dx = 1 tarr[j].dy = 1 tarr[j].w = np_image.shape[1] diff --git a/glymur/lib/test/test_openjp2.py b/glymur/lib/test/test_openjp2.py index 54d8254..8694507 100644 --- a/glymur/lib/test/test_openjp2.py +++ b/glymur/lib/test/test_openjp2.py @@ -1,5 +1,5 @@ """ -Tests for libopenjp2 wrapping functions. +Tests for libopenjp2 wrapping functions. """ # R0904: Seems like pylint is fooled in this situation # W0142: using kwargs is ok in this context @@ -212,7 +212,7 @@ class TestOpenJP2(unittest.TestCase): """Runs test designated tte3 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: xtx3_setup(tfile.name) - self.assertTrue(True) + self.assertTrue(True) def test_rta3(self): """Runs test designated rta3 in OpenJPEG test suite.""" @@ -221,13 +221,13 @@ class TestOpenJP2(unittest.TestCase): codec_format = openjp2.CODEC_J2K self.j2k_random_tile_access(tfile.name, codec_format) - self.assertTrue(True) + self.assertTrue(True) def test_tte4(self): """Runs test designated tte4 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: xtx4_setup(tfile.name) - self.assertTrue(True) + self.assertTrue(True) def test_rta4(self): """Runs test designated rta4 in OpenJPEG test suite.""" @@ -241,7 +241,7 @@ class TestOpenJP2(unittest.TestCase): """Runs test designated tte5 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: xtx5_setup(tfile.name) - self.assertTrue(True) + self.assertTrue(True) def test_rta5(self): """Runs test designated rta5 in OpenJPEG test suite.""" @@ -332,8 +332,8 @@ def tile_encoder(**kwargs): def tile_decoder(**kwargs): """Fixture called with various configurations by many tests. - - Reads a tile. That's all it does. + + Reads a tile. That's all it does. """ stream = openjp2.stream_create_default_file_stream_v3(kwargs['filename'], True) @@ -355,7 +355,7 @@ def tile_decoder(**kwargs): openjp2.setup_decoder(codec, dparam) image = openjp2.read_header(stream, codec) - openjp2.set_decode_area(codec, image, + openjp2.set_decode_area(codec, image, kwargs['x0'], kwargs['y0'], kwargs['x1'], kwargs['y1']) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 164f93d..6626075 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -15,7 +15,8 @@ import glymur # if the version is at least 2.0.0. try: import libxmp - if hasattr(libxmp, 'version') and re.match('[2-9].\d*.\d*', libxmp.version.VERSION): + if hasattr(libxmp, 'version') and re.match(r'''[2-9].\d*.\d*''', + libxmp.version.VERSION): from libxmp import XMPMeta HAS_PYTHON_XMP_TOOLKIT = True else: diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index b83f86d..f731ff7 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -894,9 +894,9 @@ class TestRepr(unittest.TestCase): tree = ET.ElementTree(elt) box = glymur.jp2box.XMLBox(xml=tree) - regexp = "glymur.jp2box.XMLBox" - regexp += "\(xml=<(xml.etree.ElementTree.){0,1}ElementTree object " - regexp += "at 0x([a-f0-9]*)>\)" + regexp = r"""glymur.jp2box.XMLBox""" + regexp += r"""\(xml=<(xml.etree.ElementTree.){0,1}ElementTree object """ + regexp += """at 0x([a-f0-9]*)>\)""" if sys.hexversion < 0x03000000: self.assertRegexpMatches(repr(box), regexp) @@ -927,9 +927,9 @@ class TestRepr(unittest.TestCase): # 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 = "glymur.jp2box.UUIDBox\(" - regexp += "the_uuid=UUID\('00000000-0000-0000-0000-000000000000'\),\s" - regexp += "raw_data=\)" + regexp = r"""glymur.jp2box.UUIDBox\(""" + regexp += """the_uuid=UUID\('00000000-0000-0000-0000-000000000000'\),\s""" + regexp += """raw_data=\)""" if sys.hexversion < 0x03000000: self.assertRegexpMatches(repr(box), regexp) diff --git a/glymur/version.py b/glymur/version.py index af7523e..260f880 100644 --- a/glymur/version.py +++ b/glymur/version.py @@ -1,10 +1,12 @@ -# This file is part of glymur, a Python interface for accessing JPEG 2000. -# -# http://glymur.readthedocs.org -# -# Copyright 2013 John Evans -# -# License: MIT +""" +This file is part of glymur, a Python interface for accessing JPEG 2000. + +http://glymur.readthedocs.org + +Copyright 2013 John Evans + +License: MIT +""" import sys import numpy as np From 77b58c1c5f956666816d9b40de746442c102c765 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 25 Jan 2014 15:36:55 -0500 Subject: [PATCH 15/26] No explicit dependence on libxmp anymore. Back to ElementTree. --- glymur/_uuid_io/Exif.py | 14 ++++++++++++++ glymur/_uuid_io/__init__.py | 2 +- glymur/jp2box.py | 28 ++++++++++++---------------- glymur/test/test_jp2box_uuid.py | 7 ++++--- glymur/test/test_jp2k.py | 5 ++++- glymur/test/test_printing.py | 12 ++++-------- 6 files changed, 39 insertions(+), 29 deletions(-) diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py index c1b30f0..967468d 100644 --- a/glymur/_uuid_io/Exif.py +++ b/glymur/_uuid_io/Exif.py @@ -6,6 +6,7 @@ import pprint import struct import sys import warnings +import xml.etree.cElementTree as ET if sys.hexversion < 0x02070000: # pylint: disable=F0401,E0611 @@ -13,8 +14,21 @@ if sys.hexversion < 0x02070000: else: from collections import OrderedDict +def xml(raw_data): + """ + XMP data to be parsed as XML. + """ + if sys.hexversion < 0x03000000: + elt = ET.fromstring(raw_data) + else: + text = raw_data.decode('utf-8') + elt = ET.fromstring(text) + + return ET.ElementTree(elt) + def tiff_header(read_buffer): """ + Interpret the uuid raw data as a tiff header. """ # Ignore the first six bytes. # Next 8 should be (73, 73, 42, 8) or (77, 77, 42, 8) diff --git a/glymur/_uuid_io/__init__.py b/glymur/_uuid_io/__init__.py index d12d20c..778f912 100644 --- a/glymur/_uuid_io/__init__.py +++ b/glymur/_uuid_io/__init__.py @@ -1,4 +1,4 @@ """ Sub package for handling various types of UUIDs. """ -from .Exif import tiff_header +from .Exif import tiff_header, xml diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 300ac12..da8e1c9 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -33,12 +33,6 @@ else: import numpy as np -try: - from libxmp import XMPMeta - _HAS_PYTHON_XMP_TOOLKIT = True -except ImportError: - _HAS_PYTHON_XMP_TOOLKIT = False - from .codestream import Codestream from .core import _COLORSPACE_MAP_DISPLAY from .core import _COLOR_TYPE_MAP_DISPLAY @@ -2220,18 +2214,15 @@ class UUIDBox(Jp2kBox): try: self._parse_raw_data() - except Exception as e: - warnings.warn(str(e)) + except RuntimeError as error: + warnings.warn(str(error)) def _parse_raw_data(self): """ Private function for parsing UUID payloads if possible. """ if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - xmp = XMPMeta() - xmp.parse_from_str(self.raw_data.decode('utf-8'), - xmpmeta_wrap=False) - self.data = xmp + self.data = _uuid_io.xml(self.raw_data) elif self.uuid.bytes == b'JpgTiffExif->JP2': self.data = _uuid_io.tiff_header(self.raw_data) else: @@ -2243,11 +2234,16 @@ class UUIDBox(Jp2kBox): return msg.format(repr(self.uuid), len(self.data)) def __str__(self): - msg = '{0}\n' - msg += ' UUID: {1}\n' - msg += ' UUID Data: {2}' + msg = '{0}\n UUID: {1}'.format(Jp2kBox.__str__(self), self.uuid) - msg = msg.format(Jp2kBox.__str__(self), self.uuid, str(self.data)) + if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + line = ' (XMP)\n UUID Data: {0}' + msg += line.format(_pretty_print_xml(self.data)) + elif self.uuid.bytes == b'JpgTiffExif->JP2': + msg += ' (EXIF)\n UUID Data: {0}'.format(str(self.data)) + else: + line = ' (unknown)\n UUID Data: {0} bytes' + msg += line.format(len(self.raw_data)) return msg diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py index 8f24f74..0d3684a 100644 --- a/glymur/test/test_jp2box_uuid.py +++ b/glymur/test/test_jp2box_uuid.py @@ -18,7 +18,7 @@ import sys import tempfile import uuid import warnings -from xml.etree import cElementTree as ET +import xml.etree if sys.hexversion < 0x02070000: import unittest2 as unittest @@ -68,7 +68,8 @@ class TestUUIDXMP(unittest.TestCase): # The data should be an XMP packet, which gets interpreted as # an ElementTree. - self.assertTrue(isinstance(jp2.box[-1].data, XMPMeta)) + self.assertTrue(isinstance(jp2.box[-1].data, + xml.etree.ElementTree.ElementTree)) class TestUUIDExif(unittest.TestCase): """Tests for UUIDs of Exif type.""" @@ -184,7 +185,7 @@ class TestUUIDExif(unittest.TestCase): tfile.flush() jp2 = glymur.Jp2k(tfile.name) - self.assertEqual(jp2.box[-1].data.ifds['Image']['Make'], "HTC") + self.assertEqual(jp2.box[-1].data['Make'], "HTC") if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index c055aba..aeb0e41 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -31,6 +31,7 @@ import numpy as np import pkg_resources import libxmp +from libxmp import XMPMeta import glymur from glymur import Jp2k @@ -364,7 +365,9 @@ class TestJp2k(unittest.TestCase): def test_xmp_attribute(self): """Verify the XMP packet in the shipping example file can be read.""" j = Jp2k(self.jp2file) - xmp = j.box[3].data + xmp = XMPMeta() + xmp.parse_from_str(j.box[3].raw_data.decode('utf-8'), + xmpmeta_wrap=False) creator_tool = xmp.get_property(libxmp.consts.XMP_NS_XMP, 'CreatorTool') self.assertEqual(creator_tool, 'Google') diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index a1f0c55..382e28d 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -1057,6 +1057,8 @@ class TestPrinting(unittest.TestCase): tfile.write(struct.pack(' Date: Sat, 25 Jan 2014 15:55:36 -0500 Subject: [PATCH 16/26] Fixing tests broken by bad merge. --- glymur/test/fixtures.py | 174 +++++++++++++++++------------------ glymur/test/test_printing.py | 1 - 2 files changed, 84 insertions(+), 91 deletions(-) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 6626075..e11709b 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -183,96 +183,90 @@ def read_pgx_header(pgx_file): nemo_xmp_box = """UUID Box (uuid) @ (77, 3146) UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) - UUID Data: - - - - Google - 2013-02-09T14:47:53 - - - 1 - 72/1 - 72/1 - 2 - HTC - HTC Glacier - 2592 - 1456 - - - 8 - 8 - 8 - - - 2 - 3 - - - 1343036288/4294967295 - 1413044224/4294967295 - - - - - 2748779008/4294967295 - 1417339264/4294967295 - 1288490240/4294967295 - 2576980480/4294967295 - 644245120/4294967295 - 257698032/4294967295 - - - - - 1 - 2528 - 1424 - 353/100 - 0 - 0/1 - WGS-84 - 2013-02-09T14:47:53 - - - 76 - - - 0220 - 0100 - - - 1 - 2 - 3 - 0 - - - 42,20.56N - 71,5.29W - 2013-02-09T19:47:53Z - NETWORK - - - 2013-02-09T14:47:53 - - - - - Glymur - Python XMP Toolkit - - - - - -""" + UUID Data: + + + + Google + 2013-02-09T14:47:53 + + + 1 + 72/1 + 72/1 + 2 + HTC + HTC Glacier + 2592 + 1456 + + + 8 + 8 + 8 + + + 2 + 3 + + + 1343036288/4294967295 + 1413044224/4294967295 + + + + + 2748779008/4294967295 + 1417339264/4294967295 + 1288490240/4294967295 + 2576980480/4294967295 + 644245120/4294967295 + 257698032/4294967295 + + + + + 1 + 2528 + 1424 + 353/100 + 0 + 0/1 + WGS-84 + 2013-02-09T14:47:53 + + + 76 + + + 0220 + 0100 + + + 1 + 2 + 3 + 0 + + + 42,20.56N + 71,5.29W + 2013-02-09T19:47:53Z + NETWORK + + + 2013-02-09T14:47:53 + + + + + Glymur + Python XMP Toolkit + + + + + """ SimpleRDF = """ Date: Sat, 25 Jan 2014 16:04:57 -0500 Subject: [PATCH 17/26] Removed restriction on only writing XMP UUIDs. #104 --- glymur/jp2box.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index b3fcf89..6dbec1d 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -2249,9 +2249,6 @@ class UUIDBox(Jp2kBox): def write(self, fptr): """Write a UUID box to file. """ - if self.uuid != uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - msg = "Only XMP UUID boxes can currently be written." - raise NotImplementedError(msg) write_buffer = struct.pack('>I4s', self.length, b'uuid') fptr.write(write_buffer) fptr.write(self.uuid.bytes) From d6a8736aea1c8ed2469ba10235701fb5935f6f47 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 25 Jan 2014 17:07:33 -0500 Subject: [PATCH 18/26] Removed shutil debugging statement. --- glymur/test/test_printing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 2828c96..b6e3484 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -1056,8 +1056,6 @@ class TestPrinting(unittest.TestCase): tfile.write(struct.pack(' Date: Sat, 25 Jan 2014 17:08:53 -0500 Subject: [PATCH 19/26] Removed python-xmp-toolkit requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b4fdc2f..b780e4d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ kwargs = {'name': 'Glymur', 'license': 'MIT', 'test_suite': 'glymur.test'} -instllrqrs = ['numpy>=1.4.1', 'python-xmp-toolkit>=2.0.0'] +instllrqrs = ['numpy>=1.4.1'] if sys.hexversion < 0x03030000: instllrqrs.append('contextlib2>=0.4') instllrqrs.append('mock>=1.0.1') From 3efd4169d947ec817a0a997d7fb06413012eed78 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 25 Jan 2014 18:14:31 -0500 Subject: [PATCH 20/26] Added fixtures for ICC profiles for 27, 33, and 34. --- glymur/test/fixtures.py | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index e11709b..ff33237 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -283,3 +283,70 @@ SimpleRDF = """ """ + +text_gbr_27 = """Colour Specification Box (colr) @ (179, 1339) + Method: any ICC profile + Precedence: 2 + Approximation: accurately represents correct colorspace definition + ICC Profile: + {'Color Space': 'RGB', + 'Connection Space': 'XYZ', + 'Creator': u'appl', + 'Datetime': datetime.datetime(2009, 2, 25, 11, 26, 11), + 'Device Attributes': 'reflective, glossy, positive media polarity, color media', + 'Device Class': 'display device profile', + 'Device Manufacturer': u'appl', + 'Device Model': '', + 'File Signature': u'acsp', + 'Flags': 'not embedded, can be used independently', + 'Illuminant': array([ 0.96420288, 1. , 0.8249054 ]), + 'Platform': u'APPL', + 'Preferred CMM Type': 1634758764, + 'Rendering Intent': 'perceptual', + 'Size': 1328, + 'Version': '2.2.0'}""" + +text_gbr_33 = """Colour Specification Box (colr) @ (179, 1339) + Method: any ICC profile + Precedence: 2 + Approximation: accurately represents correct colorspace definition + ICC Profile: + {'Size': 1328, + 'Preferred CMM Type': 1634758764, + 'Version': '2.2.0', + 'Device Class': 'display device profile', + 'Color Space': 'RGB', + 'Connection Space': 'XYZ', + 'Datetime': datetime.datetime(2009, 2, 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': array([ 0.96420288, 1. , 0.8249054 ]), + 'Creator': 'appl'}""" + +text_gbr_34 = """Colour Specification Box (colr) @ (179, 1339) + Method: any ICC profile + Precedence: 2 + Approximation: accurately represents correct colorspace definition + ICC Profile: + {'Size': 1328, + 'Preferred CMM Type': 1634758764, + 'Version': '2.2.0', + 'Device Class': 'display device profile', + 'Color Space': 'RGB', + 'Connection Space': 'XYZ', + 'Datetime': datetime.datetime(2009, 2, 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': array([ 0.96420288, 1. , 0.8249054 ]), + 'Creator': 'appl'}""" From 8617ce04421e5f44da83b3f43c2c97dea1af6b49 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 25 Jan 2014 18:16:54 -0500 Subject: [PATCH 21/26] Using fixtures for expected values. Removed one duplicated test. #104 --- glymur/test/test_printing.py | 109 +++-------------------------------- 1 file changed, 9 insertions(+), 100 deletions(-) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index b6e3484..892e283 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -36,6 +36,7 @@ else: import glymur from glymur import Jp2k from .fixtures import OPJ_DATA_ROOT, opj_data_file, nemo_xmp_box +from .fixtures import text_gbr_27, text_gbr_33, text_gbr_34 @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") @@ -284,75 +285,6 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(OPJ_DATA_ROOT is None, - "OPJ_DATA_ROOT environment variable not set") - def test_icc_profile(self): - """verify printing of colr box with ICC profile""" - filename = opj_data_file('input/nonregression/text_GBR.jp2') - with warnings.catch_warnings(): - # brand is 'jp2 ', but has any icc profile. - warnings.simplefilter("ignore") - jp2 = Jp2k(filename) - with patch('sys.stdout', new=StringIO()) as fake_out: - print(jp2.box[3].box[1]) - actual = fake_out.getvalue().strip() - lin27 = ["Colour Specification Box (colr) @ (179, 1339)", - " Method: any ICC profile", - " Precedence: 2", - " Approximation: accurately represents correct " - + "colorspace definition", - " ICC Profile:", - " {'Color Space': 'RGB',", - " 'Connection Space': 'XYZ',", - " 'Creator': u'appl',", - " 'Datetime': " - + "datetime.datetime(2009, 2, 25, 11, 26, 11),", - " 'Device Attributes': 'reflective, glossy, " - + "positive media polarity, color media',", - " 'Device Class': 'display device profile',", - " 'Device Manufacturer': u'appl',", - " 'Device Model': '',", - " 'File Signature': u'acsp',", - " 'Flags': " - + "'not embedded, can be used independently',", - " 'Illuminant': " - + "array([ 0.96420288, 1. , 0.8249054 ]),", - " 'Platform': u'APPL',", - " 'Preferred CMM Type': 1634758764,", - " 'Rendering Intent': 'perceptual',", - " 'Size': 1328,", - " 'Version': '2.2.0'}"] - lin33 = ["Colour Specification Box (colr) @ (179, 1339)", - " Method: any ICC profile", - " Precedence: 2", - " Approximation: accurately represents correct " - + "colorspace definition", - " ICC Profile:", - " {'Size': 1328,", - " 'Preferred CMM Type': 1634758764,", - " 'Version': '2.2.0',", - " 'Device Class': 'display device profile',", - " 'Color Space': 'RGB',", - " 'Connection Space': 'XYZ',", - " 'Datetime': " - + "datetime.datetime(2009, 2, 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': " - + "array([ 0.96420288, 1. , 0.8249054 ]),", - " 'Creator': 'appl'}"] - - lines = lin27 if sys.hexversion < 0x03000000 else lin33 - expected = '\n'.join(lines) - self.assertEqual(actual, expected) - @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_crg(self): @@ -963,12 +895,10 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(sys.hexversion < 0x03000000, - "Ordered dicts not printing well in 2.7") @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_jpx_approx_icc_profile(self): - """verify jpx with approx field equal to zero""" + def test_icc_profile(self): + """verify icc profile printing with a jpx""" # ICC profiles may be used in JP2, but the approximation field should # be zero unless we have jpx. This file does both. filename = opj_data_file('input/nonregression/text_GBR.jp2') @@ -980,34 +910,13 @@ class TestPrinting(unittest.TestCase): with patch('sys.stdout', new=StringIO()) as fake_out: print(jp2.box[3].box[1]) actual = fake_out.getvalue().strip() - lines = ["Colour Specification Box (colr) @ (179, 1339)", - " Method: any ICC profile", - " Precedence: 2", - " Approximation: accurately represents " - + "correct colorspace definition", - " ICC Profile:", - " {'Size': 1328,", - " 'Preferred CMM Type': 1634758764,", - " 'Version': '2.2.0',", - " 'Device Class': 'display device profile',", - " 'Color Space': 'RGB',", - " 'Connection Space': 'XYZ',", - " 'Datetime': " - + "datetime.datetime(2009, 2, 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': array([ 0.96420288, 1. ," - + " 0.8249054 ]),", - " 'Creator': 'appl'}"] + if sys.hexversion < 0x03000000: + expected = text_gbr_27 + elif sys.hexversion < 0x03040000: + expected = text_gbr_33 + else: + expected = text_gbr_34 - expected = '\n'.join(lines) self.assertEqual(actual, expected) @unittest.skipIf(OPJ_DATA_ROOT is None, From 052cadbf8e1900009a322f4c6cab56e3f548fdcb Mon Sep 17 00:00:00 2001 From: John Evans Date: Mon, 27 Jan 2014 16:48:40 -0500 Subject: [PATCH 22/26] No longer presenting ElementTree as viable for XMP in docs. #104 --- docs/source/how_do_i.rst | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 443e8db..0fb4d9d 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -236,28 +236,21 @@ The example JP2 file shipped with glymur has an XMP UUID. :: xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="Exempi + XMP Core 5.1.2"> + + Google + 2013-02-09T14:47:53 + + . . . Since the UUID data in this case is returned as an ElementTree instance, -one can use ElementTree from the standard library to access the data. -For example, to extract the **CreatorTool** attribute value, the following -would work:: - - >>> xmp = j.box[3].data.packet - >>> rdf = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' - >>> ns2 = '{http://ns.adobe.com/xap/1.0/}' - >>> name = '{0}RDF/{0}Description/{1}CreatorTool'.format(rdf, ns2) - >>> elt = xmp.find(name) - >>> elt - - >>> elt.text - 'Google' - -Yes, that's painful. A better solution is to install the Python XMP Toolkit -(developer branch):: +one might first turn to ElementTree from the standard library. There's +a better solution though, particularly if you need to create XMP, and that +is to Use the Python XMP Toolkit instead (make sure you use version 2.0 and +not 1.0.2).:: >>> from libxmp import XMPMeta >>> from libxmp.consts import XMP_NS_XMP as NS_XAP From f214459ef7fe0ab34686e0dd5467d2910c731cda Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 30 Jan 2014 18:48:56 -0500 Subject: [PATCH 23/26] Starting to fill out python xmp toolkit advocacy. #104 --- docs/source/how_do_i.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 443e8db..e620dbc 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -243,8 +243,8 @@ The example JP2 file shipped with glymur has an XMP UUID. :: Since the UUID data in this case is returned as an ElementTree instance, one can use ElementTree from the standard library to access the data. -For example, to extract the **CreatorTool** attribute value, the following -would work:: +For example, to extract the **CreatorTool** attribute value, one could do the +following >>> xmp = j.box[3].data.packet >>> rdf = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' @@ -256,8 +256,8 @@ would work:: >>> elt.text 'Google' -Yes, that's painful. A better solution is to install the Python XMP Toolkit -(developer branch):: +But that would be painful. A better solution is to install the Python XMP +Toolkit:: >>> from libxmp import XMPMeta >>> from libxmp.consts import XMP_NS_XMP as NS_XAP @@ -266,3 +266,16 @@ Yes, that's painful. A better solution is to install the Python XMP Toolkit >>> meta.get_property(NS_XAP, 'CreatorTool') 'Google' +Where the Python XMP Toolkit can really shine, though, is when you are +converting an image from another format such as TIFF or JPEG into JPEG 2000. +For example:: + + >>> import requests + >>> r = requests.get('http://photojournal.jpl.nasa.gov/tiff/PIA17145.tif') + >>> with open('PIA17145.tif', 'wb') as fptr: fptr.write(r.content) + >>> from libxmp import XMPFiles + >>> xf = XMPFile() + >>> xf.open_file('PIA17145.tif') + >>> xmp = xf.get_xmp() + + From 6e9846ca9137e7625aaf09f09ae4f0dd45e32587 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 30 Jan 2014 18:49:48 -0500 Subject: [PATCH 24/26] Fixed repr for XMP uuids. --- glymur/jp2box.py | 2 +- glymur/test/test_jp2box.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 6dbec1d..122d5c4 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -2230,7 +2230,7 @@ class UUIDBox(Jp2kBox): def __repr__(self): msg = "glymur.jp2box.UUIDBox(the_uuid={0}, " msg += "raw_data=)" - return msg.format(repr(self.uuid), len(self.data)) + return msg.format(repr(self.uuid), len(self.raw_data)) def __str__(self): msg = '{0}\n UUID: {1}'.format(Jp2kBox.__str__(self), self.uuid) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index f731ff7..a40a5c6 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -919,7 +919,7 @@ class TestRepr(unittest.TestCase): self.assertEqual(box.vendor_mask, newbox.vendor_mask) @unittest.skipIf(sys.hexversion < 0x02070000, "Requires 2.7+") - def test_uuid_box(self): + def test_uuid_box_generic(self): """Verify uuid repr method.""" uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') data = b'0123456789' @@ -936,6 +936,24 @@ class TestRepr(unittest.TestCase): else: self.assertRegex(repr(box), regexp) + @unittest.skipIf(sys.hexversion < 0x02070000, "Requires 2.7+") + def test_uuid_box_xmp(self): + """Verify uuid repr method for XMP UUID box.""" + jp2file = glymur.data.nemo() + j = Jp2k(jp2file) + box = j.box[3] + + # 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=UUID\('be7acfcb-97a9-42e8-9c71-999491e3afac'\),\s""" + regexp += """raw_data=\)""" + + if sys.hexversion < 0x03000000: + self.assertRegexpMatches(repr(box), regexp) + else: + self.assertRegex(repr(box), regexp) + @unittest.skipIf(sys.hexversion < 0x02070000, "Requires 2.7+") def test_contiguous_codestream_box(self): """Verify contiguous codestream box repr method.""" From f3fca7d7d328d3e9e74911623ddd9de7872cdac0 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 1 Feb 2014 13:21:51 -0500 Subject: [PATCH 25/26] More xmp blurbs. --- docs/source/how_do_i.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index e620dbc..3aaa300 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -268,14 +268,31 @@ Toolkit:: Where the Python XMP Toolkit can really shine, though, is when you are converting an image from another format such as TIFF or JPEG into JPEG 2000. -For example:: +For example, if you were to be converting the TIFF image found at +http://photojournal.jpl.nasa.gov/tiff/PIA17145.tif info JPEG 2000:: + + >>> import skimage.io + >>> image = skimage.io.imread('PIA17145.tif') + >>> from glymur import Jp2k + >>> jp2 = Jp2k('PIA17145.jp2', 'wb') + >>> jp2.write(image) + +Next you can extract the XMP metadata. - >>> import requests - >>> r = requests.get('http://photojournal.jpl.nasa.gov/tiff/PIA17145.tif') - >>> with open('PIA17145.tif', 'wb') as fptr: fptr.write(r.content) >>> from libxmp import XMPFiles >>> xf = XMPFile() >>> xf.open_file('PIA17145.tif') >>> xmp = xf.get_xmp() +If you are familiar with TIFF, you can verify that there's no XMP tag in the +TIFF file, but the Python XMP Toolkit takes advantage of the TIFF header +structure to populate an XMP packet for you. If you were working with a JPEG +file with Exif metadata, that information would be included in the XMP packet +as well. Now you can append the XMP packet in a UUIDBox:: + + >>> import uuid + >>> the_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac') + >>> box = glymur.jp2box.UUIDBox(uuid, str(xmp).encode()) + >>> jp2.append(box) + From 321a2544646495a159e45605f12769c6941c935d Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 8 Feb 2014 19:52:39 -0500 Subject: [PATCH 26/26] Completed XMP description. --- docs/source/how_do_i.rst | 72 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index b0b476c..c1a904c 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -262,7 +262,7 @@ following 'Google' But that would be painful. A better solution is to install the Python XMP -Toolkit:: +Toolkit (make sure it is version 2.0):: >>> from libxmp import XMPMeta >>> from libxmp.consts import XMP_NS_XMP as NS_XAP @@ -285,19 +285,77 @@ http://photojournal.jpl.nasa.gov/tiff/PIA17145.tif info JPEG 2000:: Next you can extract the XMP metadata. >>> from libxmp import XMPFiles - >>> xf = XMPFile() + >>> xf = XMPFiles() >>> xf.open_file('PIA17145.tif') >>> xmp = xf.get_xmp() + >>> print(xmp) + + + + + 1016 + 1016 + + + 8 + + + 1 + 1 + 1 + 1 + 2 + + + + + converted PNM file + + + + + + If you are familiar with TIFF, you can verify that there's no XMP tag in the TIFF file, but the Python XMP Toolkit takes advantage of the TIFF header structure to populate an XMP packet for you. If you were working with a JPEG file with Exif metadata, that information would be included in the XMP packet -as well. Now you can append the XMP packet in a UUIDBox:: +as well. Now you can append the XMP packet in a UUIDBox. In order to do this, +though, you have to know the UUID that signifies XMP data.:: >>> import uuid - >>> the_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac') - >>> box = glymur.jp2box.UUIDBox(uuid, str(xmp).encode()) + >>> xmp_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac') + >>> box = glymur.jp2box.UUIDBox(xmp_uuid, str(xmp).encode()) >>> jp2.append(box) - - + >>> print(jp2.box[-1]) + UUID Box (uuid) @ (592316, 1053) + UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) + UUID Data: + + + + 1016 + 1016 + + + 8 + + + 1 + 1 + 1 + 1 + 2 + + + + + converted PNM file + + + + +