diff --git a/CHANGES.txt b/CHANGES.txt index 5e62c1c..2dff6f3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +Oct 29, 2013 - Palette box now a 2D numpy array instead of a list of + 1D arrays. Super box constructors now take optional box list argument. + Removed ssiz attribute from SIZsegment class. + Oct 29, 2013 - v0.5.9 Fixed bad library load on linux as a result of 0.5.8 Oct 29, 2013 - v0.5.8 Fixed unnecessary warnings when default locations for diff --git a/docs/source/detailed_installation.rst b/docs/source/detailed_installation.rst index 3e4d9dc..92810b7 100644 --- a/docs/source/detailed_installation.rst +++ b/docs/source/detailed_installation.rst @@ -13,7 +13,7 @@ both read and write JPEG 2000 files, but you may wish to install version 2.0 or the 2.0+ version from OpenJPEG's development trunk for better performance. If you do that, you should compile it as a shared library (named *openjp2* instead of *openjpeg*) from the developmental source that you can retrieve -via subversion. As of this time of writing, svn revision 2347 works. +via subversion. As of this time of writing, svn revision r2354 works. You should also download the test data for the purpose of configuring and running OpenJPEG's test suite, check their instructions for all this. You should set the **OPJ_DATA_ROOT** environment variable for the purpose diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 212862d..f34264e 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -4,7 +4,7 @@ How do I...? ... read the lowest resolution thumbnail? -===================================== +========================================= Printing the Jp2k object should reveal the number of resolutions (look in the COD segment section), but you can take a shortcut by supplying -1 as the resolution level. :: @@ -15,7 +15,7 @@ resolution level. :: >>> thumbnail = j.read(rlevel=-1) ... display metadata? -================= +===================== There are two ways. From the unix command line, the script *jp2dump* is available. :: @@ -35,7 +35,7 @@ codestream box, only the main header is printed. It is possible to print >>> print(j.get_codestream()) ... add XML metadata? -================= +===================== You can append any number of XML boxes to a JP2 file (not to a raw codestream). Consider the following XML file `data.xml` : :: @@ -67,7 +67,7 @@ The **append** method can add an XML box as shown below:: >>> print(jp2) ... add metadata in a more general fashion? -======================================= +=========================================== 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` (a file consisting of a raw codestream), @@ -158,7 +158,7 @@ while **append** modifies an existing file and is currently limited to XML boxes. ... create an image with an alpha layer? -==================================== +======================================== OpenJPEG can create JP2 files with more than 3 components (requires the development version of OpenJPEG), but by default, any extra components are diff --git a/glymur/codestream.py b/glymur/codestream.py index e83c5cc..d01795f 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -646,16 +646,50 @@ class Codestream(object): length, = struct.unpack('>H', read_buffer) xy_buffer = fptr.read(36) + data = struct.unpack('>HIIIIIIIIH', xy_buffer) - num_components, = struct.unpack('>H', xy_buffer[-2:]) + rsiz = data[0] + xysiz = (data[1], data[2]) + xyosiz = (data[3], data[4]) + xytsiz = (data[5], data[6]) + xytosiz = (data[7], data[8]) - component_buffer = fptr.read(num_components * 3) + # Csiz is the number of components + Csiz = data[9] - segment = SIZsegment(xy_buffer, component_buffer, length, offset) + component_buffer = fptr.read(Csiz * 3) + data = struct.unpack('>' + 'B' * len(component_buffer), + component_buffer) + + bitdepth = tuple(((x & 0x7f) + 1) for x in data[0::3]) + signed = tuple(((x & 0xb0) > 0) for x in data[0::3]) + + xrsiz = data[1::3] + yrsiz = data[2::3] + + for j, subsampling in enumerate(zip(xrsiz, yrsiz)): + if 0 in subsampling: + msg = "Invalid subsampling value for component {0}: " + msg += "dx={1}, dy={2}." + msg = msg.format(j, subsampling[0], subsampling[1]) + warnings.warn(msg) + + kwargs = {'rsiz': rsiz, + 'xysiz': xysiz, + 'xyosiz': xyosiz, + 'xytsiz': xytsiz, + 'xytosiz': xytosiz, + 'Csiz': Csiz, + 'bitdepth': bitdepth, + 'signed': signed, + 'xyrsiz': (xrsiz, yrsiz), + 'length': length, + 'offset': offset} + segment = SIZsegment(**kwargs) # Need to keep track of the number of components from SIZ for - # other markers - self._csiz = len(segment.ssiz) + # other markers. + self._csiz = Csiz return segment @@ -1441,8 +1475,8 @@ class SIZsegment(Segment): Width and height of reference tile with respect to the reference grid. xtosiz, ytosiz : int Horizontal and vertical offsets of tile from origin of reference grid. - ssiz : iterable bytes - Encoded precision (depth) in bits and sign of each component. + Csiz : int + Number of components in image. bitdepth : iterable bytes Precision (depth) in bits of each component. signed : iterable bool @@ -1457,21 +1491,20 @@ class SIZsegment(Segment): 15444-1:2004 - Information technology -- JPEG 2000 image coding system: Core coding system """ - def __init__(self, xy_buffer, component_buffer, length, offset): - Segment.__init__(self, marker_id='SIZ') + def __init__(self, rsiz=-1, xysiz=None, xyosiz=-1, xytsiz=-1, xytosiz=-1, + Csiz=-1, bitdepth=None, signed=None, xyrsiz=-1, length=-1, + offset=-1): + Segment.__init__(self, marker_id='SIZ', length=length, offset=offset) - data = struct.unpack('>HIIIIIIIIH', xy_buffer) - - self.rsiz = data[0] - self.xsiz = data[1] - self.ysiz = data[2] - self.xosiz = data[3] - self.yosiz = data[4] - self.xtsiz = data[5] - self.ytsiz = data[6] - self.xtosiz = data[7] - self.ytosiz = data[8] - # disregarding the last element in data + self.rsiz = rsiz + self.xsiz, self.ysiz = xysiz + self.xosiz, self.yosiz = xyosiz + self.xtsiz, self.ytsiz = xytsiz + self.xtosiz, self.ytosiz = xytosiz + self.Csiz = Csiz + self.bitdepth = bitdepth + self.signed = signed + self.xrsiz, self.yrsiz = xyrsiz num_tiles_x = (self.xsiz - self.xosiz) / (self.xtsiz - self.xtosiz) num_tiles_y = (self.ysiz - self.yosiz) / (self.ytsiz - self.ytosiz) @@ -1480,26 +1513,23 @@ class SIZsegment(Segment): msg = "Invalid number of tiles ({0}).".format(numtiles) warnings.warn(msg) - data = struct.unpack('>' + 'B' * len(component_buffer), - component_buffer) - - self.ssiz = data[0::3] - - for j, subsampling in enumerate(list(zip(data[1::3], data[2::3]))): - if 0 in subsampling: - msg = "Invalid subsampling value for component {0}: " - msg += "dx={1}, dy={2}." - msg = msg.format(j, subsampling[0], subsampling[1]) - warnings.warn(msg) - self.xrsiz = data[1::3] - self.yrsiz = data[2::3] - - self.bitdepth = tuple(((x & 0x7f) + 1) for x in self.ssiz) - self.signed = tuple(((x & 0xb0) > 0) for x in self.ssiz) - - self.length = length - self.offset = offset + def __repr__(self): + msg = "glymur.codestream.SIZsegment(rsiz={rsiz}, xysiz={xysiz}, " + msg += "xyosiz={xyosiz}, xytsiz={xytsiz}, xytosiz={xytosiz}, " + msg += "Csiz={Csiz}, bitdepth={bitdepth}, signed={signed}, " + msg += "xyrsiz={xyrsiz})" + msg = msg.format(rsiz=self.rsiz, + xysiz=(self.xsiz, self.ysiz), + xyosiz=(self.xosiz, self.yosiz), + xytsiz=(self.xtsiz, self.ytsiz), + xytosiz=(self.xtosiz, self.ytosiz), + Csiz=self.Csiz, + bitdepth=self.bitdepth, + signed=self.signed, + xyrsiz=(self.xrsiz, self.yrsiz)) + return msg + def __str__(self): msg = Segment.__str__(self) msg += '\n ' @@ -1549,6 +1579,9 @@ class SOCsegment(Segment): Segment.__init__(self, marker_id='SOC') self.__dict__.update(**kwargs) + def __repr__(self): + msg = "glymur.codestream.SOCsegment()" + return msg class SODsegment(Segment): """Container for Start of Data (SOD) segment information. diff --git a/glymur/jp2box.py b/glymur/jp2box.py index bf88e4d..a66ff68 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -75,6 +75,12 @@ class Jp2kBox(object): self.offset = offset self.longname = longname + def __repr__(self): + msg = "glymur.jp2box.Jp2kBox(box_id='{0}', offset={1}, length={2}, " + msg += "longname='{3}')" + msg = msg.format(self.box_id, self.offset, self.length, self.longname) + return msg + def __str__(self): msg = "{0} Box ({1})".format(self.longname, self.box_id) msg += " @ ({0}, {1})".format(self.offset, self.length) @@ -206,6 +212,17 @@ class ColourSpecificationBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.ColourSpecificationBox(" + msg += "method={0}, precedence={1}, approximation={2}, colorspace={3}, " + msg += "icc_profile={4})" + msg = msg.format(self.method, + self.precedence, + self.approximation, + self.colorspace, + self.icc_profile) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) @@ -426,12 +443,12 @@ class ChannelDefinitionBox(Jp2kBox): offset of the box from the start of the file. longname : str more verbose description of the box. - index : int + index : list number of the channel. Defaults to monotonically increasing sequence, i.e. [0, 1, 2, ...] - channel_type : int + channel_type : list type of the channel - association : int + association : list index of the associated color """ def __init__(self, index=None, channel_type=None, association=None, @@ -458,9 +475,9 @@ class ChannelDefinitionBox(Jp2kBox): msg += " 65535 - unspecified" raise IOError(msg) - self.index = index - self.channel_type = channel_type - self.association = association + self.index = tuple(index) + self.channel_type = tuple(channel_type) + self.association = tuple(association) self.__dict__.update(**kwargs) def __str__(self): @@ -475,6 +492,12 @@ class ChannelDefinitionBox(Jp2kBox): msg = msg.format(self.index[j], color_type_string, assn) return msg + def __repr__(self): + msg = "glymur.jp2box.ChannelDefinitionBox(" + msg += "index={0}, channel_type={1}, association={2})" + msg = msg.format(self.index, self.channel_type, self.association) + return msg + def write(self, fptr): """Write a channel definition box to file. """ @@ -537,11 +560,15 @@ class CodestreamHeaderBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=[], length=0, offset=-1): Jp2kBox.__init__(self, box_id='jpch', longname='Codestream Header') self.length = length self.offset = offset - self.box = [] + self.box = box + + def __repr__(self): + msg = "glymur.jp2box.CodestreamHeaderBox(box={0})".format(self.box) + return msg def __str__(self): msg = Jp2kBox.__str__(self) @@ -595,13 +622,18 @@ class CompositingLayerHeaderBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=[], length=0, offset=-1): Jp2kBox.__init__(self, box_id='jplh', longname='Compositing Layer Header') self.length = length self.offset = offset self.box = [] + def __repr__(self): + msg = "glymur.jp2box.CompositingLayerHeaderBox(box={0})" + msg = msg.format(self.box) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: @@ -650,11 +682,11 @@ class ComponentMappingBox(Jp2kBox): Offset of the box from the start of the file. longname : str Verbose description of the box. - component_index : int + component_index : tuple Index of component in codestream that is mapped to this channel. - mapping_type : int + mapping_type : tuple mapping type, either direct use (0) or palette (1) - palette_index : int + palette_index : tuple Index component from palette """ def __init__(self, component_index, mapping_type, palette_index, @@ -666,6 +698,14 @@ class ComponentMappingBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.ComponentMappingBox(" + msg += "component_index={0}, mapping_type={1}, palette_index={2})" + msg = msg.format(self.component_index, + self.mapping_type, + self.palette_index) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) @@ -733,6 +773,10 @@ class ContiguousCodestreamBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.ContiguousCodeStreamBox(main_header={0})" + return msg.format(repr(self.main_header)) + def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Main header:' @@ -801,6 +845,13 @@ class FileTypeBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.FileTypeBox(brand='{0}', minor_version={1}, " + msg += "compatibility_list={2})" + msg = msg.format(self.brand, self.minor_version, + self.compatibility_list) + return msg + def __str__(self): lst = [Jp2kBox.__str__(self), ' Brand: {0}', @@ -914,8 +965,23 @@ class ImageHeaderBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.ImageHeaderBox(" + msg += "{height}, {width}, num_components={num_components}, " + msg += "signed={signed}, bits_per_component={bits_per_component}, " + msg += "compression={compression}, " + msg += "colorspace_unknown={colorspace_unknown}, " + msg += "ip_provided={ip_provided})" + msg = msg.format(height=self.height, width=self.width, + num_components=self.num_components, + signed=self.signed, + bits_per_component=self.bits_per_component, + compression=self.compression, + colorspace_unknown=self.colorspace_unknown, + ip_provided=self.ip_provided) + return msg + def __str__(self): - msg = Jp2kBox.__str__(self) msg = "{0}" msg += '\n Size: [{1} {2} {3}]' msg += '\n Bitdepth: {4}' @@ -1004,11 +1070,15 @@ class AssociationBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=[], length=0, offset=-1): Jp2kBox.__init__(self, box_id='asoc', longname='Association') self.length = length self.offset = offset - self.box = [] + self.box = box + + def __repr__(self): + msg = "glymur.jp2box.AssociationBox(box={0})".format(self.box) + return msg def __str__(self): msg = Jp2kBox.__str__(self) @@ -1062,11 +1132,15 @@ class JP2HeaderBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=[], length=0, offset=-1): Jp2kBox.__init__(self, box_id='jp2h', longname='JP2 Header') self.length = length self.offset = offset - self.box = [] + self.box = box + + def __repr__(self): + msg = "glymur.jp2box.JP2HeaderBox(box={0})".format(self.box) + return msg def __str__(self): msg = Jp2kBox.__str__(self) @@ -1141,6 +1215,9 @@ class JPEG2000SignatureBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + return 'glymur.jp2box.JPEG2000SignatureBox()' + def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Signature: {0:02x}{1:02x}{2:02x}{3:02x}' @@ -1193,8 +1270,8 @@ class PaletteBox(Jp2kBox): offset of the box from the start of the file. longname : str more verbose description of the box. - palette : list - Colormap represented as list of 1D arrays, one per color component. + palette : ndarray + Colormap array. """ def __init__(self, palette, bits_per_component, signed, length=0, offset=-1): @@ -1205,10 +1282,15 @@ class PaletteBox(Jp2kBox): self.length = length 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) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) - msg += '\n Size: ({0} x {1})'.format(len(self.palette[0]), - len(self.palette)) + msg += '\n Size: ({0} x {1})'.format(*self.palette.shape) return msg @staticmethod @@ -1238,68 +1320,29 @@ class PaletteBox(Jp2kBox): bps = [((x & 0x07f) + 1) for x in data] signed = [((x & 0x80) > 1) for x in data] + fmt = '>' + for bits in bps: + if bits <= 8: + fmt += 'B' + elif bits <= 16: + fmt += 'H' + elif bits <= 32: + fmt += 'I' + # Each palette component is padded out to the next largest byte. # That means a list comprehension does this in one shot. row_nbytes = sum([int(math.ceil(x/8.0)) for x in bps]) - # Form the format string so that we can intelligently unpack the - # colormap. We have to do this because it is possible that the - # colormap columns could have different datatypes. - # - # This means that we store the palette as a list of 1D arrays, - # which reverses the usual indexing scheme. read_buffer = fptr.read(num_entries * row_nbytes) - palette = _buffer2palette(read_buffer, num_entries, num_columns, bps) + palette = np.zeros((num_entries, num_columns), dtype=np.int32) + for j in range(num_entries): + palette[j] = struct.unpack_from(fmt, read_buffer, + offset=j * row_nbytes) box = PaletteBox(palette, bps, signed, length=length, offset=offset) return box -def _buffer2palette(read_buffer, num_rows, num_cols, bps): - """Construct the palette from the buffer read from file. - - Parameters - ---------- - read_buffer : iterable - Byte array of palette information read from file. - num_rows, num_cols : int - Size of palette. - bps : iterable - Bits per sample for each channel. - - Returns - ------- - palette : list of 1D arrays - Each 1D array corresponds to a channel. - """ - row_nbytes = 0 - palette = [] - fmt = '>' - for j in range(num_cols): - if bps[j] <= 8: - row_nbytes += 1 - fmt += 'B' - palette.append(np.zeros(num_rows, dtype=np.uint8)) - elif bps[j] <= 16: - row_nbytes += 2 - fmt += 'H' - palette.append(np.zeros(num_rows, dtype=np.uint16)) - elif bps[j] <= 32: - row_nbytes += 4 - fmt += 'I' - palette.append(np.zeros(num_rows, dtype=np.uint32)) - else: - msg = 'Unsupported palette bitdepth (%d).'.format(bps[j]) - raise IOError(msg) - - for j in range(num_rows): - row_buffer = read_buffer[(row_nbytes * j):(row_nbytes * (j + 1))] - row = struct.unpack(fmt, row_buffer) - for k in range(num_cols): - palette[k][j] = row[k] - - return palette - # Map rreq codes to display text. _READER_REQUIREMENTS_DISPLAY = { 0: 'File not completely understood', @@ -1426,6 +1469,18 @@ class ReaderRequirementsBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.ReaderRequirementsBox(fuam={fuam}, dcm={dcm}, " + msg += "standard_flag={standard_flag}, standard_mask={standard_mask}, " + msg += "vendor_feature={vendor_feature}, vendor_mask={vendor_mask})" + msg = msg.format(fuam=self.fuam, + dcm=self.dcm, + standard_flag=self.standard_flag, + standard_mask=self.standard_mask, + vendor_feature=self.vendor_feature, + vendor_mask=self.vendor_mask) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) @@ -1563,11 +1618,16 @@ class ResolutionBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=[], length=0, offset=-1): Jp2kBox.__init__(self, box_id='res ', longname='Resolution') self.length = length self.offset = offset - self.box = [] + self.box = box + + def __repr__(self): + msg = "glymur.jp2box.ResolutionBox(box={0})" + msg = msg.format(self.box) + return msg def __str__(self): msg = Jp2kBox.__str__(self) @@ -1629,6 +1689,11 @@ class CaptureResolutionBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.CaptureResolutionBox({0}, {1})" + msg = msg.format(self.vertical_resolution, self.horizontal_resolution) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n VCR: {0}'.format(self.vertical_resolution) @@ -1686,6 +1751,11 @@ class DisplayResolutionBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.DisplayResolutionBox({0}, {1})" + msg = msg.format(self.vertical_resolution, self.horizontal_resolution) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n VDR: {0}'.format(self.vertical_resolution) @@ -1747,6 +1817,10 @@ class LabelBox(Jp2kBox): msg += '\n Label: {0}'.format(self.label) return msg + def __repr__(self): + msg = 'glymur.jp2box.LabelBox("{0}")'.format(self.label) + return msg + @staticmethod def parse(fptr, offset, length): """Parse Label box. @@ -1808,6 +1882,9 @@ class XMLBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + return "glymur.jp2box.XMLBox(xml={0})".format(self.xml) + def __str__(self): msg = Jp2kBox.__str__(self) xml = self.xml @@ -1866,7 +1943,6 @@ class XMLBox(Jp2kBox): msg = msg.format(offset, ude.reason) warnings.warn(msg, UserWarning) - # Strip out any trailing nulls, as they can foul up XML parsing. text = text.rstrip(chr(0)) @@ -1906,6 +1982,10 @@ class UUIDListBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.UUIDListBox({0})".format(self.ulst) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) for j, uuid_item in enumerate(self.ulst): @@ -1957,11 +2037,15 @@ class UUIDInfoBox(Jp2kBox): box : list List of boxes contained in this superbox. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=[], length=0, offset=-1): Jp2kBox.__init__(self, box_id='uinf', longname='UUIDInfo') self.length = length self.offset = offset - self.box = [] + self.box = box + + def __repr__(self): + msg = "glymur.jp2box.UUIDInfoBox(box={0})".format(self.box) + return msg def __str__(self): msg = Jp2kBox.__str__(self) @@ -2030,6 +2114,11 @@ class DataEntryURLBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.DataEntryURLBox({0}, {1}, '{2}')" + msg = msg.format(self.version, self.flag, self.url) + return msg + def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n ' @@ -2112,6 +2201,7 @@ class UUIDBox(Jp2kBox): """ Jp2kBox.__init__(self, box_id='uuid', longname='UUID') self.uuid = the_uuid + self.raw_data = raw_data if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): # XMP data. Parse as XML. Seems to be a difference between @@ -2136,6 +2226,12 @@ class UUIDBox(Jp2kBox): self.length = length self.offset = offset + def __repr__(self): + msg = "glymur.jp2box.UUIDBox(the_uuid={0}, " + msg += "raw_data=)" + return msg.format(repr(self.uuid), len(self.raw_data)) + + def __str__(self): msg = '{0}\n' msg += ' UUID: {1}{2}\n' diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 1bd36a4..a9927da 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -72,6 +72,10 @@ class Jp2k(Jp2kBox): if mode == 'rb': self.parse() + def __repr__(self): + msg = "glymur.Jp2k('{0}')".format(self.filename) + return msg + def __str__(self): metadata = ['File: ' + os.path.basename(self.filename)] if len(self.box) > 0: diff --git a/glymur/test/test_codestream.py b/glymur/test/test_codestream.py index 8db5039..76042b9 100644 --- a/glymur/test/test_codestream.py +++ b/glymur/test/test_codestream.py @@ -116,5 +116,49 @@ class TestCodestream(unittest.TestCase): # codestream, so the last one is EOC. self.assertEqual(codestream.segment[-1].marker_id, 'EOC') + +class TestCodestreamRepr(unittest.TestCase): + + def setUp(self): + self.jp2file = glymur.data.nemo() + + def tearDown(self): + pass + + def test_soc(self): + """Test SOC segment repr""" + segment = glymur.codestream.SOCsegment() + newseg = eval(repr(segment)) + self.assertEqual(newseg.marker_id, 'SOC') + + def test_siz(self): + """Test SIZ segment repr""" + kwargs = {'rsiz': 0, + 'xysiz': (2592, 1456), + 'xyosiz': (0, 0), + 'xytsiz': (2592, 1456), + 'xytosiz': (0, 0), + 'Csiz': 3, + 'bitdepth': (8, 8, 8), + 'signed': (False, False, False), + 'xyrsiz': ((1, 1, 1), (1, 1, 1))} + segment = glymur.codestream.SIZsegment(**kwargs) + newseg = eval(repr(segment)) + self.assertEqual(newseg.marker_id, 'SIZ') + self.assertEqual(newseg.xsiz, 2592) + self.assertEqual(newseg.ysiz, 1456) + self.assertEqual(newseg.xosiz, 0) + self.assertEqual(newseg.yosiz, 0) + self.assertEqual(newseg.xtsiz, 2592) + self.assertEqual(newseg.ytsiz, 1456) + self.assertEqual(newseg.xtosiz, 0) + self.assertEqual(newseg.ytosiz, 0) + + self.assertEqual(newseg.xrsiz, (1, 1, 1)) + self.assertEqual(newseg.yrsiz, (1, 1, 1)) + self.assertEqual(newseg.bitdepth, (8, 8, 8)) + self.assertEqual(newseg.signed, (False, False, False)) + + if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index ef62460..05c0a83 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -18,11 +18,13 @@ Test suite specifically targeting JP2 box layout. import doctest import os +import re import shutil import struct import sys import tempfile import uuid +from uuid import UUID import xml.etree.cElementTree as ET if sys.hexversion < 0x02070000: @@ -40,7 +42,7 @@ from glymur.jp2box import JPEG2000SignatureBox from glymur.core import COLOR, OPACITY from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE -from .fixtures import OPENJP2_IS_V2_OFFICIAL +from .fixtures import OPENJP2_IS_V2_OFFICIAL, opj_data_file try: FORMAT_CORPUS_DATA_ROOT = os.environ['FORMAT_CORPUS_DATA_ROOT'] @@ -413,7 +415,7 @@ class TestAppend(unittest.TestCase): jp2 = Jp2k(tfile.name) the_xml = ET.fromstring('0') - xmlbox = glymur.jp2box.XMLBox(xml=the_xml) + xmlbox = glymur.jp2box.XMLBox(xml=ET.ElementTree(the_xml)) jp2.append(xmlbox) # The sequence of box IDs should be the same as before, but with an @@ -709,6 +711,250 @@ class TestJp2Boxes(unittest.TestCase): self.assertIsNone(box.main_header) +class TestRepr(unittest.TestCase): + """Tests for __repr__ methods.""" + def test_default_jp2k(self): + """Should be able to eval a JPEG2000SignatureBox""" + jp2k = glymur.jp2box.JPEG2000SignatureBox() + + # Test the representation instantiation. + newbox = eval(repr(jp2k)) + self.assertTrue(isinstance(newbox, glymur.jp2box.JPEG2000SignatureBox)) + self.assertEqual(newbox.signature, (13, 10, 135, 10)) + + def test_default_ftyp(self): + """Should be able to instantiate a FileTypeBox""" + ftyp = glymur.jp2box.FileTypeBox() + + # Test the representation instantiation. + newbox = eval(repr(ftyp)) + self.assertTrue(isinstance(newbox, glymur.jp2box.FileTypeBox)) + self.assertEqual(newbox.brand, 'jp2 ') + self.assertEqual(newbox.minor_version, 0) + self.assertEqual(newbox.compatibility_list, ['jp2 ']) + + def test_colourspecification_box(self): + """Verify __repr__ method on colr box.""" + # TODO: add icc_profile + box = ColourSpecificationBox(colorspace=glymur.core.SRGB) + + newbox = eval(repr(box)) + self.assertEqual(newbox.method, glymur.core.ENUMERATED_COLORSPACE) + self.assertEqual(newbox.precedence, 0) + self.assertEqual(newbox.approximation, 0) + self.assertEqual(newbox.colorspace, glymur.core.SRGB) + self.assertIsNone(newbox.icc_profile) + + def test_channeldefinition_box(self): + """Verify __repr__ method on cdef box.""" + channel_type = [COLOR, COLOR, COLOR] + association = [RED, GREEN, BLUE] + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=channel_type, + association=association) + newbox = eval(repr(cdef)) + self.assertEqual(newbox.index, (0, 1, 2)) + self.assertEqual(newbox.channel_type, (COLOR, COLOR, COLOR)) + self.assertEqual(newbox.association, (RED, GREEN, BLUE)) + + def test_jp2header_box(self): + """Verify __repr__ method on ihdr box.""" + ihdr = ImageHeaderBox(100, 200, num_components=3) + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + jp2h = JP2HeaderBox(box=[ihdr, colr]) + newbox = eval(repr(jp2h)) + self.assertEqual(newbox.box_id, 'jp2h') + self.assertEqual(newbox.box[0].box_id, 'ihdr') + self.assertEqual(newbox.box[1].box_id, 'colr') + + def test_imageheader_box(self): + """Verify __repr__ method on jhdr box.""" + ihdr = ImageHeaderBox(100, 200, num_components=3) + + newbox = eval(repr(ihdr)) + self.assertEqual(newbox.height, 100) + self.assertEqual(newbox.width, 200) + self.assertEqual(newbox.num_components, 3) + self.assertFalse(newbox.signed) + self.assertEqual(newbox.bits_per_component, 8) + self.assertEqual(newbox.compression, 7) + self.assertFalse(newbox.colorspace_unknown) + self.assertFalse(newbox.ip_provided) + + def test_association_box(self): + """Verify __repr__ method on asoc box.""" + asoc = glymur.jp2box.AssociationBox() + newbox = eval(repr(asoc)) + self.assertEqual(newbox.box_id, 'asoc') + self.assertEqual(len(newbox.box), 0) + + def test_codestreamheader_box(self): + """Verify __repr__ method on jpch box.""" + jpch = glymur.jp2box.CodestreamHeaderBox() + newbox = eval(repr(jpch)) + self.assertEqual(newbox.box_id, 'jpch') + self.assertEqual(len(newbox.box), 0) + + def test_compositinglayerheader_box(self): + """Verify __repr__ method on jplh box.""" + jplh = glymur.jp2box.CompositingLayerHeaderBox() + newbox = eval(repr(jplh)) + self.assertEqual(newbox.box_id, 'jplh') + self.assertEqual(len(newbox.box), 0) + + def test_componentmapping_box(self): + """Verify __repr__ method on cmap box.""" + cmap = glymur.jp2box.ComponentMappingBox(component_index=(0, 0, 0), + mapping_type=(1, 1, 1), + palette_index=(0, 1, 2)) + newbox = eval(repr(cmap)) + self.assertEqual(newbox.box_id, 'cmap') + self.assertEqual(newbox.component_index, (0, 0, 0)) + self.assertEqual(newbox.mapping_type, (1, 1, 1)) + self.assertEqual(newbox.palette_index, (0, 1, 2)) + + def test_resolution_boxes(self): + """Verify __repr__ method on resolution boxes.""" + resc = glymur.jp2box.CaptureResolutionBox(0.5, 2.5) + resd = glymur.jp2box.DisplayResolutionBox(2.5, 0.5) + res_super_box = glymur.jp2box.ResolutionBox(box=[resc, resd]) + + newbox = eval(repr(res_super_box)) + + self.assertEqual(newbox.box_id, 'res ') + self.assertEqual(newbox.box[0].box_id, 'resc') + self.assertEqual(newbox.box[0].vertical_resolution, 0.5) + self.assertEqual(newbox.box[0].horizontal_resolution, 2.5) + self.assertEqual(newbox.box[1].box_id, 'resd') + self.assertEqual(newbox.box[1].vertical_resolution, 2.5) + self.assertEqual(newbox.box[1].horizontal_resolution, 0.5) + + def test_label_box(self): + """Verify __repr__ method on label box.""" + lbl = glymur.jp2box.LabelBox("this is a test") + newbox = eval(repr(lbl)) + self.assertEqual(newbox.box_id, 'lbl ') + self.assertEqual(newbox.label, "this is a test") + + def test_data_entry_url_box(self): + """Verify __repr__ method on data entry url box.""" + version = 0 + flag = (0, 0, 0) + url = "http://readthedocs.glymur.org" + box = glymur.jp2box.DataEntryURLBox(version, flag, url) + newbox = eval(repr(box)) + self.assertEqual(newbox.box_id, 'url ') + self.assertEqual(newbox.version, version) + self.assertEqual(newbox.flag, flag) + self.assertEqual(newbox.url, url) + + def test_uuidinfo_box(self): + """Verify __repr__ method on uinf box.""" + uinf = glymur.jp2box.UUIDInfoBox() + newbox = eval(repr(uinf)) + self.assertEqual(newbox.box_id, 'uinf') + self.assertEqual(len(newbox.box), 0) + + def test_uuidlist_box(self): + """Verify __repr__ method on ulst box.""" + uuid1 = uuid.UUID('00000000-0000-0000-0000-000000000001') + uuid2 = uuid.UUID('00000000-0000-0000-0000-000000000002') + uuids = [uuid1, uuid2] + ulst = glymur.jp2box.UUIDListBox(ulst=uuids) + newbox = eval(repr(ulst)) + self.assertEqual(newbox.box_id, 'ulst') + self.assertEqual(newbox.ulst[0], uuid1) + self.assertEqual(newbox.ulst[1], uuid2) + + def test_jp2k_box(self): + """Verify Superclass repr.""" + box = glymur.jp2box.Jp2kBox(box_id='one', offset=2, length=3, + longname='four') + newbox = eval(repr(box)) + self.assertEqual(newbox.box_id, 'one') + self.assertEqual(newbox.offset, 2) + self.assertEqual(newbox.length, 3) + self.assertEqual(newbox.longname, 'four') + + def test_palette_box(self): + """Verify Palette box repr.""" + palette = np.array([[255, 0, 1000], [0, 255, 0]], dtype=np.int32) + bps = (8, 8, 16) + signed = (True, False, True) + box = glymur.jp2box.PaletteBox(palette=palette, bits_per_component=bps, + signed=(True, False, True)) + # The palette can't be reinstantiated thru eval/repr. + s = repr(box) + self.assertTrue(True) + + @unittest.skipIf(sys.hexversion < 0x02070000, "Requires 2.7+") + def test_xml_box(self): + """Verify xml box repr.""" + elt = ET.fromstring('0') + tree = ET.ElementTree(elt) + box = glymur.jp2box.XMLBox(xml=tree) + + regexp = "glymur.jp2box.XMLBox" + regexp += "\(xml=<(xml.etree.ElementTree.){0,1}ElementTree object " + regexp += "at 0x([a-f0-9]*)>\)" + + if sys.hexversion < 0x03000000: + self.assertRegexpMatches(repr(box), regexp) + else: + self.assertRegex(repr(box), regexp) + + def test_readerrequirements_box(self): + """Verify rreq repr method.""" + box = glymur.jp2box.ReaderRequirementsBox(fuam=160, dcm=192, + standard_flag=(5, 61, 43), + standard_mask=(128, 96, 64), + vendor_feature=[], + vendor_mask=[]) + newbox = eval(repr(box)) + self.assertEqual(box.fuam, newbox.fuam) + self.assertEqual(box.dcm, newbox.dcm) + self.assertEqual(box.standard_flag, newbox.standard_flag) + self.assertEqual(box.standard_mask, newbox.standard_mask) + self.assertEqual(box.vendor_feature, newbox.vendor_feature) + self.assertEqual(box.vendor_mask, newbox.vendor_mask) + + @unittest.skipIf(sys.hexversion < 0x02070000, "Requires 2.7+") + def test_uuid_box(self): + """Verify uuid repr method.""" + uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') + data = b'0123456789' + box = glymur.jp2box.UUIDBox(the_uuid=uuid_instance, raw_data=data) + + # Since the raw_data parameter is a sequence of bytes which could be + # quite long, don't bother trying to make it conform to eval(repr()). + regexp = "glymur.jp2box.UUIDBox\(" + regexp += "the_uuid=UUID\('00000000-0000-0000-0000-000000000000'\),\s" + regexp += "raw_data=\)" + + if sys.hexversion < 0x03000000: + self.assertRegexpMatches(repr(box), regexp) + else: + self.assertRegex(repr(box), regexp) + + @unittest.skipIf(sys.hexversion < 0x02070000, "Requires 2.7+") + def test_contiguous_codestream_box(self): + """Verify contiguous codestream box repr method.""" + jp2file = glymur.data.nemo() + jp2 = Jp2k(jp2file) + box = jp2.box[-1] + + # Difficult to eval(repr()) this, so just match the general pattern. + regexp = "glymur.jp2box.ContiguousCodeStreamBox" + regexp += "\(main_header=