Merge branch 'issue103' into devel

This commit is contained in:
jevans 2013-08-18 20:00:48 -04:00
commit 4b93f80fa3
10 changed files with 190 additions and 16 deletions

View file

@ -3,7 +3,7 @@ How do I...?
------------ ------------
Read the lowest resolution thumbnail? read the lowest resolution thumbnail?
===================================== =====================================
Printing the Jp2k object should reveal the number of resolutions (look in the 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 COD segment section), but you can take a shortcut by supplying -1 as the
@ -14,7 +14,7 @@ resolution level. ::
>>> j = glymur.Jp2k(file) >>> j = glymur.Jp2k(file)
>>> thumbnail = j.read(rlevel=-1) >>> thumbnail = j.read(rlevel=-1)
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. ::
@ -34,8 +34,41 @@ codestream box, only the main header is printed. It is possible to print
>>> print(j.get_codestream()) >>> print(j.get_codestream())
Add XML Metadata? 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` : ::
<?xml version="1.0"?>
<info>
<locality>
<city>Boston</city>
<snowfall>24.9 inches</snowfall>
</locality>
<locality>
<city>Portland</city>
<snowfall>31.9 inches</snowfall>
</locality>
<locality>
<city>New York City</city>
<snowfall>11.4 inches</snowfall>
</locality>
</info>
The **append** method can add an XML box (only XML boxes are currently
allowed)::
>>> import shutil
>>> import glymur
>>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2')
>>> from xml.etree import cElementTree as ET
>>> jp2 = glymur.Jp2k('myfile.jp2')
>>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml')
>>> jp2.append(xmlbox)
>>> print(jp2)
add metadata in a more general fashion?
=======================================
An existing raw codestream (or JP2 file) can be wrapped (re-wrapped) in a 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 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), codestream provided by `goodstuff.j2k` (a file consisting of a raw codestream),
@ -87,7 +120,7 @@ though. Take the following example content in an XML file `favorites.xml` : ::
and add it after the JP2 header box, but before the codestream box :: and add it after the JP2 header box, but before the codestream box ::
>>> boxes = jp2.box # The box attribute is the list of JP2 boxes >>> boxes = jp2.box # The box attribute is the list of JP2 boxes
>>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml') >>> xmlbox = glymur.jp2box.XMLBox(filename='favorites.xml')
>>> boxes.insert(3, xmlbox) >>> boxes.insert(3, xmlbox)
>>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes) >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
>>> print(jp2_xml) >>> print(jp2_xml)
@ -119,7 +152,12 @@ and add it after the JP2 header box, but before the codestream box ::
. (truncated) . (truncated)
. .
Create an image with an alpha layer? 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.
create an image with an alpha layer?
==================================== ====================================
OpenJPEG can create JP2 files with more than 3 components (requires OpenJPEG can create JP2 files with more than 3 components (requires
@ -181,7 +219,7 @@ Here's how the Preview application on the mac shows the RGBA image.
.. image:: goodstuff_alpha.png .. image:: goodstuff_alpha.png
Work with XMP UUIDs? work with XMP UUIDs?
==================== ====================
The example JP2 file shipped with glymur has an XMP UUID. :: The example JP2 file shipped with glymur has an XMP UUID. ::

View file

@ -1308,7 +1308,7 @@ class QCCsegment(Segment):
self.offset = offset self.offset = offset
self.mantissa, self.exponent = parse_quantization(self.spqcc, self.mantissa, self.exponent = parse_quantization(self.spqcc,
self.sqcc) self.sqcc)
self.guard_bits = (self.sqcc & 0xe0) >> 5 self.guard_bits = (self.sqcc & 0xe0) >> 5
def __str__(self): def __str__(self):

View file

@ -432,6 +432,42 @@ class Jp2k(Jp2kBox):
# Refresh the metadata. # Refresh the metadata.
self.parse() self.parse()
def append(self, box):
"""Append a JP2 box to the file in-place.
Parameters
----------
box : Jp2Box
Instance of a JP2 box. Currently only XML boxes are allowed.
"""
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.")
# Check the last box. If the length field is zero, then rewrite
# the length field to reflect the true length of the box.
with open(self.filename, 'rb') as ifile:
offset = self.box[-1].offset
ifile.seek(offset)
read_buffer = ifile.read(4)
box_length, = struct.unpack('>I', read_buffer)
if box_length == 0:
# Reopen the file in write mode and rewrite the length field.
true_box_length = os.path.getsize(ifile.name) - offset
with open(self.filename, 'r+b') as ofile:
ofile.seek(offset)
write_buffer = struct.pack('>I', true_box_length)
ofile.write(write_buffer)
# Can now safely append the box.
with open(self.filename, 'ab') as ofile:
box.write(ofile)
self.parse()
def wrap(self, filename, boxes=None): def wrap(self, filename, boxes=None):
"""Write the codestream back out to file, wrapped in new JP2 jacket. """Write the codestream back out to file, wrapped in new JP2 jacket.
@ -1133,6 +1169,7 @@ def _unpack_colorspace(colorspace, img_array, cparams):
return colorspace return colorspace
def _populate_comptparms(img_array, cparams): def _populate_comptparms(img_array, cparams):
"""Instantiate and populate comptparms structure. """Instantiate and populate comptparms structure.
@ -1171,6 +1208,7 @@ def _populate_comptparms(img_array, cparams):
return comptparms return comptparms
def _populate_image_struct(cparams, image, imgdata): def _populate_image_struct(cparams, image, imgdata):
"""Populates image struct needed for compression. """Populates image struct needed for compression.
@ -1203,6 +1241,7 @@ def _populate_image_struct(cparams, image, imgdata):
return image return image
def _validate_compression_params(img_array, cparams): def _validate_compression_params(img_array, cparams):
"""Check that the compression parameters are valid. """Check that the compression parameters are valid.
@ -1313,4 +1352,3 @@ class LibraryNotFoundError(IOError):
""" """
def __init__(self, msg): def __init__(self, msg):
IOError.__init__(self, msg) IOError.__init__(self, msg)

View file

@ -30,7 +30,7 @@ NO_READ_BACKEND_MSG += "order to run the tests in this suite."
try: try:
from matplotlib.pyplot import imread from matplotlib.pyplot import imread
# The whole point of trying to import PIL is to determine if it's there # The whole point of trying to import PIL is to determine if it's there
# or not. We won't use it directly. # or not. We won't use it directly.
# pylint: disable=F0401,W0611 # pylint: disable=F0401,W0611
import PIL import PIL

View file

@ -91,7 +91,7 @@ class TestCallbacks15(unittest.TestCase):
def test_info_callbacks_on_read(self): def test_info_callbacks_on_read(self):
"""Verify stdout when reading. """Verify stdout when reading.
Verify that we get the expected stdio output when our internal info Verify that we get the expected stdio output when our internal info
callback handler is enabled. callback handler is enabled.
""" """

View file

@ -5,7 +5,7 @@ Test suite for codestream parsing.
# unittest doesn't work well with R0904. # unittest doesn't work well with R0904.
# pylint: disable=R0904 # pylint: disable=R0904
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 # tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
# pylint: disable=E1101 # pylint: disable=E1101
# unittest2 is python2.6 only (pylint/python-2.7) # unittest2 is python2.6 only (pylint/python-2.7)

View file

@ -4,7 +4,7 @@ OPENJP2 may be present in some form or other.
# unittest doesn't work well with R0904. # unittest doesn't work well with R0904.
# pylint: disable=R0904 # pylint: disable=R0904
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 # tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
# pylint: disable=E1101 # pylint: disable=E1101
# unittest.mock only in Python 3.3 (python2.7/pylint import issue) # unittest.mock only in Python 3.3 (python2.7/pylint import issue)

View file

@ -18,8 +18,11 @@ Test suite specifically targeting JP2 box layout.
import doctest import doctest
import os import os
import shutil
import struct
import sys import sys
import tempfile import tempfile
import uuid
import xml.etree.cElementTree as ET import xml.etree.cElementTree as ET
if sys.hexversion < 0x02070000: if sys.hexversion < 0x02070000:
@ -502,6 +505,100 @@ class TestColourSpecificationBox(unittest.TestCase):
approximation=approx) approximation=approx)
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.")
class TestAppend(unittest.TestCase):
"""Tests for append method."""
def setUp(self):
self.j2kfile = glymur.data.goodstuff()
self.jp2file = glymur.data.nemo()
def tearDown(self):
pass
def test_append_xml(self):
"""Should be able to append an XML box."""
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
shutil.copyfile(self.jp2file, tfile.name)
jp2 = Jp2k(tfile.name)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
jp2.append(xmlbox)
# 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 ']
self.assertEqual(box_ids, expected)
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
b'<data>0</data>')
def test_only_jp2_allowed_to_append(self):
"""Only JP2 files are allowed to be appended."""
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
shutil.copyfile(self.j2kfile, tfile.name)
jp2 = 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)
with self.assertRaises(IOError):
jp2.append(uuidbox)
def test_length_field_is_zero(self):
"""L=0 (length field in box header) is handled.
L=0 implies that the containing box is the last box. If this is not
handled properly, the appended box is never seen.
"""
baseline_jp2 = Jp2k(self.jp2file)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
offset = baseline_jp2.box[-1].offset
tfile.write(ifile.read(offset))
# Write the L, T fields of the jp2c box such that L == 0
write_buffer = struct.pack('>I4s', int(0), b'jp2c')
tfile.write(write_buffer)
# Write out the rest of the codestream.
ifile.seek(offset+8)
tfile.write(ifile.read())
tfile.flush()
jp2 = Jp2k(tfile.name)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
jp2.append(xmlbox)
# 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 ']
self.assertEqual(box_ids, expected)
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
b'<data>0</data>')
def test_only_xml_allowed_to_append(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.
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
data = b'0123456789'
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
with self.assertRaises(IOError):
jp2.append(uuidbox)
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.") "Missing openjp2 library.")
class TestWrap(unittest.TestCase): class TestWrap(unittest.TestCase):
@ -703,7 +800,7 @@ class TestJp2Boxes(unittest.TestCase):
def test_default_ihdr(self): def test_default_ihdr(self):
"""Should be able to instantiate an image header box.""" """Should be able to instantiate an image header box."""
ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256, ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256,
num_components=3) num_components=3)
self.assertEqual(ihdr.height, 512) self.assertEqual(ihdr.height, 512)
self.assertEqual(ihdr.width, 256) self.assertEqual(ihdr.width, 256)
self.assertEqual(ihdr.num_components, 3) self.assertEqual(ihdr.num_components, 3)
@ -715,7 +812,7 @@ class TestJp2Boxes(unittest.TestCase):
"""Should be able to set jp2h boxes.""" """Should be able to set jp2h boxes."""
box = JP2HeaderBox() box = JP2HeaderBox()
box.box = [ImageHeaderBox(height=512, width=256), box.box = [ImageHeaderBox(height=512, width=256),
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)] ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
self.assertTrue(True) self.assertTrue(True)
def test_default_ccodestreambox(self): def test_default_ccodestreambox(self):

View file

@ -17,7 +17,7 @@ suite.
# unittest fools pylint with "too many public methods" # unittest fools pylint with "too many public methods"
# pylint: disable=R0904 # pylint: disable=R0904
# Some tests use numpy test infrastructure, which means the tests never # Some tests use numpy test infrastructure, which means the tests never
# reference "self", so pylint claims it should be a function. No, no, no. # reference "self", so pylint claims it should be a function. No, no, no.
# pylint: disable=R0201 # pylint: disable=R0201

View file

@ -441,7 +441,8 @@ class TestSuiteWrite(unittest.TestCase):
self.assertEqual(len(codestream.segment[2].spcod), 9) self.assertEqual(len(codestream.segment[2].spcod), 9)
# 18 SOP segments. # 18 SOP segments.
nsops = [x.nsop for x in codestream.segment if x.marker_id == 'SOP'] nsops = [x.nsop for x in codestream.segment
if x.marker_id == 'SOP']
self.assertEqual(nsops, list(range(18))) self.assertEqual(nsops, list(range(18)))
def test_NR_ENC_Bretagne2_ppm_7_encode(self): def test_NR_ENC_Bretagne2_ppm_7_encode(self):