diff --git a/docs/source/api.rst b/docs/source/api.rst index e1efdac..7584718 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -5,7 +5,7 @@ API Jp2k ---- .. autoclass:: glymur.Jp2k - :members: read, write, read_bands, get_codestream + :members: read, write, wrap, read_bands, get_codestream Individual Boxes ---------------- @@ -24,9 +24,9 @@ ColourSpecificationBox .. autoclass:: glymur.jp2box.ColourSpecificationBox :members: -ComponentDefinitionBox +ChannelDefinitionBox '''''''''''''''''''''' -.. autoclass:: glymur.jp2box.ComponentDefinitionBox +.. autoclass:: glymur.jp2box.ChannelDefinitionBox :members: ComponentMappingBox diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 355ff14..a4c0167 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -34,6 +34,92 @@ codestream box, only the main header is printed. It is possible to print >>> print(j.get_codestream()) +Add XML 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`, you can use the **wrap** method with +no box argument: :: + + >>> import glymur + >>> jfile = glymur.data.goodstuff() + >>> j2k = glymur.Jp2k(jfile) + >>> jp2 = j2k.wrap("newfile.jp2") + >>> print(jp2) + File: newfile.jp2 + 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: [800 480 3] + Bitdepth: 8 + Signed: False + Compression: wavelet + Colorspace Unknown: False + Colour Specification Box (colr) @ (62, 15) + Method: enumerated colorspace + Precedence: 0 + Colorspace: sRGB + Contiguous Codestream Box (jp2c) @ (77, 115228) + Main header: + . + . (truncated) + . + +The raw codestream was wrapped in a JP2 jacket with four boxes in the outer +layer (the signature, file type, JP2 header, and contiguous codestream), with +two additional boxes (image header and color specification) contained in the +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, we'll need to specify all of the +boxes. If you already have a JP2 jacket in place, you can just reuse it, +though. Take the following example content in an XML file `favorites.xml` : :: + + + + Light Ale + + +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 + >>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml') + >>> boxes.insert(3, xmlbox) + >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes) + >>> print(jp2_xml) + File: newfile_with_xml.jp2 + 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: [800 480 3] + Bitdepth: 8 + Signed: False + Compression: wavelet + Colorspace Unknown: False + Colour Specification Box (colr) @ (62, 15) + Method: enumerated colorspace + Precedence: 0 + Colorspace: sRGB + XML Box (xml ) @ (77, 76) + + Light Ale + + + Contiguous Codestream Box (jp2c) @ (153, 115236) + Main header: + . + . (truncated) + . + + Work with XMP UUIDs? ==================== The example JP2 file shipped with glymur has an XMP UUID. :: diff --git a/glymur/codestream.py b/glymur/codestream.py index 04c208f..327dd19 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -175,7 +175,7 @@ class Codestream(object): segment = self._parseGenericSegment(f, marker_id) else: - msg = 'Invalid marker id encountered at byte {0:d}' + msg = 'Invalid marker id encountered at byte {0:d} ' msg += 'in codestream: "0x{1:x}"' msg = msg.format(offset, marker_id) raise IOError(msg) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 54edfbd..bc20bbf 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -958,12 +958,6 @@ class JP2HeaderBox(Jp2kBox): def _write(self, f): """Write a JP2 Header box to file. """ - # Make sure they are in proper order. - if self.box[0].id != 'ihdr': - msg = "The first box in the JP2 Header superbox must be the Image " - msg += "Header box." - raise IOError(msg) - # Write the contained boxes, then come back and write the length. orig_pos = f.tell() f.write(struct.pack('>I', 0)) @@ -1590,7 +1584,10 @@ class XMLBox(Jp2kBox): msg = Jp2kBox.__str__(self) xml = self.xml if self.xml is not None: - msg += _pretty_print_xml(self.xml) + try: + msg += _pretty_print_xml(self.xml) + except TypeError: + msg += _pretty_print_xml(self.xml.getroot()) else: msg += '\n {0}'.format(xml) return msg diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 846d7fd..93a3a93 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -408,14 +408,29 @@ class Jp2k(Jp2kBox): self._parse() def wrap(self, filename, boxes=None): - """Write a codestream, wrapping it in the JP2 format. + """Write the codestream back out to file, wrapped in new JP2 jacket. Parameters ---------- filename : str JP2 file to be created from a raw codestream. boxes : list - JP2 box definitions to define the JP2 file format. + JP2 box definitions to define the JP2 file format. If not + provided, a default ""jacket" is assumed, consisting of JP2 + signature, file type, JP2 header, and contiguous codestream boxes. + + Returns + ------- + jp2 : Jp2k object + Newly wrapped Jp2k object. + + Examples + -------- + >>> import glymur, tempfile + >>> jfile = glymur.data.goodstuff() + >>> j2k = glymur.Jp2k(jfile) + >>> tfile = tempfile.NamedTemporaryFile(suffix='jp2') + >>> j2k.wrap(tfile.name) """ if boxes is None: # Try to create a reasonable default. @@ -502,14 +517,31 @@ class Jp2k(Jp2kBox): box._write(ofile) else: # The codestream gets written last. - ofile.write(struct.pack('>I', self.length + 8)) - ofile.write('jp2c'.encode()) - - with open(self.filename, 'rb') as ifile: - ofile.write(ifile.read()) + if len(self.box) == 0: + # Am I a raw codestream? If so, then it is pretty + # easy, just write the codestream box header plus all + # of myself out to file. + ofile.write(struct.pack('>I', self.length + 8)) + ofile.write('jp2c'.encode()) + with open(self.filename, 'rb') as ifile: + ofile.write(ifile.read()) + else: + # OK, I'm a jp2 file. Need to find out where the + # raw codestream actually starts. + jp2c = [box for box in self.box if box.id == 'jp2c'] + jp2c = jp2c[0] + ofile.write(struct.pack('>I', jp2c.length + 8)) + ofile.write('jp2c'.encode()) + with open(self.filename, 'rb') as ifile: + # Seek 8 bytes past the L, T fields to get to the + # raw codestream. + ifile.seek(jp2c.offset + 8) + ofile.write(ifile.read(jp2c.length - 8)) ofile.flush() + jp2 = Jp2k(filename) + return jp2 def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False): diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 59a241c..75a16b0 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -234,44 +234,6 @@ class TestXML(unittest.TestCase): self.jp2file = glymur.data.nemo() self.j2kfile = glymur.data.goodstuff() - j2k = Jp2k(self.j2kfile) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - self.jP = JPEG2000SignatureBox() - self.ftyp = FileTypeBox() - self.jp2h = JP2HeaderBox() - self.jp2c = ContiguousCodestreamBox() - self.ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - - def tearDown(self): - pass - - def test_basic_xml(self): - # Should be able to write an XMLBox. - j2k = Jp2k(self.j2kfile) - - self.jp2h.box = [self.ihdr, self.colr] - - the_xml = ET.fromstring('0') - xmlb = glymur.jp2box.XMLBox(xml=the_xml) - self.assertEqual(ET.tostring(xmlb.xml), - b'0') - - boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] - - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j2k.wrap(tfile.name, boxes=boxes) - jp2 = Jp2k(tfile.name) - self.assertEqual(jp2.box[3].id, 'xml ') - self.assertEqual(ET.tostring(jp2.box[3].xml), - b'0') - - def test_xml_from_file(self): raw_xml = b""" @@ -295,30 +257,77 @@ class TestXML(unittest.TestCase): """ - with tempfile.NamedTemporaryFile(suffix=".xml") as tfile: + with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile: tfile.write(raw_xml) tfile.flush() + self.xmlfile = tfile.name - j2k = Jp2k(self.j2kfile) - - self.jp2h.box = [self.ihdr, self.colr] + j2k = Jp2k(self.j2kfile) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) - xmlb = glymur.jp2box.XMLBox(filename=tfile.name) - boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j2k.wrap(tfile.name, boxes=boxes) - jp2 = Jp2k(tfile.name) + self.jP = JPEG2000SignatureBox() + self.ftyp = FileTypeBox() + self.jp2h = JP2HeaderBox() + self.jp2c = ContiguousCodestreamBox() + self.ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - output_boxes = [box.id for box in jp2.box] - self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', - 'jp2c']) + def tearDown(self): + os.unlink(self.xmlfile) + pass - elts = jp2.box[3].xml.findall('country') - self.assertEqual(len(elts), 3) + def test_negative_both_file_and_xml_provided(self): + """The XML should come from only one source.""" + j2k = Jp2k(self.j2kfile) + xml_object = ET.parse(self.xmlfile) + with self.assertRaises((IOError, OSError)) as ce: + xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object) - neighbor = elts[1].find('neighbor') - self.assertEqual(neighbor.attrib['name'], 'Malaysia') - self.assertEqual(neighbor.attrib['direction'], 'N') + def test_basic_xml(self): + # Should be able to write an XMLBox. + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + the_xml = ET.fromstring('0') + xmlb = glymur.jp2box.XMLBox(xml=the_xml) + self.assertEqual(ET.tostring(xmlb.xml), + b'0') + + boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + self.assertEqual(jp2.box[3].id, 'xml ') + self.assertEqual(ET.tostring(jp2.box[3].xml), + b'0') + + def test_xml_from_file(self): + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile) + boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + + output_boxes = [box.id for box in jp2.box] + self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', + 'jp2c']) + + elts = jp2.box[3].xml.findall('country') + self.assertEqual(len(elts), 3) + + neighbor = elts[1].find('neighbor') + self.assertEqual(neighbor.attrib['name'], 'Malaysia') + self.assertEqual(neighbor.attrib['direction'], 'N') class TestColourSpecificationBox(unittest.TestCase): @@ -369,22 +378,25 @@ class TestColourSpecificationBox(unittest.TestCase): def test_ColourSpecificationBox_with_colorspace_and_icc(self): # Colour specification boxes can't have both. - with self.assertRaises(IOError): + with self.assertRaises((OSError, IOError)): colorspace = glymur.core.SRGB icc_profile = b'\x01\x02\x03\x04' - b = glymur.jp2box.ColourSpecificationBox(colorspace, icc_profile) + b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + icc_profile=icc_profile) def test_ColourSpecificationBox_with_bad_method(self): colorspace = glymur.core.SRGB method = -1 with self.assertRaises(IOError): - b = glymur.jp2box.ColourSpecificationBox(colorspace, method) + b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + method=method) def test_ColourSpecificationBox_with_bad_approximation(self): colorspace = glymur.core.SRGB - approximation = -1 + approx = -1 with self.assertRaises(IOError): - b = glymur.jp2box.ColourSpecificationBox(colorspace, approximation) + b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + approximation=approx) @unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None,