Added append capability, #103

This commit is contained in:
jevans 2013-08-18 14:47:49 -04:00
commit 08ff86e4a2
2 changed files with 138 additions and 3 deletions

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

@ -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):