Merge branch 'issue11' into devel

Conflicts:
	docs/source/introduction.rst
This commit is contained in:
jevans 2013-06-05 20:46:10 -04:00
commit 56e76aac03
11 changed files with 667 additions and 61 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
sizes are not in harmony. Added statement to docs about upstream library
dependence. Added roadmap to docs.

View file

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

View file

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

View file

@ -1,6 +1,6 @@
************
------------
How do I...?
************
------------
Get the code?
=============
@ -12,7 +12,7 @@ Go to either of
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. ::
$ jp2dump /path/to/glymur/installation/data/nemo.jp2
@ -25,9 +25,9 @@ From within Python, it is as simple as printing the Jp2k object, i.e. ::
>>> j = Jp2k(file)
>>> print(j)
The primary emphasis is on JP2 metadata, but it is possible to
display just raw codestream as well. This will display metadata present in the
codestream's main header only. ::
This prints the metadata found in the JP2 boxes, but in the case of the
codestream box, only the main header is printed. It is possible to print
**only** the codestream information as well, i.e. ::
>>> print(j.get_codestream())

View file

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

View file

@ -1,6 +1,6 @@
=========================================
----------------------------------------
Glymur: a Python interface for JPEG 2000
=========================================
----------------------------------------
**Glymur** contains a Python interface to the OpenJPEG library
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,
however.
Of particular focus is retrieval of metadata. Reading XMP UUID boxes
is supported, as the data block consists of an XMP packet, which 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.
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.
------------
''''''''''''
Requirements
------------
''''''''''''
glymur works on Python 2.7 and 3.3. Python 3.3 is strongly recommended.
OpenJPEG
@ -133,9 +133,9 @@ Windows
-------
Not currently supported.
------------------------------------
''''''''''''''''''''''''''''''''''''
Installation, Testing, Configuration
------------------------------------
''''''''''''''''''''''''''''''''''''
From this point forward, python3 will be referred to as just "python".

View file

@ -1,9 +1,10 @@
-------
Roadmap
=======
-------
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
* add read support or Exif UUIDs
* 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 math
import os
import pprint
import struct
import sys
import uuid
@ -1570,6 +1571,9 @@ class UUIDBox(Jp2kBox):
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)'
uuid_data = '\n' + pprint.pformat(self.data)
else:
uuid_type = ''
uuid_data = '{0} bytes'.format(len(self.data))
@ -1618,12 +1622,522 @@ class UUIDBox(Jp2kBox):
else:
text = buffer.decode('utf-8')
kwargs['data'] = ET.fromstring(text)
elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2':
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:
kwargs['data'] = buffer
box = UUIDBox(**kwargs)
return box
class Exif:
"""
Attributes
----------
buffer : bytes
Raw byte stream consisting of the UUID data.
endian : str
Either '<' for big-endian, or '>' for little-endian.
"""
def __init__(self, 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.buffer = buffer
# Ignore the first six bytes.
# Next 8 should be (73, 73, 42, 8)
data = struct.unpack('<BBHI', buffer[6:14])
if data[0] == 73 and data[1] == 73:
# little endian
self.endian = '<'
else:
# big endian
self.endian = '>'
offset = data[3]
# This is the 'Exif Image' portion.
exif = ExifImageIfd(self.endian, buffer[6:], offset)
self.exif_image = exif.ifd
if 'ExifTag' in self.exif_image.keys():
offset = self.exif_image['ExifTag']
photo = ExifPhotoIfd(self.endian, buffer[6:], offset)
self.exif_photo = photo.ifd
if 'InteroperabilityTag' in self.exif_photo.keys():
offset = self.exif_photo['InteroperabilityTag']
interop = ExifInteroperabilityIfd(self.endian,
buffer[6:],
offset)
self.iop = interop.ifd
if 'GPSTag' in self.exif_image.keys():
offset = self.exif_image['GPSTag']
gps = ExifGPSInfoIfd(self.endian, buffer[6:], offset)
self.exif_gpsinfo = gps.ifd
class Ifd:
"""
Attributes
----------
buffer : bytes
Raw byte stream consisting of the UUID data.
datatype2fmt : dictionary
Class attribute, maps the TIFF enumerated datatype to the python
datatype and data width.
endian : str
Either '<' for big-endian, or '>' for little-endian.
num_tags : int
Number of tags in the IFD.
raw_ifd : dictionary
Maps tag number 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, buffer, offset):
self.endian = endian
self.buffer = buffer
self.num_tags, = struct.unpack(endian + 'H',
buffer[offset:offset + 2])
fmt = self.endian + 'HHII' * self.num_tags
ifd_buffer = buffer[offset + 2:offset + 2 + self.num_tags * 12]
data = struct.unpack(fmt, ifd_buffer)
self.raw_ifd = {}
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 = 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.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
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, buffer, offset):
Ifd.__init__(self, endian, buffer, offset)
# Now post process the raw IFD.
self.ifd = {}
for tag, value in self.raw_ifd.items():
tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value
class ExifPhotoIfd(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, buffer, offset):
Ifd.__init__(self, endian, buffer, offset)
# Now post process the raw IFD.
self.ifd = {}
for tag, value in self.raw_ifd.items():
tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value
class ExifGPSInfoIfd(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, buffer, offset):
Ifd.__init__(self, endian, buffer, offset)
# Now post process the raw IFD.
self.ifd = {}
for tag, value in self.raw_ifd.items():
tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value
class ExifInteroperabilityIfd(Ifd):
tagnum2name = {1: 'InteroperabilityIndex',
2: 'InteroperabilityVersion',
4096: 'RelatedImageFileFormat',
4097: 'RelatedImageWidth',
4098: 'RelatedImageLength'}
def __init__(self, endian, buffer, offset):
Ifd.__init__(self, endian, buffer, offset)
# Now post process the raw IFD.
self.ifd = {}
for tag, value in self.raw_ifd.items():
tag_name = self.tagnum2name[tag]
self.ifd[tag_name] = value
# Map each box ID to the corresponding class.
_box_with_id = {
'asoc': AssociationBox,

View file

@ -52,6 +52,7 @@ class TestJp2k(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Setup a JP2 file with a bad XML box.
jp2file = pkg_resources.resource_filename(glymur.__name__,
"data/nemo.jp2")
with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile:

View file

@ -11,6 +11,7 @@ else:
from io import StringIO
import glymur
from glymur import Jp2k
try:
data_root = os.environ['OPJ_DATA_ROOT']
@ -22,6 +23,22 @@ except:
class TestPrinting(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Setup a plain JP2 file without the two UUID boxes.
jp2file = pkg_resources.resource_filename(glymur.__name__,
"data/nemo.jp2")
with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile:
cls._plain_nemo_file = tfile.name
ijfile = Jp2k(jp2file)
data = ijfile.read(reduce=3)
ojfile = Jp2k(cls._plain_nemo_file, 'wb')
ojfile.write(data)
@classmethod
def tearDownClass(cls):
os.unlink(cls._plain_nemo_file)
def setUp(self):
# Save sys.stdout.
self.stdout = sys.stdout
@ -30,15 +47,14 @@ class TestPrinting(unittest.TestCase):
"data/nemo.jp2")
# Save the output of dumping nemo.jp2 for more than one test.
lines = ['File: nemo.jp2',
'JPEG 2000 Signature Box (jP ) @ (0, 12)',
lines = ['JPEG 2000 Signature Box (jP ) @ (0, 12)',
' Signature: 0d0a870a',
'File Type Box (ftyp) @ (12, 20)',
' Brand: jp2 ',
" Compatibility: ['jp2 ']",
'JP2 Header Box (jp2h) @ (32, 45)',
' Image Header Box (ihdr) @ (40, 22)',
' Size: [1456 2592 3]',
' Size: [182 324 3]',
' Bitdepth: 8',
' Signed: False',
' Compression: wavelet',
@ -47,45 +63,29 @@ class TestPrinting(unittest.TestCase):
' Method: enumerated colorspace',
' Precedence: 0',
' Colorspace: sRGB',
'UUID Box (uuid) @ (77, 638)',
' UUID: 4a706754-6966-6645-7869-662d3e4a5032',
' UUID Data: 614 bytes',
'UUID Box (uuid) @ (715, 2412)',
' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)',
' UUID Data: ',
' <ns0:xmpmeta xmlns:ns0="adobe:ns:meta/" '
+ 'xmlns:ns2="http://ns.adobe.com/xap/1.0/" '
+ 'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '
+ 'ns0:xmptk="XMP Core 4.4.0-Exiv2">',
' <rdf:RDF>',
' <rdf:Description ns2:CreatorTool="glymur" '
+ 'rdf:about="" />',
' </rdf:RDF>',
' </ns0:xmpmeta>',
' ',
'Contiguous Codestream Box (jp2c) @ (3127, 1133427)',
'Contiguous Codestream Box (jp2c) @ (77, 112814)',
' Main header:',
' SOC marker segment @ (3135, 0)',
' SIZ marker segment @ (3137, 47)',
' SOC marker segment @ (85, 0)',
' SIZ marker segment @ (87, 47)',
' Profile: 2',
' Reference Grid Height, Width: (1456 x 2592)',
' Reference Grid Height, Width: (182 x 324)',
' Vertical, Horizontal Reference Grid Offset: '
+ '(0 x 0)',
' Reference Tile Height, Width: (512 x 512)',
' Reference Tile Height, Width: (182 x 324)',
' Vertical, Horizontal Reference Tile Offset: '
+ '(0 x 0)',
' Bitdepth: (8, 8, 8)',
' Signed: (False, False, False)',
' Vertical, Horizontal Subsampling: '
+ '((1, 1), (1, 1), (1, 1))',
' COD marker segment @ (3186, 12)',
' COD marker segment @ (136, 12)',
' Coding style:',
' Entropy coder, without partitions',
' SOP marker segments: False',
' EPH marker segments: False',
' Coding style parameters:',
' Progression order: LRCP',
' Number of layers: 3',
' Number of layers: 1',
' Multiple component transformation usage: '
+ 'reversible',
' Number of resolutions: 6',
@ -102,26 +102,29 @@ class TestPrinting(unittest.TestCase):
+ 'False',
' Predictable termination: False',
' Segmentation symbols: False',
' QCD marker segment @ (3200, 19)',
' QCD marker segment @ (150, 19)',
' Quantization style: no quantization, '
+ '2 guard bits',
' Step size: [(0, 8), (0, 9), (0, 9), '
+ '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), '
+ '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), '
+ '(0, 10)]']
self.expectedNemo = '\n'.join(lines)
self.expectedPlain = '\n'.join(lines)
def tearDown(self):
# Restore stdout.
sys.stdout = self.stdout
#import pdb; pdb.set_trace()
def test_jp2dump(self):
glymur.jp2dump(self.jp2file)
glymur.jp2dump(self._plain_nemo_file)
actual = sys.stdout.getvalue().strip()
self.actual = actual
self.expected = self.expectedNemo
self.assertEqual(actual, self.expectedNemo)
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, self.expectedPlain)
def test_COC_segment(self):
j = glymur.Jp2k(self.jp2file)
@ -429,17 +432,42 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
def test_entire_file(self):
def test_xmp(self):
# Verify the printing of a UUID/XMP box.
j = glymur.Jp2k(self.jp2file)
print(j.box[4])
actual = sys.stdout.getvalue().strip()
lst = ['UUID Box (uuid) @ (715, 2412)',
' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)',
' UUID Data: ',
' <ns0:xmpmeta xmlns:ns0="adobe:ns:meta/" '
+ 'xmlns:ns2="http://ns.adobe.com/xap/1.0/" '
+ 'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '
+ 'ns0:xmptk="XMP Core 4.4.0-Exiv2">',
' <rdf:RDF>',
' <rdf:Description ns2:CreatorTool="glymur" '
+ 'rdf:about="" />',
' </rdf:RDF>',
' </ns0:xmpmeta>']
expected = '\n'.join(lst)
self.assertEqual(actual, expected)
def test_entire_file(self):
j = glymur.Jp2k(self._plain_nemo_file)
print(j)
actual = sys.stdout.getvalue().strip()
self.assertEqual(actual, self.expectedNemo)
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, self.expectedPlain)
def test_codestream(self):
j = glymur.Jp2k(self.jp2file)
print(j.get_codestream())
actual = sys.stdout.getvalue().strip()
lst = ['Codestream:',
' SOC marker segment @ (3135, 0)',
' SIZ marker segment @ (3137, 47)',
@ -782,5 +810,63 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
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__":
unittest.main()

View file

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