From a3fb3ec03727059304813d4cfdc161f4ead8f5b1 Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 4 Feb 2014 20:51:27 -0500 Subject: [PATCH] Write support for Palette, Component Mapping box. Closes #149 --- CHANGES.txt | 11 ++++--- glymur/jp2box.py | 67 +++++++++++++++++++++++++++++++++++--- glymur/test/test_jp2box.py | 26 +++++++++++++++ 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 894a103..324a530 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,8 +1,9 @@ -Feb 03, 2014 - Added read support for JPX free, number list, data reference, - fragment table, and fragment list boxes. Palette box now a 2D numpy array - instead of a list of 1D arrays. JP2 super box constructors now take - optional box list argument. Fixed bug where JPX files with more than one - codestream but advertising jp2 compatibility were not being read. +Feb 03, 2014 - Added write support for Palette and Component Mapping boxes. + Added read support for JPX free, number list, data reference, fragment + table, and fragment list boxes. Palette box now a 2D numpy array instead + of a list of 1D arrays. JP2 super box constructors now take optional box + list argument. Fixed bug where JPX files with more than one codestream + but advertising jp2 compatibility were not being read. Jan 28, 2014 - v0.5.10 Fixed bad warning when reader requirements box mask length is unsupported. diff --git a/glymur/jp2box.py b/glymur/jp2box.py index c1a6700..d3b4ed6 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -15,6 +15,7 @@ References import copy import datetime +import io import math import os import pprint @@ -23,6 +24,7 @@ import sys import uuid import warnings import xml.etree.cElementTree as ET + if sys.hexversion < 0x02070000: # pylint: disable=F0401,E0611 from ordereddict import OrderedDict @@ -719,6 +721,20 @@ class ComponentMappingBox(Jp2kBox): msg = msg.format(self.component_index[k], k) return msg + def write(self, fptr): + """Write a Component Mapping box to file. + """ + length = 8 + 4 * len(self.component_index) + write_buffer = struct.pack('>I4s', length, self.box_id.encode()) + fptr.write(write_buffer) + + for j in range(len(self.component_index)): + write_buffer = struct.pack('>HBB', + self.component_index[j], + self.mapping_type[j], + self.palette_index[j]) + fptr.write(write_buffer) + @staticmethod def parse(fptr, offset, length): """Parse component mapping box. @@ -1527,9 +1543,9 @@ class PaletteBox(Jp2kBox): self.offset = offset def __repr__(self): - msg = "glymur.jp2box.PaletteBox({0}, bits_per_component={1}, " - msg += "signed={2})" - msg = msg.format(self.palette, self.bits_per_component, self.signed) + msg = "glymur.jp2box.PaletteBox(ndarray, bits_per_component={0}, " + msg += "signed={1})" + msg = msg.format(self.bits_per_component, self.signed) return msg def __str__(self): @@ -1537,6 +1553,49 @@ class PaletteBox(Jp2kBox): msg += '\n Size: ({0} x {1})'.format(*self.palette.shape) return msg + def write(self, fptr): + """Write a Palette box to file. + """ + # Box length is usual header (8) + # + num entries NE (2) + num columns NC (1) + # + (bps/8, /signed) for each column (3) + bps * NC + # + + bytes_per_row = sum(self.bits_per_component) / 8 + bytes_per_palette = bytes_per_row * self.palette.shape[0] + box_length = 8 + 3 + self.palette.shape[1] + bytes_per_palette + + # Write the usual header. + write_buffer = struct.pack('>I4s', + int(box_length), self.box_id.encode()) + fptr.write(write_buffer) + + write_buffer = struct.pack('>HB', self.palette.shape[0], + self.palette.shape[1]) + fptr.write(write_buffer) + + bps_signed = [x - 1 for x in self.bits_per_component] + for j, item in enumerate(bps_signed): + if self.signed[j]: + bps_signed[j] |= 0x80 + write_buffer = struct.pack('>' + 'B' * self.palette.shape[1], + *bps_signed) + fptr.write(write_buffer) + + if self.bits_per_component[0] <= 8: + dtype = np.uint8 + code = 'B' + elif self.bits_per_component[0] <= 16: + dtype = np.uint16 + code = 'H' + elif self.bits_per_component[0] <= 32: + dtype = np.uint32 + code = 'I' + + fmt = '>' + code * self.palette.shape[1] + for row in self.palette: + write_buffer = struct.pack(fmt, *row) + fptr.write(write_buffer) + @staticmethod def parse(fptr, offset, length): """Parse palette box. @@ -1561,7 +1620,7 @@ class PaletteBox(Jp2kBox): # Need to determine bps and signed or not read_buffer = fptr.read(num_columns) data = struct.unpack('>' + 'B' * num_columns, read_buffer) - bps = [((x & 0x07f) + 1) for x in data] + bps = [((x & 0x7f) + 1) for x in data] signed = [((x & 0x80) > 1) for x in data] fmt = '>' diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 05c0a83..ff4d075 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -26,6 +26,7 @@ import tempfile import uuid from uuid import UUID import xml.etree.cElementTree as ET +import warnings if sys.hexversion < 0x02070000: import unittest2 as unittest @@ -496,6 +497,7 @@ class TestWrap(unittest.TestCase): def setUp(self): self.j2kfile = glymur.data.goodstuff() self.jp2file = glymur.data.nemo() + self.jpxfile = glymur.data.jpxfile() def tearDown(self): pass @@ -557,6 +559,30 @@ class TestWrap(unittest.TestCase): j2k.wrap(tfile.name) self.verify_wrapped_raw(tfile.name) + @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") + def test_palette(self): + """basic test for rewrapping a jpx file""" + with warnings.catch_warnings(): + # This file has a rreq mask length that we do not recognize. + warnings.simplefilter("ignore") + jpx = Jp2k(self.jpxfile) + idx = [0, 1, 3, 6] + boxes = [jpx.box[idx] for idx in [0, 1, 3, 6]] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + jp2 = jpx.wrap(tfile.name, boxes=boxes) + + # Verify the outer boxes. + boxes = [box.box_id for box in jp2.box] + self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c']) + + # Verify the inside boxes. + boxes = [box.box_id for box in jp2.box[2].box] + self.assertEqual(boxes, ['ihdr', 'colr', 'pclr', 'cmap']) + + expected_offsets = [0, 12, 40, 887] + for j, offset in enumerate(expected_offsets): + self.assertEqual(jp2.box[j].offset, offset) + @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_wrap_jp2(self): """basic test for rewrapping a jp2 file, no specified boxes"""