diff --git a/glymur/jp2box.py b/glymur/jp2box.py index e6a7921..fd0a356 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -167,7 +167,7 @@ class ColourSpecificationBox(Jp2kBox): colorspace is not None, then icc_profile must be empty. """ def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0, - approximation=1, colorspace=None, icc_profile=None, **kwargs): + approximation=0, colorspace=None, icc_profile=None, **kwargs): Jp2kBox.__init__(self, id='', longname='Colour Specification') if colorspace is not None and icc_profile is not None: @@ -208,6 +208,20 @@ class ColourSpecificationBox(Jp2kBox): return msg + def _write(self, f): + """Write an Colour Specification box to file. + """ + length = 15 if self.icc_profile is None else 11 + len(self.icc_profile) + f.write(struct.pack('>I', length)) + f.write('colr'.encode()) + + buffer = struct.pack('>BBBI', + self.method, + self.precedence, + self.approximation, + self.colorspace) + f.write(buffer) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 color specification box. @@ -756,6 +770,25 @@ class ImageHeaderBox(Jp2kBox): self.colorspace_unknown) return msg + def _write(self, f): + """Write an Image Header box to file. + """ + f.write(struct.pack('>I', 22)) + f.write('ihdr'.encode()) + + # signedness and bps are stored together in a single byte + bit_depth_signedness = 0x80 if self.signed else 0x00 + bit_depth_signedness |= self.bits_per_component - 1 + buffer = struct.pack('>IIHBBBB', + self.height, + self.width, + self.num_components, + bit_depth_signedness, + self.compression, + 1 if self.colorspace_unknown else 0, + 1 if self.ip_provided else 0) + f.write(buffer) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 image header box. diff --git a/glymur/jp2k.py b/glymur/jp2k.py index fcd0893..0409c03 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -407,17 +407,37 @@ class Jp2k(Jp2kBox): filename : str JP2 file to be created from a raw codestream. """ + if len(self.box) > 0: + msg = "This method can only be used on files consisting only of " + msg += "a raw codestream. It cannot be used on a file that " + msg += "already has the JP2 format." + raise IOError(msg) + boxes = [JPEG2000SignatureBox(), FileTypeBox(), - JP2HeaderBox(), - ContiguousCodestreamBox()] - boxes[2].box = [ImageHeaderBox(height=1456, - width=2592, - num_components=3), + 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 fp: + with open(filename, 'wb') as ofile: for box in boxes: - box._write(fp) + box._write(ofile) + + # 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()) + + ofile.flush() + + def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False): """Read a JPEG 2000 image. diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index bb068d2..27bf303 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -28,7 +28,7 @@ class TestJp2Boxes(unittest.TestCase): j = Jp2k(jp2file) c = [box for box in j.box if box.id == 'jp2c'][0] - with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as ofile: + with tempfile.NamedTemporaryFile(suffix='.j2c', delete=False) as ofile: with open(jp2file, 'rb') as ifile: # Everything up until the jp2c box. ifile.seek(c.offset+8) @@ -74,7 +74,7 @@ class TestJp2Boxes(unittest.TestCase): b = glymur.jp2box.ColourSpecificationBox(colorspace=glymur.core.SRGB) self.assertEqual(b.method, glymur.core.ENUMERATED_COLORSPACE) self.assertEqual(b.precedence, 0) - self.assertEqual(b.approximation, 1) + self.assertEqual(b.approximation, 0) self.assertEqual(b.colorspace, glymur.core.SRGB) self.assertIsNone(b.icc_profile) @@ -108,9 +108,71 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.main_header, []) def test_tojp2(self): - j = Jp2k(self.raw_codestream) + j2k = Jp2k(self.raw_codestream) + with tempfile.NamedTemporaryFile(suffix=".jp2", mode="wb") as tfile: + j2k.tojp2(tfile.name) + + 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) + + 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): + # Should not use "tojp2" method on file that is already jp2. + j = glymur.Jp2k(self.jp2file) with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j.tojp2(tfile.name) + with self.assertRaises(IOError): + j.tojp2(tfile.name) if __name__ == "__main__": unittest.main()