Added Exif support. Closes #11.

This commit is contained in:
jevans 2013-06-05 20:42:42 -04:00
commit 949671aa12
11 changed files with 400 additions and 49 deletions

View file

@ -1,3 +1,5 @@
Jun 05, 2013 - v0.1.4 Added Exif UUID read support.
Jun 02, 2013 - v0.1.3p1 Raising IOErrors when code block size and precinct Jun 02, 2013 - v0.1.3p1 Raising IOErrors when code block size and precinct
sizes are not in harmony. Added statement to docs about upstream library sizes are not in harmony. Added statement to docs about upstream library
dependence. Added roadmap to docs. dependence. Added roadmap to docs.

View file

@ -1,5 +1,6 @@
---
API API
=== ---
Jp2k Jp2k
---- ----

View file

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

View file

@ -1,6 +1,6 @@
************ ------------
How do I...? How do I...?
************ ------------
Get the code? Get the code?
============= =============
@ -9,7 +9,7 @@ Go to http://github.com/quintusdias/glymur
Display metadata? Display metadata?
================= =================
There are two ways. From the unix command line, the script **jp2dump** is There are two ways. From the unix command line, the script *jp2dump* is
available. :: available. ::
$ jp2dump /path/to/glymur/installation/data/nemo.jp2 $ jp2dump /path/to/glymur/installation/data/nemo.jp2
@ -22,8 +22,8 @@ From within Python, it is as simple as printing the Jp2k object, i.e. ::
>>> j = Jp2k(file) >>> j = Jp2k(file)
>>> print(j) >>> print(j)
The primary emphasis is on JP2 metadata, but it is possible to This prints the metadata found in the JP2 boxes, but in the case of the
display just raw codestream as well. This will display metadata present in the codestream box, only the main header is printed. It is possible to print
codestream's main header only. :: **only** the codestream information as well, i.e. ::
>>> print(j.get_codestream()) >>> print(j.get_codestream())

View file

@ -3,8 +3,9 @@
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
==================================
Welcome to glymur's documentation! Welcome to glymur's documentation!
=================================== ==================================
Contents: Contents:
@ -16,9 +17,9 @@ Contents:
how_do_i how_do_i
roadmap roadmap
------------------
Indices and tables Indices and tables
================== ------------------
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`

View file

@ -1,6 +1,6 @@
========================================= ----------------------------------------
Glymur: a Python interface for JPEG 2000 Glymur: a Python interface for JPEG 2000
========================================= ----------------------------------------
**Glymur** contains a Python interface to the OpenJPEG library **Glymur** contains a Python interface to the OpenJPEG library
which allows linux and mac users to read and write JPEG 2000 files. For more which allows linux and mac users to read and write JPEG 2000 files. For more
@ -13,15 +13,15 @@ Glymur supports both reading and writing of JPEG 2000 images (part 1). Writing
JPEG 2000 images is currently limited to images that can fit in memory, JPEG 2000 images is currently limited to images that can fit in memory,
however. however.
Of particular focus is retrieval of metadata. Reading XMP UUID Of particular focus is retrieval of metadata. Reading Exif UUIDs is supported,
boxes is supported, as the data block consists of XML. There is 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, some very limited support for reading JPX metadata. For instance,
**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can **asoc** and **labl** boxes are recognized, so GMLJP2 metadata can
be retrieved from such JPX files. be retrieved from such JPX files.
------------ ''''''''''''
Requirements Requirements
------------ ''''''''''''
glymur works on Python 2.7 and 3.3. Python 3.3 is strongly recommended. glymur works on Python 2.7 and 3.3. Python 3.3 is strongly recommended.
OpenJPEG OpenJPEG
@ -132,9 +132,9 @@ Windows
------- -------
Not currently supported. Not currently supported.
------------------------------------ ''''''''''''''''''''''''''''''''''''
Installation, Testing, Configuration Installation, Testing, Configuration
------------------------------------ ''''''''''''''''''''''''''''''''''''
From this point forward, python3 will be referred to as just "python". From this point forward, python3 will be referred to as just "python".

View file

@ -1,9 +1,10 @@
-------
Roadmap Roadmap
======= -------
Here's an incomplete list of what I'd like to focus on in the near future. Here's an incomplete list of what I'd like to focus on in the near future.
* continue to monitor upstream changes in the **openjp2** library * continue to monitor upstream changes in the **openjp2** library
* add read support or Exif UUIDs
* investigate using CFFI or cython instead of ctypes to wrap **openjp2** * investigate using CFFI or cython instead of ctypes to wrap **openjp2**
* add read support for ICC profiles

View file

@ -14,6 +14,7 @@ References
import copy import copy
import math import math
import os import os
import pprint
import struct import struct
import sys import sys
import uuid import uuid
@ -1570,6 +1571,9 @@ class UUIDBox(Jp2kBox):
if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
uuid_type = ' (XMP)' uuid_type = ' (XMP)'
uuid_data = _pretty_print_xml(self.data) uuid_data = _pretty_print_xml(self.data)
elif self.uuid.bytes == b'JpgTiffExif->JP2':
uuid_type = ' (Exif)'
uuid_data = '\n' + pprint.pformat(self.data)
else: else:
uuid_type = '' uuid_type = ''
uuid_data = '{0} bytes'.format(len(self.data)) uuid_data = '{0} bytes'.format(len(self.data))
@ -1610,7 +1614,7 @@ class UUIDBox(Jp2kBox):
n = offset + length - f.tell() n = offset + length - f.tell()
buffer = f.read(n) buffer = f.read(n)
if kwargs['uuid'] == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): if kwargs['uuid'] == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
# XMP data. Parse as XML. Seems to be a difference between # XMP data. Parse as XML. Seems to be a difference between
# ElementTree in version 2.7 and 3.3. # ElementTree in version 2.7 and 3.3.
if sys.hexversion < 0x03000000: if sys.hexversion < 0x03000000:
parser = ET.XMLParser(encoding='utf-8') parser = ET.XMLParser(encoding='utf-8')
@ -1619,18 +1623,25 @@ class UUIDBox(Jp2kBox):
text = buffer.decode('utf-8') text = buffer.decode('utf-8')
kwargs['data'] = ET.fromstring(text) kwargs['data'] = ET.fromstring(text)
elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2':
kwargs['data'] = Exif(buffer).exif_image e = Exif(buffer)
d = {}
d['Exif'] = e.exif_image
d['Photo'] = e.exif_photo
d['GPSInfo'] = e.exif_gpsinfo
d['Iop'] = e.exif_iop
kwargs['data'] = d
else: else:
kwargs['data'] = buffer kwargs['data'] = buffer
box = UUIDBox(**kwargs) box = UUIDBox(**kwargs)
return box return box
class Exif: class Exif:
""" """
Attributes Attributes
---------- ----------
buffer : bytes buffer : bytes
Raw byte stream consisting of the UUID data. Raw byte stream consisting of the UUID data.
endian : str endian : str
Either '<' for big-endian, or '>' for little-endian. Either '<' for big-endian, or '>' for little-endian.
""" """
@ -1657,7 +1668,7 @@ class Exif:
offset = data[3] offset = data[3]
# This is the 'Exif Image' portion. # This is the 'Exif Image' portion.
exif = ExifImageIfd(self.endian, buffer[6:], offset) exif = ExifImageIfd(self.endian, buffer[6:], offset)
self.exif_image = exif.ifd self.exif_image = exif.ifd
if 'ExifTag' in self.exif_image.keys(): if 'ExifTag' in self.exif_image.keys():
@ -1677,12 +1688,13 @@ class Exif:
gps = ExifGPSInfoIfd(self.endian, buffer[6:], offset) gps = ExifGPSInfoIfd(self.endian, buffer[6:], offset)
self.exif_gpsinfo = gps.ifd self.exif_gpsinfo = gps.ifd
class Ifd: class Ifd:
""" """
Attributes Attributes
---------- ----------
buffer : bytes buffer : bytes
Raw byte stream consisting of the UUID data. Raw byte stream consisting of the UUID data.
datatype2fmt : dictionary datatype2fmt : dictionary
Class attribute, maps the TIFF enumerated datatype to the python Class attribute, maps the TIFF enumerated datatype to the python
datatype and data width. datatype and data width.
@ -1706,36 +1718,45 @@ class Ifd:
self.endian = endian self.endian = endian
self.buffer = buffer self.buffer = buffer
self.num_tags, = struct.unpack(endian + 'H', buffer[offset:offset + 2]) self.num_tags, = struct.unpack(endian + 'H',
buffer[offset:offset + 2])
fmt = self.endian + 'HHII' * self.num_tags fmt = self.endian + 'HHII' * self.num_tags
data = struct.unpack(fmt, buffer[offset + 2:offset + 2 + self.num_tags * 12]) ifd_buffer = buffer[offset + 2:offset + 2 + self.num_tags * 12]
data = struct.unpack(fmt, ifd_buffer)
self.raw_ifd = {} self.raw_ifd = {}
for j, tag in enumerate(data[0::4]): for j, tag in enumerate(data[0::4]):
tag_entry = buffer[offset + 2 + j * 12 + 8:offset + 2 + j * 12 + 8 + 4] # The offset to the tag offset/payload is the offset to the IFD
payload = self.parse_tag(data[j * 4 + 1], # plus 2 bytes for the number of tags plus 12 bytes for each
data[j * 4 + 2], # tag entry plus 8 bytes to the offset/payload itself.
tag_entry) toffp = buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4]
self.raw_ifd[tag] = payload 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): def parse_tag(self, dtype, count, offset_buf):
"""Interpret an Exif image tag data payload. """Interpret an Exif image tag data payload.
""" """
fmt = self.datatype2fmt[dtype][0] * count fmt = self.datatype2fmt[dtype][0] * count
payload_size = self.datatype2fmt[dtype][1] * count payload_size = self.datatype2fmt[dtype][1] * count
if payload_size <= 4: if payload_size <= 4:
# Interpret the payload from the 4 bytes in the tag entry. # Interpret the payload from the 4 bytes in the tag entry.
target_buffer = offset_buf[:payload_size] target_buffer = offset_buf[:payload_size]
else: else:
# Interpret the payload at the offset specified by the 4 bytes in the # Interpret the payload at the offset specified by the 4 bytes in
# tag entry. # the tag entry.
offset, = struct.unpack(self.endian + 'I', offset_buf) offset, = struct.unpack(self.endian + 'I', offset_buf)
target_buffer = self.buffer[offset:offset + payload_size] target_buffer = self.buffer[offset:offset + payload_size]
if dtype == 2: if dtype == 2:
# ASCII # ASCII
payload = target_buffer.decode('utf-8').rstrip() if sys.hexversion < 0x03000000:
payload = target_buffer.rstrip('\x00')
else:
payload = target_buffer.decode('utf-8').rstrip('\x00')
else: else:
payload = struct.unpack(self.endian + fmt, target_buffer) payload = struct.unpack(self.endian + fmt, target_buffer)
if dtype == 5 or dtype == 10: if dtype == 5 or dtype == 10:
@ -1746,22 +1767,226 @@ class Ifd:
rational_payload.append(value) rational_payload.append(value)
payload = rational_payload payload = rational_payload
if count == 1: if count == 1:
# If just a single value, then return a scalar instead of a # If just a single value, then return a scalar instead of a
# tuple. # tuple.
payload = payload[0] payload = payload[0]
return payload return payload
class ExifImageIfd(Ifd): class ExifImageIfd(Ifd):
tagnum2name = {271: 'Make', """
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', 272: 'Model',
273: 'StripOffsets',
274: 'Orientation',
277: 'SamplesPerPixel',
278: 'RowsPerStrip',
279: 'StripByteCounts',
282: 'XResolution', 282: 'XResolution',
283: 'YResolution', 283: 'YResolution',
284: 'PlanarConfiguration',
290: 'GrayResponseUnit',
291: 'GrayResponseCurve',
292: 'T4Options',
293: 'T6Options',
296: 'ResolutionUnit', 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', 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', 34665: 'ExifTag',
34853: 'GPSTag'} 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, buffer, offset): def __init__(self, endian, buffer, offset):
Ifd.__init__(self, endian, buffer, offset) Ifd.__init__(self, endian, buffer, offset)
@ -1772,18 +1997,77 @@ class ExifImageIfd(Ifd):
tag_name = self.tagnum2name[tag] tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value self.ifd[tag_name] = value
class ExifPhotoIfd(Ifd): class ExifPhotoIfd(Ifd):
tagnum2name = {34855: 'ISOSpeedRatings', 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', 36864: 'ExifVersion',
36867: 'DateTimeOriginal', 36867: 'DateTimeOriginal',
36868: 'DateTimeDigitized', 36868: 'DateTimeDigitized',
37121: 'ComponentsConfiguration', 37121: 'ComponentsConfiguration',
37122: 'CompressedBitsPerPixel',
37377: 'ShutterSpeedValue',
37378: 'ApertureValue',
37379: 'BrightnessValue',
37380: 'ExposureBiasValue',
37381: 'MaxApertureValue',
37382: 'SubjectDistance',
37383: 'MeteringMode',
37384: 'LightSource',
37385: 'Flash',
37386: 'FocalLength', 37386: 'FocalLength',
37396: 'SubjectArea',
37500: 'MakerNote',
37510: 'UserComment',
37520: 'SubSecTime',
37521: 'SubSecTimeOriginal',
37522: 'SubSecTimeDigitized',
40960: 'FlashpixVersion', 40960: 'FlashpixVersion',
40961: 'ColorSpace', 40961: 'ColorSpace',
40962: 'PixelXDimension', 40962: 'PixelXDimension',
40963: 'PixelYDimension', 40963: 'PixelYDimension',
40965: 'InteroperabilityTag'} 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, buffer, offset): def __init__(self, endian, buffer, offset):
Ifd.__init__(self, endian, buffer, offset) Ifd.__init__(self, endian, buffer, offset)
@ -1794,6 +2078,7 @@ class ExifPhotoIfd(Ifd):
tag_name = self.tagnum2name[tag] tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value self.ifd[tag_name] = value
class ExifGPSInfoIfd(Ifd): class ExifGPSInfoIfd(Ifd):
tagnum2name = {0: 'GPSVersionID', tagnum2name = {0: 'GPSVersionID',
1: 'GPSLatitudeRef', 1: 'GPSLatitudeRef',
@ -1836,6 +2121,7 @@ class ExifGPSInfoIfd(Ifd):
tag_name = self.tagnum2name[tag] tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value self.ifd[tag_name] = value
class ExifInteroperabilityIfd(Ifd): class ExifInteroperabilityIfd(Ifd):
tagnum2name = {1: 'InteroperabilityIndex', tagnum2name = {1: 'InteroperabilityIndex',
2: 'InteroperabilityVersion', 2: 'InteroperabilityVersion',
@ -1875,6 +2161,7 @@ _box_with_id = {
'uuid': UUIDBox, 'uuid': UUIDBox,
'xml ': XMLBox} 'xml ': XMLBox}
def _indent(elem, level=0): def _indent(elem, level=0):
"""Recipe for pretty printing XML. Please see """Recipe for pretty printing XML. Please see
@ -1894,6 +2181,7 @@ def _indent(elem, level=0):
if level and (not elem.tail or not elem.tail.strip()): if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i elem.tail = i
def _pretty_print_xml(xml, level=0): def _pretty_print_xml(xml, level=0):
"""Pretty print XML data. """Pretty print XML data.
""" """

View file

@ -171,7 +171,7 @@ class TestSuiteNegative(unittest.TestCase):
ofile.write(data, cbsize=(13, 12)) ofile.write(data, cbsize=(13, 12))
def test_codeblock_size_with_precinct_size(self): def test_codeblock_size_with_precinct_size(self):
# Seems like code block sizes should never exceed half that of # Seems like code block sizes should never exceed half that of
# precinct size. # precinct size.
ifile = Jp2k(self.jp2file) ifile = Jp2k(self.jp2file)
data = ifile.read(reduce=3) data = ifile.read(reduce=3)

View file

@ -810,5 +810,63 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines) expected = '\n'.join(lines)
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
def test_exif_uuid(self):
j = glymur.Jp2k(self.jp2file)
print(j.box[3])
actual = sys.stdout.getvalue().strip()
lines = ["UUID Box (uuid) @ (77, 638)",
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)",
" UUID Data: ",
"{'Exif': {'ExifTag': 138,",
" 'GPSTag': 354,",
" 'Make': 'HTC',",
" 'Model': 'HTC Glacier',",
" 'ResolutionUnit': 2,",
" 'XResolution': 72.0,",
" 'YCbCrPositioning': 1,",
" 'YResolution': 72.0},",
" 'GPSInfo': {'GPSAltitude': 0.0,",
" 'GPSAltitudeRef': 0,",
" 'GPSDateStamp': '2013:02:09',",
" 'GPSLatitude': [42.0, 20.0, 33.61],",
" 'GPSLatitudeRef': 'N',",
" 'GPSLongitude': [71.0, 5.0, 17.32],",
" 'GPSLongitudeRef': 'W',",
" 'GPSMapDatum': 'WGS-84',",
" 'GPSProcessingMethod': (65,",
" 83,",
" 67,",
" 73,",
" 73,",
" 0,",
" 0,",
" 0,",
" 78,",
" 69,",
" 84,",
" 87,",
" 79,",
" 82,",
" 75),",
" 'GPSTimeStamp': [19.0, 47.0, 53.0],",
" 'GPSVersionID': (2, 2, 0)},",
" 'Iop': None,",
" 'Photo': {'ColorSpace': 1,",
" 'ComponentsConfiguration': (1, 2, 3, 0),",
" 'DateTimeDigitized': '2013:02:09 14:47:53',",
" 'DateTimeOriginal': '2013:02:09 14:47:53',",
" 'ExifVersion': (48, 50, 50, 48),",
" 'FlashpixVersion': (48, 49, 48, 48),",
" 'FocalLength': 3.53,",
" 'ISOSpeedRatings': 76,",
" 'InteroperabilityTag': 324,",
" 'PixelXDimension': 2528,",
" 'PixelYDimension': 1424}}"]
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

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