diff --git a/glymur/data/__init__.py b/glymur/data/__init__.py index ff39c0e..3d82c0e 100644 --- a/glymur/data/__init__.py +++ b/glymur/data/__init__.py @@ -1,8 +1,10 @@ """Shipping JPEG 2000 files. -There is only one JP2 file at the moment, "nemo.jp2", converted from the -original JPEG photo of the aftermath of NEMO, the nor'easter that shutdown -Boston in February of 2013. +These include: + nemo.jp2: converted from the original JPEG photo of the aftermath of NEMO, + the nor'easter that shutdown Boston in February of 2013. + goodstuff.j2k: my favorite bevorage. + """ import pkg_resources @@ -17,3 +19,14 @@ def nemo(): """ file = pkg_resources.resource_filename(__name__, "nemo.jp2") return file + +def goodstuff(): + """Shortcut for specifying path to goodstuff.j2k. + + Returns + ------- + file : str + Platform-independent path to goodstuff.j2k. + """ + file = pkg_resources.resource_filename(__name__, "goodstuff.j2k") + return file diff --git a/glymur/data/goodstuff.j2k b/glymur/data/goodstuff.j2k new file mode 100644 index 0000000..3e3c1b6 Binary files /dev/null and b/glymur/data/goodstuff.j2k differ diff --git a/glymur/jp2box.py b/glymur/jp2box.py index fd0a356..09d5251 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -211,6 +211,10 @@ class ColourSpecificationBox(Jp2kBox): def _write(self, f): """Write an Colour Specification box to file. """ + if self.colorspace is None: + msg = "Writing Colour Specification boxes without enumerated " + msg += "colorspaces is not supported at this time." + raise NotImplementedError(msg) length = 15 if self.icc_profile is None else 11 + len(self.icc_profile) f.write(struct.pack('>I', length)) f.write('colr'.encode()) @@ -925,6 +929,12 @@ 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)) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 0409c03..4d679b9 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -399,13 +399,15 @@ class Jp2k(Jp2kBox): self._parse() - def tojp2(self, filename): + def wrap(self, filename, boxes=None): """Write a codestream, wrapping it in the JP2 format. Parameters ---------- filename : str - JP2 file to be created from a raw codestream. + JP2 file to be created from a raw codestream. + boxes : list + JP2 box definitions to define the JP2 file format. """ if len(self.box) > 0: msg = "This method can only be used on files consisting only of " @@ -413,17 +415,19 @@ class Jp2k(Jp2kBox): msg += "already has the JP2 format." raise IOError(msg) - boxes = [JPEG2000SignatureBox(), - FileTypeBox(), - JP2HeaderBox()] - c = self.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - boxes[2].box = [ImageHeaderBox(height=height, - width=width, - num_components=num_components), - ColourSpecificationBox(colorspace=SRGB)] + if boxes is None: + # Try to create a reasonable default. + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = self.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), + ColourSpecificationBox(colorspace=SRGB)] with open(filename, 'wb') as ofile: for box in boxes: box._write(ofile) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 27bf303..17cb70e 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -107,10 +107,10 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.id, 'jp2c') self.assertEqual(b.main_header, []) - def test_tojp2(self): + def test_wrap(self): j2k = Jp2k(self.raw_codestream) with tempfile.NamedTemporaryFile(suffix=".jp2", mode="wb") as tfile: - j2k.tojp2(tfile.name) + j2k.wrap(tfile.name) jp2 = Jp2k(tfile.name) self.assertEqual(len(jp2.box), 4) @@ -159,20 +159,121 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) self.assertIsNone(jp2.box[2].box[1].icc_profile) + def test_default_layout_but_with_specified_boxes(self): + j2k = Jp2k(self.raw_codestream) + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), + ColourSpecificationBox(colorspace=glymur.core.SRGB)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + self.assertEqual(len(jp2.box), 4) + + self.assertEqual(jp2.box[0].id, 'jP ') + self.assertEqual(jp2.box[0].offset, 0) + self.assertEqual(jp2.box[0].length, 12) + self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature') + + self.assertEqual(jp2.box[1].id, 'ftyp') + self.assertEqual(jp2.box[1].offset, 12) + self.assertEqual(jp2.box[1].length, 20) + self.assertEqual(jp2.box[1].longname, 'File Type') + + self.assertEqual(jp2.box[2].id, 'jp2h') + self.assertEqual(jp2.box[2].offset, 32) + self.assertEqual(jp2.box[2].length, 45) + self.assertEqual(jp2.box[2].longname, 'JP2 Header') + + self.assertEqual(jp2.box[3].id, 'jp2c') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 1133427) + + # jp2h super box + self.assertEqual(len(jp2.box[2].box), 2) + + self.assertEqual(jp2.box[2].box[0].id, 'ihdr') + self.assertEqual(jp2.box[2].box[0].offset, 40) + self.assertEqual(jp2.box[2].box[0].length, 22) + self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') + self.assertEqual(jp2.box[2].box[0].height, 1456) + self.assertEqual(jp2.box[2].box[0].width, 2592) + self.assertEqual(jp2.box[2].box[0].num_components, 3) + self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) + self.assertEqual(jp2.box[2].box[0].signed, False) + self.assertEqual(jp2.box[2].box[0].compression, 7) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].ip_provided, False) + + self.assertEqual(jp2.box[2].box[1].id, 'colr') + self.assertEqual(jp2.box[2].box[1].offset, 62) + self.assertEqual(jp2.box[2].box[1].length, 15) + self.assertEqual(jp2.box[2].box[1].longname, + 'Colour Specification') + self.assertEqual(jp2.box[2].box[1].precedence, 0) + self.assertEqual(jp2.box[2].box[1].approximation, 0) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) + self.assertIsNone(jp2.box[2].box[1].icc_profile) + def test_image_header_box_not_first_in_jp2_header(self): # The specification says that ihdr must be the first box in jp2h. - self.assertTrue(False) + j2k = Jp2k(self.raw_codestream) + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ColourSpecificationBox(colorspace=glymur.core.SRGB), + ImageHeaderBox(height=height, + width=width, + num_components=num_components)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) - def test_color_specification_box_with_icc_profile(self): - # Not sure how this should be done so don't allow it. - self.assertTrue(False) - def test_tojp2_on_jp2(self): + def test_color_specification_box_with_out_enumerated_colorspace(self): + j2k = Jp2k(self.raw_codestream) + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), + ColourSpecificationBox(colorspace=None)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(NotImplementedError): + j2k.wrap(tfile.name, boxes=boxes) + + def test_wrap_on_jp2(self): # Should not use "tojp2" method on file that is already jp2. j = glymur.Jp2k(self.jp2file) with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: with self.assertRaises(IOError): - j.tojp2(tfile.name) + j.wrap(tfile.name) + + def test_default_xml(self): + # Should be able to write an xml box. + self.assertTrue(False) + + def test_default_component_definition(self): + # Should be able to specify a component definition box in order to, + # say, create an image with an alpha layer. + self.assertTrue(False) if __name__ == "__main__": unittest.main()