Merge branch 'issue139' into devel

This commit is contained in:
John Evans 2014-03-07 11:01:01 -05:00
commit d74c514302
29 changed files with 3646 additions and 1499 deletions

View file

@ -11,8 +11,8 @@ before_install:
# command to install dependencies
install:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors contextlib2 mock; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install --use-mirrors numpy; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors lxml contextlib2 mock; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install --use-mirrors lxml numpy; fi
# command to run tests
script:

View file

@ -1,11 +1,15 @@
Feb 03, 2014 - Removed support for Python 2.6. Added write support for
JP2 Palette and Component Mapping boxes, JPX Association and
NumberList 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.
Mar 06, 2014 - Added Cinema2K, Cinema4K write support.
Changed constructor for ChannelDefinition box. Removed support
for Python 2.6. Added write support for JP2 UUID, DataEntryURL,
Palette and Component Mapping boxes, JPX Association, NumberList
and DataReference boxes. Added read support for JPX free,
number list, data reference, fragment table, and fragment list
boxes. Improved JPX Reader Requirements box support. Added
get_printoptions, set_printoptions functions. 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.

View file

@ -6,10 +6,29 @@ import glymur
description='Print JPEG2000 metadata.'
parser = argparse.ArgumentParser(description=description)
parser.add_argument('-c', '--codestream', help='dump codestream',
action='store_true')
parser.add_argument('-x', '--noxml', help='Suppress XML.',
action='store_true')
parser.add_argument('-s', '--short', help='Only print box id, offset, and length.',
action='store_true')
chelp='Level of codestream information. 0 suppressed all details, 1 prints headers, 2 prints the full codestream'
parser.add_argument('-c', '--codestream',
help=chelp,
nargs=1,
type=int,
default=[0])
parser.add_argument('filename')
args = parser.parse_args()
if args.noxml:
glymur.set_printoptions(xml=False)
if args.short:
glymur.set_printoptions(short=True)
if args.codestream[0] == 0:
glymur.set_printoptions(codestream=False)
print_full_codestream = False
elif args.codestream[0] == 1:
print_full_codestream = False
else:
print_full_codestream = True
filename = args.filename
glymur.jp2dump(args.filename, codestream=args.codestream)
glymur.jp2dump(args.filename, codestream=print_full_codestream)

45
docs/source/changelog.rst Normal file
View file

@ -0,0 +1,45 @@
---------
ChangeLog
---------
0.6.0 (pending)
===============
* Added Cinema2K, Cinema4K write support.
* Added lxml requirement.
* added set_printoptions, get_printoptions function
* dropped support for Python 2.6, added support for Python 3.4
* dropped windows support (it might work, it might not, I don't much care)
* added write support for JP2 UUID, dataEntryURL, palette, and component mapping boxes
* added read/write support for JPX free, number list, and data reference boxes
* Added read support for JPX fragment list and fragment table boxes
* incompatible change to channel definition box constructor, channel_type and association are no longer keyword arguments
* incompatible change to palette box constructor, it now takes a 2D numpy array instead of a list of 1D arrays
0.5.0 (September 16, 2013)
==========================
* added write support when using OpenJPEG version 1.5
* added version module
0.4.0 (August 18, 2013)
==========================
* added append method
0.3.0 (July 31, 2013)
==========================
* added support for OpenJPEG library version 2.0.0
0.2.0 (July 11, 2013)
==========================
* added Python 2.6, Python 2.7 on windows
* read/write using OpenJPEG library version 2.0.0
* read using OpenJPEG 1.4
0.1.0 (May 27, 2013)
====================
* first release using development version (2.x) of OpenJPEG

View file

@ -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 r2354 works.
via subversion. As of this time of writing, svn revision r2369 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
@ -45,13 +45,6 @@ the path will be ::
$XDG_CONFIG_HOME/glymur/glymurrc
On windows, the path to the configuration file can be determined
by starting up Python and typing ::
import os
os.path.join(os.path.expanduser('~'), 'glymur', 'glymurrc')
You may also include a line for the version 1.x openjpeg library if you have it
installed in a non-standard place, i.e. ::
@ -70,14 +63,13 @@ packages/RPMs/ports/whatever without going through pip.
Mac OS X
--------
All the necessary packages are available to use glymur with Python 2.6, 2.7,
and 3.3 via MacPorts. For python 3.3, you should install the following set of
ports:
All the necessary packages are available to use glymur with MacPorts.
For python 3.3, you should install the following set of ports:
* python33
* py33-numpy
* py33-lxml
* py33-distribute
* py33-matplotlib (optional, for running certain tests)
* py33-Pillow (optional, for running certain tests)
MacPorts supplies both OpenJPEG 1.5.0 and OpenJPEG 2.0.0.
@ -93,6 +85,7 @@ packages may also need to be installed. Consult your package manager
documentation or use pip.
* setuptools
* python-lxml
* matplotlib
* pillow
* contextlib2 (2.7 only)
@ -108,12 +101,8 @@ difficulties:
Windows
-------
32-bit WinPython 2.7.5 seemed to work with OpenJPEG 1.X, 2.0, and the
development version, but still required contextlib2 and mock to be
installed via pip. WinPython 3.3.2, however, seems to have trouble
with OpenJPEG 2.0, so I would suggest using the development version with
that configuration. I no longer have any access to a windows machine,
so I cannot currently offer much guidance here.
The 0.6.x series of Glymur is untested on windows and I make no promises here.
I suggest that windows users check the 0.5.x series.
'''''''
@ -145,5 +134,5 @@ OpenJPEG counterparts are already failing, and others which do pass but
still produce heaps of output on stderr. Rather than let this swamp
the signal (that most of those tests are actually passing), they've been
filtered out for now. There are also more skipped tests on Python 2.7
than on Python 3.3. The important part is whether or not any test
than on Python3. The important part is whether or not any test
errors are reported at the end.

View file

@ -5,14 +5,15 @@ 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
Printing the Jp2k object should reveal the number of resolutions
(look in the COD segment section of the codestream), but you can
take a shortcut by supplying -1 as the
resolution level. ::
>>> import glymur
>>> file = glymur.data.nemo()
>>> j = glymur.Jp2k(file)
>>> thumbnail = j.read(rlevel=-1)
>>> jp2 = glymur.Jp2k(file)
>>> thumbnail = jp2.read(rlevel=-1)
... display metadata?
=====================
@ -23,16 +24,182 @@ available. ::
From within Python, it is as simple as printing the Jp2k object, i.e. ::
>>> from glymur import Jp2k
>>> import glymur
>>> file = glymur.data.nemo()
>>> j = Jp2k(file)
>>> print(j)
>>> jp2 = glymur.Jp2k(file)
>>> print(jp2)
File: nemo.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: [1456 2592 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
<ns0:xmpmeta xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ns0="adobe:ns:meta/" xmlns:ns2="http://ns.adobe.com/xap/1.0/" xmlns:ns3="http://ns.adobe.com/tiff/1.0/" xmlns:ns4="http://ns.adobe.com/exif/1.0/" xmlns:ns5="http://ns.adobe.com/photoshop/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="Exempi + XMP Core 5.1.2">
<rdf:RDF>
<rdf:Description rdf:about="">
<ns2:CreatorTool>Google</ns2:CreatorTool>
<ns2:CreateDate>2013-02-09T14:47:53</ns2:CreateDate>
</rdf:Description>
<rdf:Description rdf:about="">
<ns3:YCbCrPositioning>1</ns3:YCbCrPositioning>
<ns3:XResolution>72/1</ns3:XResolution>
<ns3:YResolution>72/1</ns3:YResolution>
<ns3:ResolutionUnit>2</ns3:ResolutionUnit>
<ns3:Make>HTC</ns3:Make>
<ns3:Model>HTC Glacier</ns3:Model>
<ns3:ImageWidth>2592</ns3:ImageWidth>
<ns3:ImageLength>1456</ns3:ImageLength>
<ns3:BitsPerSample>
<rdf:Seq>
<rdf:li>8</rdf:li>
<rdf:li>8</rdf:li>
<rdf:li>8</rdf:li>
</rdf:Seq>
</ns3:BitsPerSample>
<ns3:PhotometricInterpretation>2</ns3:PhotometricInterpretation>
<ns3:SamplesPerPixel>3</ns3:SamplesPerPixel>
<ns3:WhitePoint>
<rdf:Seq>
<rdf:li>1343036288/4294967295</rdf:li>
<rdf:li>1413044224/4294967295</rdf:li>
</rdf:Seq>
</ns3:WhitePoint>
<ns3:PrimaryChromaticities>
<rdf:Seq>
<rdf:li>2748779008/4294967295</rdf:li>
<rdf:li>1417339264/4294967295</rdf:li>
<rdf:li>1288490240/4294967295</rdf:li>
<rdf:li>2576980480/4294967295</rdf:li>
<rdf:li>644245120/4294967295</rdf:li>
<rdf:li>257698032/4294967295</rdf:li>
</rdf:Seq>
</ns3:PrimaryChromaticities>
</rdf:Description>
<rdf:Description rdf:about="">
<ns4:ColorSpace>1</ns4:ColorSpace>
<ns4:PixelXDimension>2528</ns4:PixelXDimension>
<ns4:PixelYDimension>1424</ns4:PixelYDimension>
<ns4:FocalLength>353/100</ns4:FocalLength>
<ns4:GPSAltitudeRef>0</ns4:GPSAltitudeRef>
<ns4:GPSAltitude>0/1</ns4:GPSAltitude>
<ns4:GPSMapDatum>WGS-84</ns4:GPSMapDatum>
<ns4:DateTimeOriginal>2013-02-09T14:47:53</ns4:DateTimeOriginal>
<ns4:ISOSpeedRatings>
<rdf:Seq>
<rdf:li>76</rdf:li>
</rdf:Seq>
</ns4:ISOSpeedRatings>
<ns4:ExifVersion>0220</ns4:ExifVersion>
<ns4:FlashpixVersion>0100</ns4:FlashpixVersion>
<ns4:ComponentsConfiguration>
<rdf:Seq>
<rdf:li>1</rdf:li>
<rdf:li>2</rdf:li>
<rdf:li>3</rdf:li>
<rdf:li>0</rdf:li>
</rdf:Seq>
</ns4:ComponentsConfiguration>
<ns4:GPSLatitude>42,20.56N</ns4:GPSLatitude>
<ns4:GPSLongitude>71,5.29W</ns4:GPSLongitude>
<ns4:GPSTimeStamp>2013-02-09T19:47:53Z</ns4:GPSTimeStamp>
<ns4:GPSProcessingMethod>NETWORK</ns4:GPSProcessingMethod>
</rdf:Description>
<rdf:Description rdf:about="">
<ns5:DateCreated>2013-02-09T14:47:53</ns5:DateCreated>
</rdf:Description>
<rdf:Description rdf:about="">
<dc:Creator>
<rdf:Seq>
<rdf:li>Glymur</rdf:li>
<rdf:li>Python XMP Toolkit</rdf:li>
</rdf:Seq>
</dc:Creator>
</rdf:Description>
</rdf:RDF>
</ns0:xmpmeta>
Contiguous Codestream Box (jp2c) @ (3223, 1132296)
Main header:
SOC marker segment @ (3231, 0)
SIZ marker segment @ (3233, 47)
Profile: 2
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
Reference Tile Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Tile Offset: (0 x 0)
Bitdepth: (8, 8, 8)
Signed: (False, False, False)
Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))
COD marker segment @ (3282, 12)
Coding style:
Entropy coder, without partitions
SOP marker segments: False
EPH marker segments: False
Coding style parameters:
Progression order: LRCP
Number of layers: 2
Multiple component transformation usage: reversible
Number of resolutions: 2
Code block height, width: (64 x 64)
Wavelet transform: 5-3 reversible
Precinct size: default, 2^15 x 2^15
Code block context:
Selective arithmetic coding bypass: False
Reset context probabilities on coding pass boundaries: False
Termination on each coding pass: False
Vertically stripe causal context: False
Predictable termination: False
Segmentation symbols: False
QCD marker segment @ (3296, 7)
Quantization style: no quantization, 2 guard bits
Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]
CME marker segment @ (3305, 37)
"Created by OpenJPEG version 2.0.0"
That's fairly overwhelming, and perhaps lost in the flood of information
is the fact that the codestream metadata is limited to just what's in the
main codestream header. You can suppress the codestream and XML details by
making use of the :py:meth:`set_printoptions` function::
This prints the metadata found in the JP2 boxes, but in the case of the
codestream box, only the main header is printed. It is possible to print
**only** the codestream information as well, i.e. ::
>>> glymur.set_printoptions(codestream=False, xml=False)
>>> print(jp2)
File: nemo.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: [1456 2592 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
Contiguous Codestream Box (jp2c) @ (3223, 1132296)
>>> print(j.get_codestream())
It is possible to print all the gory codestream details as well, i.e. ::
>>> print(j.get_codestream()) # details not shown
... add XML metadata?
=====================
@ -55,12 +222,11 @@ Consider the following XML file `data.xml` : ::
</locality>
</info>
The **append** method can add an XML box as shown below::
The :py:meth:`append` method can add an XML box as shown below::
>>> import shutil
>>> import glymur
>>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2')
>>> from xml.etree import cElementTree as ET
>>> jp2 = glymur.Jp2k('myfile.jp2')
>>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml')
>>> jp2.append(xmlbox)
@ -71,9 +237,10 @@ The **append** method can add an XML box as shown below::
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),
you can use the **wrap** method with no box argument: ::
you can use the :py:meth:`wrap` method with no box argument: ::
>>> import glymur
>>> glymur.set_printoptions(codestream=False)
>>> jfile = glymur.data.goodstuff()
>>> j2k = glymur.Jp2k(jfile)
>>> jp2 = j2k.wrap("newfile.jp2")
@ -96,10 +263,6 @@ you can use the **wrap** method with no box argument: ::
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
@ -108,8 +271,9 @@ 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 before the codestream box, we'll need to
re-specify all of the boxes. If you already have a JP2 jacket in place, you can just reuse that,
though. Take the following example content in an XML file `favorites.xml` : ::
re-specify all of the boxes. If you already have a JP2 jacket in place,
you can just reuse that, though. Take the following example content in
an XML file `favorites.xml` : ::
<?xml version="1.0"?>
<favorite_things>
@ -145,17 +309,12 @@ the following will work. ::
<favorite_things>
<category>Light Ale</category>
</favorite_things>
Contiguous Codestream Box (jp2c) @ (153, 115236)
Main header:
.
. (truncated)
.
As to the question of which method you should use, **append** or **wrap**,
to add metadata, you should keep in mind that **wrap** produces a new JP2 file,
while **append** modifies an existing file and is currently limited to XML
boxes.
As to the question of which method you should use, :py:meth:`append` or
:py:meth:`wrap`, to add metadata, you should keep in mind that :py:meth:`wrap`
produces a new JP2 file, while :py:meth:`append` modifies an existing file and
is currently limited to XML and UUID boxes.
... create an image with an alpha layer?
========================================
@ -198,7 +357,7 @@ channel, but we aren't doing that). ::
>>> from glymur.core import RED, GREEN, BLUE, WHOLE_IMAGE
>>> asoc = [RED, GREEN, BLUE, WHOLE_IMAGE]
>>> cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=ctype, association=asoc)
>>> cdef = glymur.jp2box.ChannelDefinitionBox(ctype, asoc)
>>> print(cdef)
Channel Definition Box (cdef) @ (0, 0)
Channel 0 (color) ==> (1)
@ -219,32 +378,210 @@ Here's how the Preview application on the mac shows the RGBA image.
.. image:: goodstuff_alpha.png
work with XMP UUIDs?
====================
... work with XMP UUIDs?
========================
XMP is metadata on steroids.
The example JP2 file shipped with glymur has an XMP UUID. ::
>>> import glymur
>>> j = glymur.Jp2k(glymur.data.nemo())
>>> print(j.box[4])
UUID Box (uuid) @ (715, 2412)
>>> print(j.box[3]) # formatting added to the XML below
<ns0:xmpmeta xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:ns0="adobe:ns:meta/"
xmlns:ns2="http://ns.adobe.com/xap/1.0/"
xmlns:ns3="http://ns.adobe.com/tiff/1.0/"
xmlns:ns4="http://ns.adobe.com/exif/1.0/"
xmlns:ns5="http://ns.adobe.com/photoshop/1.0/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
ns0:xmptk="Exempi + XMP Core 5.1.2">
<rdf:RDF>
<rdf:Description rdf:about="">
<ns2:CreatorTool>Google</ns2:CreatorTool>
<ns2:CreateDate>2013-02-09T14:47:53</ns2:CreateDate>
</rdf:Description>
.
.
.
</ns0:xmpmeta>
Since the UUID data in this case is returned as an lxml ElementTree
instance, one can use lxml to access the data. For example, to
extract the **CreatorTool** attribute value, one could do the
following
>>> xmp = j.box[3].data
>>> rdf = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
>>> ns2 = '{http://ns.adobe.com/xap/1.0/}'
>>> name = '{0}RDF/{0}Description/{1}CreatorTool'.format(rdf, ns2)
>>> elt = xmp.find(name)
>>> elt
<Element '{http://ns.adobe.com/xap/1.0/#}CreatorTool' at 0xb50684a4>
>>> elt.text
'Google'
But that would be painful. A better solution is to install the Python XMP
Toolkit (make sure it is version 2.0)::
>>> from libxmp import XMPMeta
>>> from libxmp.consts import XMP_NS_XMP as NS_XAP
>>> meta = XMPMeta()
>>> meta.parse_from_str(j.box[3].raw_data.decode('utf-8'))
>>> meta.get_property(NS_XAP, 'CreatorTool')
'Google'
Where the Python XMP Toolkit can really shine, though, is when you are
converting an image from another format such as TIFF or JPEG into JPEG 2000.
For example, if you were to be converting the TIFF image found at
http://photojournal.jpl.nasa.gov/tiff/PIA17145.tif info JPEG 2000::
>>> import skimage.io
>>> image = skimage.io.imread('PIA17145.tif')
>>> from glymur import Jp2k
>>> jp2 = Jp2k('PIA17145.jp2', 'wb')
>>> jp2.write(image)
Next you can extract the XMP metadata.
>>> from libxmp import XMPFiles
>>> xf = XMPFiles()
>>> xf.open_file('PIA17145.tif')
>>> xmp = xf.get_xmp()
>>> print(xmp)
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Exempi + XMP Core 5.1.2">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:tiff="http://ns.adobe.com/tiff/1.0/">
<tiff:ImageWidth>1016</tiff:ImageWidth>
<tiff:ImageLength>1016</tiff:ImageLength>
<tiff:BitsPerSample>
<rdf:Seq>
<rdf:li>8</rdf:li>
</rdf:Seq>
</tiff:BitsPerSample>
<tiff:Compression>1</tiff:Compression>
<tiff:PhotometricInterpretation>1</tiff:PhotometricInterpretation>
<tiff:SamplesPerPixel>1</tiff:SamplesPerPixel>
<tiff:PlanarConfiguration>1</tiff:PlanarConfiguration>
<tiff:ResolutionUnit>2</tiff:ResolutionUnit>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">converted PNM file</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
If you are familiar with TIFF, you can verify that there's no XMP tag in the
TIFF file, but the Python XMP Toolkit takes advantage of the TIFF header
structure to populate an XMP packet for you. If you were working with a JPEG
file with Exif metadata, that information would be included in the XMP packet
as well. Now you can append the XMP packet in a UUIDBox. In order to do this,
though, you have to know the UUID that signifies XMP data.::
>>> import uuid
>>> xmp_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
>>> box = glymur.jp2box.UUIDBox(xmp_uuid, str(xmp).encode())
>>> jp2.append(box)
>>> print(jp2.box[-1])
UUID Box (uuid) @ (592316, 1053)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
<ns0:xmpmeta xmlns:ns0="adobe:ns:meta/" xmlns:ns2="http://ns.adobe.com/xap/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="XMP Core 4.4.0-Exiv2">
<ns0:xmpmeta xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ns0="adobe:ns:meta/" xmlns:ns2="http://ns.adobe.com/tiff/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="Exempi + XMP Core 5.1.2">
<rdf:RDF>
<rdf:Description ns2:CreatorTool="glymur" rdf:about="" />
<rdf:Description rdf:about="">
<ns2:ImageWidth>1016</ns2:ImageWidth>
<ns2:ImageLength>1016</ns2:ImageLength>
<ns2:BitsPerSample>
<rdf:Seq>
<rdf:li>8</rdf:li>
</rdf:Seq>
</ns2:BitsPerSample>
<ns2:Compression>1</ns2:Compression>
<ns2:PhotometricInterpretation>1</ns2:PhotometricInterpretation>
<ns2:SamplesPerPixel>1</ns2:SamplesPerPixel>
<ns2:PlanarConfiguration>1</ns2:PlanarConfiguration>
<ns2:ResolutionUnit>2</ns2:ResolutionUnit>
</rdf:Description>
<rdf:Description rdf:about="">
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">converted PNM file</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>
</rdf:RDF>
</ns0:xmpmeta>
Since the UUID data in this case is returned as an ElementTree instance, one can
use ElementTree to access the data. For example, to extract the
**CreatorTool** attribute value, the following would work::
You can also build up XMP metadata from scratch. For instance, if we try to
wrap `goodstuff.j2k` again::
>>> import glymur
>>> jfile = glymur.data.goodstuff()
>>> j2k = glymur.Jp2k(jfile)
>>> jp2 = j2k.wrap("goodstuff.jp2")
Now build up the metadata piece-by-piece. It would help to have the XMP
standard close at hand::
>>> from libxmp import XMPMeta
>>> from libxmp.consts import XMP_NS_TIFF as NS_TIFF
>>> from libxmp.consts import XMP_NS_DC as NS_DC
>>> xmp = XMPMeta()
>>> ihdr = jp2.box[2].box[0]
>>> xmp.set_property(NS_TIFF, "ImageWidth", str(ihdr.width))
>>> xmp.set_property(NS_TIFF, "ImageHeight", str(ihdr.height))
>>> xmp.set_property(NS_TIFF, "BitsPerSample", '3')
>>> xmp.set_property(NS_DC, "Title", u'Stürm und Drang')
>>> xmp.set_property(NS_DC, "Creator", 'Glymur')
We can then append the XMP in a UUID box just as before::
>>> import uuid
>>> xmp_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
>>> box = glymur.jp2box.UUIDBox(xmp_uuid, str(xmp).encode())
>>> jp2.append(box)
>>> glymur.set_printoptions(codestream=False)
>>> print(jp2)
File: goodstuff.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)
UUID Box (uuid) @ (115305, 671)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
<ns0:xmpmeta xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ns0="adobe:ns:meta/" xmlns:ns2="http://ns.adobe.com/tiff/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="Exempi + XMP Core 5.1.2">
<rdf:RDF>
<rdf:Description rdf:about="">
<ns2:ImageWidth>480</ns2:ImageWidth>
<ns2:ImageHeight>800</ns2:ImageHeight>
<ns2:BitsPerSample>3</ns2:BitsPerSample>
</rdf:Description>
<rdf:Description rdf:about="">
<dc:Title>Stürm und Drang</dc:Title>
<dc:Creator>Glymur</dc:Creator>
</rdf:Description>
</rdf:RDF>
</ns0:xmpmeta>
>>> xmp = j.box[4].data
>>> ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
>>> ns1 = '{http://ns.adobe.com/xap/1.0/}'
>>> name = '{0}RDF/{0}Description'.format(ns0)
>>> elt = xmp.find(name)
>>> elt
<Element '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description' at 0xb4baa93c>
>>> elt.attrib['{0}CreatorTool'.format(ns1)]
'glymur'

View file

@ -16,6 +16,7 @@ Contents:
detailed_installation
how_do_i
roadmap
changelog
api
------------------

View file

@ -7,28 +7,24 @@ which allows one to read and write JPEG 2000 files from within Python.
Glymur supports both reading and writing of JPEG 2000 images, but writing
JPEG 2000 images is currently limited to images that can fit in memory
Of particular focus is retrieval of metadata. Reading Exif UUIDs is supported,
as is reading XMP UUIDs as the XMP data packet is just XML. There is
some very limited support for reading JPX metadata. For instance,
**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can
be retrieved from such JPX files.
In regards to metadata, most JP2 boxes are properly interpreted.
Certain optional JP2 boxes can also be written, including XML boxes and
XMP UUIDs. There is some very limited support for reading JPX metadata.
Glymur 0.6 works on Python versions 2.7, 3.3 and 3.4. If you have 2.6, you should
use the 0.5 series.
Glymur 0.6 works on Python versions 2.7, 3.3 and 3.4. If you have Python 2.6,
you should use the 0.5 series of Glymur.
OpenJPEG Installation
=====================
Glymur will read JPEG 2000 images with versions 1.3, 1.4, 1.5, 2.0,
and the trunk/development version of OpenJPEG. Writing images is
only supported with the 1.5 or better, however, and the trunk/development
version is strongly recommended. For more information about OpenJPEG,
please consult http://www.openjpeg.org.
version of OpenJPEG is strongly recommended. For more information about
OpenJPEG, please consult http://www.openjpeg.org.
If you use MacPorts or if you have a sufficiently recent version of
Linux, your package manager should already provide you with a version of
OpenJPEG 1.X which glymur can already use. If your platform is windows,
I suggest using the windows installers provided to you by the OpenJPEG
folks at https://code.google.com/p/openjpeg/downloads/list .
OpenJPEG 1.X which glymur can already use.
Glymur Installation
===================

View file

@ -5,6 +5,4 @@ Roadmap
Here's an incomplete list of what I'd like to focus on in the future.
* continue to monitor upstream changes in the openjp2 library
* investigate using CFFI or cython instead of ctypes to wrap openjp2
* investigate adding write support for UUID/XMP boxes (potentially a big project)
* investigate JPIP (likely to be an even bigger project)
* investigate JPIP (likely to be a big project)

View file

@ -8,6 +8,7 @@ __version__ = version.version
from .jp2k import Jp2k
from .jp2dump import jp2dump
from .jp2box import get_printoptions, set_printoptions
from . import data

507
glymur/_uuid_io.py Normal file
View file

@ -0,0 +1,507 @@
# -*- coding: utf-8 -*-
"""
Part of glymur.
"""
import pprint
import re
import struct
import sys
import warnings
import lxml.etree as ET
if sys.hexversion < 0x02070000:
# pylint: disable=F0401,E0611
from ordereddict import OrderedDict
else:
from collections import OrderedDict
def xml(raw_data):
"""
XMP data to be parsed as XML.
"""
if sys.hexversion < 0x03000000:
elt = ET.fromstring(raw_data)
else:
text = raw_data.decode('utf-8')
elt = ET.fromstring(text)
return ET.ElementTree(elt)
def tiff_header(read_buffer):
"""
Interpret the uuid raw data as a tiff header.
"""
# Ignore the first six bytes.
# Next 8 should be (73, 73, 42, 8) or (77, 77, 42, 8)
data = struct.unpack('<BB', read_buffer[6:8])
if data[0] == 73 and data[1] == 73:
# little endian
endian = '<'
elif data[0] == 77 and data[1] == 77:
# big endian
endian = '>'
else:
msg = "Bad byte order indication: {0}".format(read_buffer[6:8])
raise RuntimeError(msg)
_, offset = struct.unpack(endian + 'HI', read_buffer[8:14])
# This is the 'Exif Image' portion.
exif = _ExifImageIfd(endian, read_buffer[6:], offset)
return exif.processed_ifd
class _Ifd(object):
"""
Attributes
----------
read_buffer : bytes
Raw byte stream consisting of the UUID data.
datatype2fmt : dictionary
Class attribute, maps the TIFF enumerated datatype to the python
datatype and data width.
endian : str
Either '<' for big-endian, or '>' for little-endian.
num_tags : int
Number of tags in the IFD.
raw_ifd : dictionary
Maps tag number to "mildly-interpreted" tag value.
processed_ifd : dictionary
Maps tag name to "mildly-interpreted" tag value.
"""
datatype2fmt = {1: ('B', 1),
2: ('B', 1),
3: ('H', 2),
4: ('I', 4),
5: ('II', 8),
7: ('B', 1),
9: ('i', 4),
10: ('ii', 8)}
def __init__(self, endian, read_buffer, offset):
self.endian = endian
self.read_buffer = read_buffer
self.processed_ifd = OrderedDict()
self.num_tags, = struct.unpack(endian + 'H',
read_buffer[offset:offset + 2])
fmt = self.endian + 'HHII' * self.num_tags
ifd_buffer = read_buffer[offset + 2:offset + 2 + self.num_tags * 12]
data = struct.unpack(fmt, ifd_buffer)
self.raw_ifd = OrderedDict()
for j, tag in enumerate(data[0::4]):
# The offset to the tag offset/payload is the offset to the IFD
# plus 2 bytes for the number of tags plus 12 bytes for each
# tag entry plus 8 bytes to the offset/payload itself.
toffp = read_buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4]
tag_data = self.parse_tag(data[j * 4 + 1],
data[j * 4 + 2],
toffp)
self.raw_ifd[tag] = tag_data
def parse_tag(self, dtype, count, offset_buf):
"""Interpret an Exif image tag data payload.
"""
fmt = self.datatype2fmt[dtype][0] * count
payload_size = self.datatype2fmt[dtype][1] * count
if payload_size <= 4:
# Interpret the payload from the 4 bytes in the tag entry.
target_buffer = offset_buf[:payload_size]
else:
# Interpret the payload at the offset specified by the 4 bytes in
# the tag entry.
offset, = struct.unpack(self.endian + 'I', offset_buf)
target_buffer = self.read_buffer[offset:offset + payload_size]
if dtype == 2:
# ASCII
if sys.hexversion < 0x03000000:
payload = target_buffer.rstrip('\x00')
else:
payload = target_buffer.decode('utf-8').rstrip('\x00')
else:
payload = struct.unpack(self.endian + fmt, target_buffer)
if dtype == 5 or dtype == 10:
# Rational or Signed Rational. Construct the list of values.
rational_payload = []
for j in range(count):
value = float(payload[j * 2]) / float(payload[j * 2 + 1])
rational_payload.append(value)
payload = rational_payload
if count == 1:
# If just a single value, then return a scalar instead of a
# tuple.
payload = payload[0]
return payload
def post_process(self, tagnum2name):
"""Map the tag name instead of tag number to the tag value.
"""
for tag, value in self.raw_ifd.items():
try:
tag_name = tagnum2name[tag]
except KeyError:
# Ok, we don't recognize this tag. Just use the numeric id.
msg = 'Unrecognized Exif tag: {0}'.format(tag)
warnings.warn(msg, UserWarning)
tag_name = tag
self.processed_ifd[tag_name] = value
class _ExifImageIfd(_Ifd):
"""
Attributes
----------
tagnum2name : dict
Maps Exif image tag numbers to the tag names.
ifd : dict
Maps tag names to tag values.
"""
tagnum2name = {11: 'ProcessingSoftware',
254: 'NewSubfileType',
255: 'SubfileType',
256: 'ImageWidth',
257: 'ImageLength',
258: 'BitsPerSample',
259: 'Compression',
262: 'PhotometricInterpretation',
263: 'Threshholding',
264: 'CellWidth',
265: 'CellLength',
266: 'FillOrder',
269: 'DocumentName',
270: 'ImageDescription',
271: 'Make',
272: 'Model',
273: 'StripOffsets',
274: 'Orientation',
277: 'SamplesPerPixel',
278: 'RowsPerStrip',
279: 'StripByteCounts',
282: 'XResolution',
283: 'YResolution',
284: 'PlanarConfiguration',
290: 'GrayResponseUnit',
291: 'GrayResponseCurve',
292: 'T4Options',
293: 'T6Options',
296: 'ResolutionUnit',
301: 'TransferFunction',
305: 'Software',
306: 'DateTime',
315: 'Artist',
316: 'HostComputer',
317: 'Predictor',
318: 'WhitePoint',
319: 'PrimaryChromaticities',
320: 'ColorMap',
321: 'HalftoneHints',
322: 'TileWidth',
323: 'TileLength',
324: 'TileOffsets',
325: 'TileByteCounts',
330: 'SubIFDs',
332: 'InkSet',
333: 'InkNames',
334: 'NumberOfInks',
336: 'DotRange',
337: 'TargetPrinter',
338: 'ExtraSamples',
339: 'SampleFormat',
340: 'SMinSampleValue',
341: 'SMaxSampleValue',
342: 'TransferRange',
343: 'ClipPath',
344: 'XClipPathUnits',
345: 'YClipPathUnits',
346: 'Indexed',
347: 'JPEGTables',
351: 'OPIProxy',
512: 'JPEGProc',
513: 'JPEGInterchangeFormat',
514: 'JPEGInterchangeFormatLength',
515: 'JPEGRestartInterval',
517: 'JPEGLosslessPredictors',
518: 'JPEGPointTransforms',
519: 'JPEGQTables',
520: 'JPEGDCTables',
521: 'JPEGACTables',
529: 'YCbCrCoefficients',
530: 'YCbCrSubSampling',
531: 'YCbCrPositioning',
532: 'ReferenceBlackWhite',
700: 'XMLPacket',
18246: 'Rating',
18249: 'RatingPercent',
32781: 'ImageID',
33421: 'CFARepeatPatternDim',
33422: 'CFAPattern',
33423: 'BatteryLevel',
33432: 'Copyright',
33434: 'ExposureTime',
33437: 'FNumber',
33723: 'IPTCNAA',
34377: 'ImageResources',
34665: 'ExifTag',
34675: 'InterColorProfile',
34850: 'ExposureProgram',
34852: 'SpectralSensitivity',
34853: 'GPSTag',
34855: 'ISOSpeedRatings',
34856: 'OECF',
34857: 'Interlace',
34858: 'TimeZoneOffset',
34859: 'SelfTimerMode',
36867: 'DateTimeOriginal',
37122: 'CompressedBitsPerPixel',
37377: 'ShutterSpeedValue',
37378: 'ApertureValue',
37379: 'BrightnessValue',
37380: 'ExposureBiasValue',
37381: 'MaxApertureValue',
37382: 'SubjectDistance',
37383: 'MeteringMode',
37384: 'LightSource',
37385: 'Flash',
37386: 'FocalLength',
37387: 'FlashEnergy',
37388: 'SpatialFrequencyResponse',
37389: 'Noise',
37390: 'FocalPlaneXResolution',
37391: 'FocalPlaneYResolution',
37392: 'FocalPlaneResolutionUnit',
37393: 'ImageNumber',
37394: 'SecurityClassification',
37395: 'ImageHistory',
37396: 'SubjectLocation',
37397: 'ExposureIndex',
37398: 'TIFFEPStandardID',
37399: 'SensingMethod',
40091: 'XPTitle',
40092: 'XPComment',
40093: 'XPAuthor',
40094: 'XPKeywords',
40095: 'XPSubject',
50341: 'PrintImageMatching',
50706: 'DNGVersion',
50707: 'DNGBackwardVersion',
50708: 'UniqueCameraModel',
50709: 'LocalizedCameraModel',
50710: 'CFAPlaneColor',
50711: 'CFALayout',
50712: 'LinearizationTable',
50713: 'BlackLevelRepeatDim',
50714: 'BlackLevel',
50715: 'BlackLevelDeltaH',
50716: 'BlackLevelDeltaV',
50717: 'WhiteLevel',
50718: 'DefaultScale',
50719: 'DefaultCropOrigin',
50720: 'DefaultCropSize',
50721: 'ColorMatrix1',
50722: 'ColorMatrix2',
50723: 'CameraCalibration1',
50724: 'CameraCalibration2',
50725: 'ReductionMatrix1',
50726: 'ReductionMatrix2',
50727: 'AnalogBalance',
50728: 'AsShotNeutral',
50729: 'AsShotWhiteXY',
50730: 'BaselineExposure',
50731: 'BaselineNoise',
50732: 'BaselineSharpness',
50733: 'BayerGreenSplit',
50734: 'LinearResponseLimit',
50735: 'CameraSerialNumber',
50736: 'LensInfo',
50737: 'ChromaBlurRadius',
50738: 'AntiAliasStrength',
50739: 'ShadowScale',
50740: 'DNGPrivateData',
50741: 'MakerNoteSafety',
50778: 'CalibrationIlluminant1',
50779: 'CalibrationIlluminant2',
50780: 'BestQualityScale',
50781: 'RawDataUniqueID',
50827: 'OriginalRawFileName',
50828: 'OriginalRawFileData',
50829: 'ActiveArea',
50830: 'MaskedAreas',
50831: 'AsShotICCProfile',
50832: 'AsShotPreProfileMatrix',
50833: 'CurrentICCProfile',
50834: 'CurrentPreProfileMatrix',
50879: 'ColorimetricReference',
50931: 'CameraCalibrationSignature',
50932: 'ProfileCalibrationSignature',
50934: 'AsShotProfileName',
50935: 'NoiseReductionApplied',
50936: 'ProfileName',
50937: 'ProfileHueSatMapDims',
50938: 'ProfileHueSatMapData1',
50939: 'ProfileHueSatMapData2',
50940: 'ProfileToneCurve',
50941: 'ProfileEmbedPolicy',
50942: 'ProfileCopyright',
50964: 'ForwardMatrix1',
50965: 'ForwardMatrix2',
50966: 'PreviewApplicationName',
50967: 'PreviewApplicationVersion',
50968: 'PreviewSettingsName',
50969: 'PreviewSettingsDigest',
50970: 'PreviewColorSpace',
50971: 'PreviewDateTime',
50972: 'RawImageDigest',
50973: 'OriginalRawFileDigest',
50974: 'SubTileBlockSize',
50975: 'RowInterleaveFactor',
50981: 'ProfileLookTableDims',
50982: 'ProfileLookTableData',
51008: 'OpcodeList1',
51009: 'OpcodeList2',
51022: 'OpcodeList3',
51041: 'NoiseProfile'}
def __init__(self, endian, read_buffer, offset):
_Ifd.__init__(self, endian, read_buffer, offset)
self.post_process(self.tagnum2name)
class _ExifPhotoIfd(_Ifd):
"""Represents tags found in the Exif sub ifd.
"""
tagnum2name = {33434: 'ExposureTime',
33437: 'FNumber',
34850: 'ExposureProgram',
34852: 'SpectralSensitivity',
34855: 'ISOSpeedRatings',
34856: 'OECF',
34864: 'SensitivityType',
34865: 'StandardOutputSensitivity',
34866: 'RecommendedExposureIndex',
34867: 'ISOSpeed',
34868: 'ISOSpeedLatitudeyyy',
34869: 'ISOSpeedLatitudezzz',
36864: 'ExifVersion',
36867: 'DateTimeOriginal',
36868: 'DateTimeDigitized',
37121: 'ComponentsConfiguration',
37122: 'CompressedBitsPerPixel',
37377: 'ShutterSpeedValue',
37378: 'ApertureValue',
37379: 'BrightnessValue',
37380: 'ExposureBiasValue',
37381: 'MaxApertureValue',
37382: 'SubjectDistance',
37383: 'MeteringMode',
37384: 'LightSource',
37385: 'Flash',
37386: 'FocalLength',
37396: 'SubjectArea',
37500: 'MakerNote',
37510: 'UserComment',
37520: 'SubSecTime',
37521: 'SubSecTimeOriginal',
37522: 'SubSecTimeDigitized',
40960: 'FlashpixVersion',
40961: 'ColorSpace',
40962: 'PixelXDimension',
40963: 'PixelYDimension',
40964: 'RelatedSoundFile',
40965: 'InteroperabilityTag',
41483: 'FlashEnergy',
41484: 'SpatialFrequencyResponse',
41486: 'FocalPlaneXResolution',
41487: 'FocalPlaneYResolution',
41488: 'FocalPlaneResolutionUnit',
41492: 'SubjectLocation',
41493: 'ExposureIndex',
41495: 'SensingMethod',
41728: 'FileSource',
41729: 'SceneType',
41730: 'CFAPattern',
41985: 'CustomRendered',
41986: 'ExposureMode',
41987: 'WhiteBalance',
41988: 'DigitalZoomRatio',
41989: 'FocalLengthIn35mmFilm',
41990: 'SceneCaptureType',
41991: 'GainControl',
41992: 'Contrast',
41993: 'Saturation',
41994: 'Sharpness',
41995: 'DeviceSettingDescription',
41996: 'SubjectDistanceRange',
42016: 'ImageUniqueID',
42032: 'CameraOwnerName',
42033: 'BodySerialNumber',
42034: 'LensSpecification',
42035: 'LensMake',
42036: 'LensModel',
42037: 'LensSerialNumber'}
def __init__(self, endian, read_buffer, offset):
_Ifd.__init__(self, endian, read_buffer, offset)
self.post_process(self.tagnum2name)
class _ExifGPSInfoIfd(_Ifd):
"""Represents information found in the GPSInfo sub IFD.
"""
tagnum2name = {0: 'GPSVersionID',
1: 'GPSLatitudeRef',
2: 'GPSLatitude',
3: 'GPSLongitudeRef',
4: 'GPSLongitude',
5: 'GPSAltitudeRef',
6: 'GPSAltitude',
7: 'GPSTimeStamp',
8: 'GPSSatellites',
9: 'GPSStatus',
10: 'GPSMeasureMode',
11: 'GPSDOP',
12: 'GPSSpeedRef',
13: 'GPSSpeed',
14: 'GPSTrackRef',
15: 'GPSTrack',
16: 'GPSImgDirectionRef',
17: 'GPSImgDirection',
18: 'GPSMapDatum',
19: 'GPSDestLatitudeRef',
20: 'GPSDestLatitude',
21: 'GPSDestLongitudeRef',
22: 'GPSDestLongitude',
23: 'GPSDestBearingRef',
24: 'GPSDestBearing',
25: 'GPSDestDistanceRef',
26: 'GPSDestDistance',
27: 'GPSProcessingMethod',
28: 'GPSAreaInformation',
29: 'GPSDateStamp',
30: 'GPSDifferential'}
def __init__(self, endian, read_buffer, offset):
_Ifd.__init__(self, endian, read_buffer, offset)
self.post_process(self.tagnum2name)
class _ExifInteroperabilityIfd(_Ifd):
"""Represents tags found in the Interoperability sub IFD.
"""
tagnum2name = {1: 'InteroperabilityIndex',
2: 'InteroperabilityVersion',
4096: 'RelatedImageFileFormat',
4097: 'RelatedImageWidth',
4098: 'RelatedImageLength'}
def __init__(self, endian, read_buffer, offset):
_Ifd.__init__(self, endian, read_buffer, offset)
self.post_process(self.tagnum2name)

View file

@ -663,7 +663,6 @@ class Codestream(object):
bitdepth = tuple(((x & 0x7f) + 1) for x in data[0::3])
signed = tuple(((x & 0x80) > 0) for x in data[0::3])
xrsiz = data[1::3]
yrsiz = data[2::3]
@ -1538,7 +1537,7 @@ class SIZsegment(Segment):
signed=self.signed,
xyrsiz=(self.xrsiz, self.yrsiz))
return msg
def __str__(self):
msg = Segment.__str__(self)
msg += '\n '

View file

@ -1,5 +1,8 @@
"""Core definitions to be shared amongst the modules.
"""
import copy
import lxml.etree as ET
# Progression order
LRCP = 0
RLCP = 1
@ -7,6 +10,26 @@ RPCL = 2
PCRL = 3
CPRL = 4
STD = 0
CINEMA2K = 3
CINEMA4K = 4
RSIZ = {
'STD': STD,
'CINEMA2K': CINEMA2K,
'CINEMA4K': CINEMA4K}
OFF = 0
CINEMA2K_24 = 1
CINEMA2K_48 = 2
CINEMA4K_24 = 3
CINEMA_MODE = {
'off': OFF,
'cinema2k_24': CINEMA2K_24,
'cinema2k_48': CINEMA2K_48,
'cinema4k_24': CINEMA4K_24, }
PROGRESSION_ORDER = {
'LRCP': LRCP,
'RLCP': RLCP,

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -16,17 +16,19 @@ if sys.hexversion >= 0x03030000:
else:
from contextlib2 import ExitStack
from collections import Counter
import ctypes
import math
import os
import struct
from uuid import UUID
import warnings
import numpy as np
from .codestream import Codestream
from .core import SRGB, GREYSCALE
from .core import PROGRESSION_ORDER
from .core import PROGRESSION_ORDER, RSIZ, CINEMA_MODE
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
from .jp2box import Jp2kBox
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
@ -37,6 +39,16 @@ from .lib import openjp2 as opj2
from . import version
from .lib import c as libc
# Codestream lengths for 24fps, 48fps
CINEMA_24_CS = 1302083
CINEMA_48_CS = 651041
# Maximum size per color components for 2K and 4K at 24 fps
COMP_24_CS = 1041666
# Maximum size per color components for 2K at 48 fps
COMP_48_CS = 520833
JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
'uuid']
@ -151,6 +163,111 @@ class Jp2k(Jp2kBox):
msg += "profile if the file type box brand is 'jp2 '."
warnings.warn(msg)
def _set_cinema_params(self, cparams, cinema_mode, fps):
"""Populate compression parameters structure for cinema2K.
Parameters
----------
params : ctypes struct
Corresponds to compression parameters structure used by the
library.
cinema_mode : str
Either 'cinema2k' or 'cinema4k'
fps : int
Frames per second, should be either 24 or 48.
"""
if cinema_mode == 'cinema2k':
if fps == 24:
cparams.cp_cinema = CINEMA_MODE['cinema2k_24']
elif fps == 48:
cparams.cp_cinema = CINEMA_MODE['cinema2k_48']
else:
raise IOError('Cinema2K frame rate must be either 24 or 48.')
cparams.cp_rsiz = RSIZ['CINEMA2K']
else:
cparams.cp_cinema = CINEMA_MODE['cinema4k_24']
cparams.cp_rsiz = RSIZ['CINEMA4K']
# No tiling
cparams.tile_size_on = opj2.FALSE
cparams.cp_tdx = 1
cparams.cp_tdy = 1
# One tile part for each component.
cparams.tp_flag = ord('C')
cparams.tp_on = 1
# tile and image shall be as (0,0)
cparams.cp_tx0 = 0
cparams.cp_ty0 = 0
cparams.image_offset_x0 = 0
cparams.image_offset_y0 = 0
# Codeblock size = 32 * 32
cparams.cblockw_init = 32
cparams.cblockh_init = 32
# code block style, no mode switch enabled.
cparams.mode = 0
# no ROI
cparams.roi_compno = -1
# no subsampling
cparams.subsampling_dx = 1
cparams.subsampling_dy = 1
# 9-7 transform
cparams.irreversible = 1
# number of layers
if cparams.tcp_numlayers > 1:
# TODO: warning or error
cparams.tcp_numlayers = 1
if cinema_mode == 'cinema2k':
if cparams.numresolution > 6:
# TODO: warning or error
cparams.numresolution = 6
else:
if cparams.numresolution < 2:
# TODO: warning or error
cparams.numresolution = 1
elif cparams.numresolution > 7:
cparams.numresolution = 7
# precincts
cparams.csty |= 0x01
cparams.res_spec = cparams.numresolution - 1
for j in range(cparams.res_spec):
cparams.prcw_init[j] = 256
cparams.prch_init[j] = 256
# Progression order shall be CPRL
cparams.prog_order = PROGRESSION_ORDER['CPRL']
# progression order changes not allowed for 2K
if cinema_mode == 'cinema2k':
cparams.numpocs = 0
else:
cparams.poc[0].tile = 1
cparams.poc[0].resno0 = 0
cparams.poc[0].compno0 = 0
cparams.poc[0].layno1 = 1
cparams.poc[0].resno1 = cparams.numresolution - 1
cparams.poc[0].compno1 = 3
cparams.poc[0].prg1 = PROGRESSION_ORDER['CPRL']
cparams.poc[1].tile = 1
cparams.poc[1].resno0 = 0
cparams.poc[1].compno0 = 0
cparams.poc[1].layno1 = 1
cparams.poc[1].resno1 = cparams.numresolution
cparams.poc[1].compno1 = 3
cparams.poc[1].prg1 = PROGRESSION_ORDER['CPRL']
cparams.numpocs = 2
def _populate_cparams(self, **kwargs):
"""Populate compression parameters structure from input arguments.
@ -217,6 +334,14 @@ class Jp2k(Jp2kBox):
cparams.tcp_numlayers = 1
cparams.cp_disto_alloc = 1
if 'cinema2k' in kwargs:
self._set_cinema_params(cparams, 'cinema2k', kwargs['cinema2k'])
return cparams
if 'cinema4k' in kwargs:
self._set_cinema_params(cparams, 'cinema4k', kwargs['cinema4k'])
return cparams
if 'cbsize' in kwargs:
cparams.cblockw_init = kwargs['cbsize'][1]
cparams.cblockh_init = kwargs['cbsize'][0]
@ -338,6 +463,8 @@ class Jp2k(Jp2kBox):
Image data to be written to file.
cbsize : tuple, optional
Code block size (DY, DX).
cinema2k : int, optional
frames per second, either 24 or 48
colorspace : str, optional
Either 'rgb' or 'gray'.
cratios : iterable
@ -469,9 +596,51 @@ class Jp2k(Jp2kBox):
self.parse()
def _set_cinema_rate(self, cparams, image):
max_rate = 0
temp_rate = 0
cparams.cp_disto_alloc = 1
num_pixels = image.contents.comps[0].w * image.contents.comps[0].h
num_samples = num_pixels * image.contents.numcomps
rate_numerator = num_samples * image.contents.comps[0].prec
rate_denominator = 8 * image.contents.comps[0].dx
rate_denominator *= image.contents.comps[0].dy
if cparams.cp_cinema in [CINEMA_MODE['cinema2k_24'],
CINEMA_MODE['cinema4k_24']]:
max_rate = rate_numerator / (rate_denominator * CINEMA_24_CS)
if cparams.tcp_rates[0] == 0:
cparams.tcp_rates[0] = max_rate
else:
temp_rate = rate_numerator / (cparams.tcp_rates[0] * 8 * num_pixels)
if temp_rate > CINEMA_24_CS:
# TODO warning, reset
cparams.tcp_rates[0] = max_rate
else:
# TODO warning
pass
cparams.max_comp_size = COMP_24_CS
else:
max_rate = rate_numerator / (rate_denominator * CINEMA_48_CS)
if cparams.tcp_rates[0] == 0:
cparams.tcp_rates[0] = max_rate
else:
temp_rate = rate_numerator / (cparams.tcp_rates[0] * 8 * num_pixels)
if temp_rate > CINEMA_48_CS:
# TODO warning, reset
cparams.tcp_rates[0] = max_rate
else:
# TODO warning
pass
cparams.max_comp_size = COMP_48_CS
def _write_openjp2(self, img_array, verbose=False, **kwargs):
"""
Write JPEG 2000 file using OpenJPEG 1.5 interface.
Write JPEG 2000 file using OpenJPEG 2.0 interface.
"""
cparams, colorspace = self._process_write_inputs(img_array, **kwargs)
@ -487,17 +656,22 @@ class Jp2k(Jp2kBox):
stack.callback(opj2.image_destroy, image)
_populate_image_struct(cparams, image, img_array)
if 'cinema2k' in kwargs:
self._set_cinema_rate(cparams, image)
if 'cinema4k' in kwargs:
self._set_cinema_rate(cparams, image)
codec = opj2.create_compress(cparams.codec_fmt)
stack.callback(opj2.destroy_codec, codec)
info_handler = _INFO_CALLBACK if verbose else None
opj2.set_info_handler(codec, info_handler)
opj2.set_warning_handler(codec, _WARNING_CALLBACK)
opj2.set_error_handler(codec, _ERROR_CALLBACK)
opj2.setup_encoder(codec, cparams, image)
if _OPENJP2_IS_OFFICIAL_V2:
fptr = libc.fopen(self.filename, 'wb')
strm = opj2.stream_create_default_file_stream(fptr, False)
@ -508,11 +682,11 @@ class Jp2k(Jp2kBox):
strm = opj2.stream_create_default_file_stream_v3(self.filename,
False)
stack.callback(opj2.stream_destroy_v3, strm)
opj2.start_compress(codec, image, strm)
opj2.encode(codec, strm)
opj2.end_compress(codec, strm)
# Refresh the metadata.
self.parse()
@ -522,14 +696,18 @@ class Jp2k(Jp2kBox):
Parameters
----------
box : Jp2Box
Instance of a JP2 box. Currently only XML boxes are allowed.
Instance of a JP2 box. Only UUID and XML boxes can currently be
appended.
"""
if self._codec_format == opj2.CODEC_J2K:
msg = "Only JP2 files can currently have boxes appended to them."
raise IOError(msg)
if box.box_id != 'xml ':
raise IOError("Only XML boxes can currently be appended.")
if not ((box.box_id == 'xml ') or
(box.box_id == 'uuid' and
box.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'))):
msg = "Only XML boxes and XMP UUID boxes can currently be appended."
raise IOError(msg)
# Check the last box. If the length field is zero, then rewrite
# the length field to reflect the true length of the box.
@ -1018,7 +1196,7 @@ class Jp2k(Jp2kBox):
>>> jp2 = glymur.Jp2k(jfile)
>>> codestream = jp2.get_codestream()
>>> print(codestream.segment[1])
SIZ marker segment @ (3137, 47)
SIZ marker segment @ (3233, 47)
Profile: 2
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
@ -1104,11 +1282,38 @@ def _validate_nonzero_image_size(nrows, ncols, component_index):
raise IOError(msg)
JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
'uuid']
def _validate_jp2_box_sequence(boxes):
"""Run through series of tests for JP2 box legality.
This is non-exhaustive.
"""
_validate_signature_compatibility(boxes)
_validate_jp2h(boxes)
_validate_jp2c(boxes)
if boxes[1].brand == 'jpx ':
_validate_jpx_box_sequence(boxes)
else:
count = _collect_box_count(boxes)
for id in count.keys():
if id not in JP2_IDS:
msg = "The presence of a '{0}' box requires that the file type "
msg += "brand be set to 'jpx '."
raise IOError(msg.format(id))
def _validate_jpx_box_sequence(boxes):
"""Run through series of tests for JPX box legality."""
_validate_label(boxes)
_validate_jpx_brand(boxes, boxes[1].brand)
_validate_jpx_compatibility(boxes, boxes[1].compatibility_list)
_validate_singletons(boxes)
_validate_top_level(boxes)
def _validate_signature_compatibility(boxes):
"""Validate the file signature and compatibility status."""
# Check for a bad sequence of boxes.
# 1st two boxes must be 'jP ' and 'ftyp'
if boxes[0].box_id != 'jP ' or boxes[1].box_id != 'ftyp':
@ -1116,6 +1321,14 @@ def _validate_jp2_box_sequence(boxes):
msg += "must be the file type box."
raise IOError(msg)
# The compatibility list must contain at a minimum 'jp2 '.
if 'jp2 ' not in boxes[1].compatibility_list:
msg = "The ftyp box must contain 'jp2 ' in the compatibility list."
raise IOError(msg)
def _validate_jp2c(boxes):
"""Validate the codestream box in relation to other boxes."""
# jp2c must be preceeded by jp2h
jp2h_lst = [idx for (idx, box) in enumerate(boxes)
if box.box_id == 'jp2h']
@ -1132,8 +1345,20 @@ def _validate_jp2_box_sequence(boxes):
msg = "The codestream box must be preceeded by a jp2 header box."
raise IOError(msg)
def _validate_jp2h(boxes):
"""Validate the JP2 Header box."""
_check_jp2h_child_boxes(boxes, 'top-level')
jp2h_lst = [box for box in boxes if box.box_id == 'jp2h']
jp2h = jp2h_lst[0]
# 1st jp2 header box cannot be empty.
if len(jp2h.box) == 0:
msg = "The JP2 header superbox cannot be empty."
raise IOError(msg)
# 1st jp2 header box must be ihdr
jp2h = boxes[jp2h_idx]
if jp2h.box[0].box_id != 'ihdr':
msg = "The first box in the jp2 header box must be the image "
msg += "header box."
@ -1147,15 +1372,12 @@ def _validate_jp2_box_sequence(boxes):
raise IOError(msg)
colr = jp2h.box[colr_lst[0]]
# Any cdef box must be in the jp2 header following the image header.
cdef_lst = [j for (j, box) in enumerate(boxes) if box.box_id == 'cdef']
if len(cdef_lst) != 0:
msg = "Any channel defintion box must be in the JP2 header "
msg += "following the image header."
raise IOError(msg)
_validate_channel_definition(jp2h, colr)
cdef_lst = [j for (j, box) in enumerate(jp2h.box)
if box.box_id == 'cdef']
def _validate_channel_definition(jp2h, colr):
"""Validate the channel definition box."""
cdef_lst = [j for (j, box) in enumerate(jp2h.box) if box.box_id == 'cdef']
if len(cdef_lst) > 1:
msg = "Only one channel definition box is allowed in the "
msg += "JP2 header."
@ -1174,18 +1396,79 @@ def _validate_jp2_box_sequence(boxes):
msg = "All color channels must be defined in the "
msg += "channel definition box."
raise IOError(msg)
# The compatibility list must contain at a minimum 'jp2 '.
if 'jp2 ' not in boxes[1].compatibility_list:
msg = "The ftyp box must contain 'jp2 ' in the compatibility list."
JP2H_CHILDREN = set(['bpcc', 'cdef', 'cmap', 'ihdr', 'pclr'])
def _check_jp2h_child_boxes(boxes, parent_box_name):
"""Certain boxes can only reside in the JP2 header."""
box_ids = set([box.box_id for box in boxes])
intersection = box_ids.intersection(JP2H_CHILDREN)
if len(intersection) > 0 and parent_box_name != 'jp2h':
msg = "A '{0}' box can only be nested in a JP2 header box."
raise IOError(msg.format(list(intersection)[0]))
# Recursively check any contained superboxes.
for box in boxes:
if hasattr(box, 'box'):
_check_jp2h_child_boxes(box.box, box.box_id)
def _collect_box_count(boxes):
"""Count the occurences of each box type."""
count = Counter([box.box_id for box in boxes])
# Add the counts in the superboxes.
for box in boxes:
if hasattr(box, 'box'):
count.update(_collect_box_count(box.box))
return count
TOP_LEVEL_ONLY_BOXES = set(['dtbl'])
def _check_superbox_for_top_levels(boxes):
"""Several boxes can only occur at the top level."""
# We are only looking at the boxes contained in a superbox, so if any of
# the blacklisted boxes show up here, it's an error.
box_ids = set([box.box_id for box in boxes])
intersection = box_ids.intersection(TOP_LEVEL_ONLY_BOXES)
if len(intersection) > 0:
msg = "A '{0}' box cannot be nested in a superbox."
raise IOError(msg.format(list(intersection)[0]))
# Recursively check any contained superboxes.
for box in boxes:
if hasattr(box, 'box'):
_check_superbox_for_top_levels(box.box)
def _validate_top_level(boxes):
"""Several boxes can only occur at the top level."""
# Add the counts in the superboxes.
for box in boxes:
if hasattr(box, 'box'):
_check_superbox_for_top_levels(box.box)
count = _collect_box_count(boxes)
# Which boxes occur more than once?
multiples = [box_id for box_id, bcount in count.items() if bcount > 1]
if 'dtbl' in multiples:
raise IOError('There can only be one dtbl box in a file.')
# If there is one data reference box, then there must also be one ftbl.
if 'dtbl' in count and 'ftbl' not in count:
msg = 'The presence of a data reference box requires the presence of '
msg += 'a fragment table box as well.'
raise IOError(msg)
# JPX checks.
_asoc_check(boxes)
_jpx_brand(boxes, boxes[1].brand)
_jpx_compatibility(boxes, boxes[1].compatibility_list)
def _validate_singletons(boxes):
"""Several boxes can only occur once."""
count = _collect_box_count(boxes)
# Which boxes occur more than once?
multiples = [box_id for box_id, bcount in count.items() if bcount > 1]
if 'dtbl' in multiples:
raise IOError('There can only be one dtbl box in a file.')
def _jpx_brand(boxes, brand):
def _validate_jpx_brand(boxes, brand):
"""
If there is a JPX box then the brand must be 'jpx '.
"""
@ -1197,39 +1480,38 @@ def _jpx_brand(boxes, brand):
raise RuntimeError(msg)
if hasattr(box, 'box') != 0:
# Same set of checks on any child boxes.
_jpx_brand(box.box, brand)
_validate_jpx_brand(box.box, brand)
def _jpx_compatibility(boxes, compatibility_list):
def _validate_jpx_compatibility(boxes, compatibility_list):
"""
If there is a JPX box then the compatibility list must also contain 'jpx '.
"""
jpx_cl = set(compatibility_list)
for box in boxes:
if box.box_id in JPX_IDS:
if 'jpx ' not in compatibility_list:
msg = "A JPX box requires that 'jpx ' be present in the "
msg += "ftype compatibility list."
if len(set(['jpx ', 'jpxb']).intersection(jpx_cl)) == 0:
msg = "A JPX box requires that either 'jpx ' or 'jpxb' be "
msg += "present in the ftype compatibility list."
raise RuntimeError(msg)
if hasattr(box, 'box') != 0:
# Same set of checks on any child boxes.
_jpx_compatibility(box.box, compatibility_list)
_validate_jpx_compatibility(box.box, compatibility_list)
def _asoc_check(boxes):
def _validate_label(boxes):
"""
Association boxes can only contain number list boxes and xml boxes, as far
as we know.
Label boxes can only be inside association, codestream headers, or
compositing layer header boxes.
"""
for box in boxes:
if box.box_id == 'asoc':
if box.box[0].box_id != 'nlst' or box.box[1].box_id != 'xml ':
msg = "An Association box can only contain a NumberList box "
msg += "followed by an XML box."
raise RuntimeError(msg)
if hasattr(box, 'box') != 0:
# Same set of checks on any child boxes.
_asoc_check(box.box)
if box.box_id != 'asoc':
if hasattr(box, 'box'):
for boxi in box.box:
if boxi.box_id == 'lbl ':
msg = "A label box cannot be nested inside a {0} box."
msg = msg.format(box.box_id)
raise IOError(msg)
# Same set of checks on any child boxes.
_validate_label(box.box)
def extract_image_cube(image):
"""Extract 3D image from openjpeg data structure.
@ -1397,6 +1679,10 @@ def _populate_image_struct(cparams, image, imgdata):
# Stage the image data to the openjpeg data structure.
for k in range(0, num_comps):
if cparams.cp_cinema:
image.contents.comps[k].prec = 12
image.contents.comps[k].bpp = 12
layer = np.ascontiguousarray(imgdata[:, :, k], dtype=np.int32)
dest = image.contents.comps[k].data
src = layer.ctypes.data

View file

@ -11,8 +11,8 @@ import numpy as np
from .config import glymur_config
_, OPENJPEG = glymur_config()
# Maximum number of tile parts expected by JPWL: increase at your will
JPWL_MAX_NO_TILESPECS = 16
# Maximum number of tile parts expected by JPWL: increase at your will
JPWL_MAX_NO_TILESPECS = 16
J2K_MAXRLVLS = 33 # Number of maximum resolution level authorized
PATH_LEN = 4096 # maximum allowed size for filenames
@ -58,7 +58,7 @@ class CommonStructType(ctypes.Structure):
("mj2_handle", ctypes.c_void_p)]
STREAM_READ = 0x0001 # The stream was opened for reading.
STREAM_READ = 0x0001 # The stream was opened for reading.
STREAM_WRITE = 0x0002 # The stream was opened for writing.
class CioType(ctypes.Structure):
"""Byte input-output stream (CIO)
@ -81,7 +81,7 @@ class CioType(ctypes.Structure):
class CompressionInfoType(CommonStructType):
"""Common fields between JPEG-2000 compression and decompression contexts.
"""Common fields between JPEG-2000 compression and decompression contexts.
This is for compression contexts. Corresponds to common_struct_t.
"""
pass
@ -91,68 +91,68 @@ class PocType(ctypes.Structure):
"""Progression order changes."""
_fields_ = [("resno", ctypes.c_int),
# Resolution num start, Component num start, given by POC
("compno0", ctypes.c_int),
("compno0", ctypes.c_int),
# Layer num end,Resolution num end, Component num end, given by POC
("layno1", ctypes.c_int),
("resno1", ctypes.c_int),
("compno1", ctypes.c_int),
("layno1", ctypes.c_int),
("resno1", ctypes.c_int),
("compno1", ctypes.c_int),
# Layer num start,Precinct num start, Precinct num end
("layno0", ctypes.c_int),
("precno0", ctypes.c_int),
("precno1", ctypes.c_int),
# Layer num start,Precinct num start, Precinct num end
("layno0", ctypes.c_int),
("precno0", ctypes.c_int),
("precno1", ctypes.c_int),
# Progression order enum
# OPJ_PROG_ORDER prg1,prg;
("prg1", ctypes.c_int),
("prg", ctypes.c_int),
("prg1", ctypes.c_int),
("prg", ctypes.c_int),
# Progression order string
# Progression order string
# char progorder[5];
("progorder", ctypes.c_char * 5),
# Tile number
# Tile number
# int tile;
("tile", ctypes.c_int),
("tile", ctypes.c_int),
# /** Start and end values for Tile width and height*/
# int tx0,tx1,ty0,ty1;
("tx0", ctypes.c_int),
("tx1", ctypes.c_int),
("ty0", ctypes.c_int),
("ty1", ctypes.c_int),
("tx0", ctypes.c_int),
("tx1", ctypes.c_int),
("ty0", ctypes.c_int),
("ty1", ctypes.c_int),
# /** Start value, initialised in pi_initialise_encode*/
# int layS, resS, compS, prcS;
("layS", ctypes.c_int),
("resS", ctypes.c_int),
("compS", ctypes.c_int),
("layS", ctypes.c_int),
("resS", ctypes.c_int),
("compS", ctypes.c_int),
("prcS", ctypes.c_int),
# /** End value, initialised in pi_initialise_encode */
# int layE, resE, compE, prcE;
("layE", ctypes.c_int),
("resE", ctypes.c_int),
("compE", ctypes.c_int),
("prcE", ctypes.c_int),
("layE", ctypes.c_int),
("resE", ctypes.c_int),
("compE", ctypes.c_int),
("prcE", ctypes.c_int),
# Start and end values of Tile width and height, initialised in
# pi_initialise_encode int txS,txE,tyS,tyE,dx,dy;
("txS", ctypes.c_int),
("txE", ctypes.c_int),
("tyS", ctypes.c_int),
("tyE", ctypes.c_int),
("dx", ctypes.c_int),
("dy", ctypes.c_int),
("txS", ctypes.c_int),
("txE", ctypes.c_int),
("tyS", ctypes.c_int),
("tyE", ctypes.c_int),
("dx", ctypes.c_int),
("dy", ctypes.c_int),
# Temporary values for Tile parts, initialised in pi_create_encode
# Temporary values for Tile parts, initialised in pi_create_encode
# int lay_t, res_t, comp_t, prc_t,tx0_t,ty0_t;
("lay_t", ctypes.c_int),
("res_t", ctypes.c_int),
("comp_t", ctypes.c_int),
("prc_t", ctypes.c_int),
("tx0_t", ctypes.c_int),
("lay_t", ctypes.c_int),
("res_t", ctypes.c_int),
("comp_t", ctypes.c_int),
("prc_t", ctypes.c_int),
("tx0_t", ctypes.c_int),
("ty0_t", ctypes.c_int)]
@ -374,23 +374,23 @@ class DecompressionParametersType(ctypes.Structure):
class ImageComptParmType(ctypes.Structure):
"""Component parameters structure used by the opj_image_create function.
"""
_fields_ = [
# XRsiz: horizontal separation of a sample of ith component with
# respect to the reference grid
("dx", ctypes.c_int),
_fields_ = [
# XRsiz: horizontal separation of a sample of ith component with
# respect to the reference grid
("dx", ctypes.c_int),
# YRsiz: vertical separation of a sample of ith component with
# YRsiz: vertical separation of a sample of ith component with
# respect to the reference grid */
("dy", ctypes.c_int),
# data width, height
("w", ctypes.c_int),
("h", ctypes.c_int),
("dy", ctypes.c_int),
# x component offset compared to the whole image
# y component offset compared to the whole image
("x0", ctypes.c_int),
("y0", ctypes.c_int),
# data width, height
("w", ctypes.c_int),
("h", ctypes.c_int),
# x component offset compared to the whole image
# y component offset compared to the whole image
("x0", ctypes.c_int),
("y0", ctypes.c_int),
# precision
('prec', ctypes.c_int),
@ -398,7 +398,7 @@ class ImageComptParmType(ctypes.Structure):
# image depth in bits
('bpp', ctypes.c_int),
# signed (1) / unsigned (0)
# signed (1) / unsigned (0)
('sgnd', ctypes.c_int)]
@ -511,7 +511,7 @@ def destroy_compress(cinfo):
def encode(cinfo, cio, image):
"""Wrapper for openjpeg library function opj_encode.
Encodes an image into a JPEG-2000 codestream.
Encodes an image into a JPEG-2000 codestream.
Parameters
----------
@ -540,7 +540,7 @@ def destroy_decompress(dinfo):
def image_cmptparm_t_from_np(np_image):
"""Return appropriate image_cmptparm_t based on given numpy array.
"""
try:
try:
num_comps = np_image.shape[2]
except IndexError:
num_comps = 1
@ -557,17 +557,17 @@ def image_cmptparm_t_from_np(np_image):
bpp = 8
sgnd = 1
elif np_image.dtype == np.uint16:
prec = 16
bpp = 16
prec = 16
bpp = 16
sgnd = 0
elif np_image.dtype == np.int16:
prec = 16
bpp = 16
prec = 16
bpp = 16
sgnd = 1
else:
raise(TypeError("unhandled"))
for j in range(0, num_comps):
for j in range(0, num_comps):
tarr[j].dx = 1
tarr[j].dy = 1
tarr[j].w = np_image.shape[1]

View file

@ -1,5 +1,5 @@
"""
Tests for libopenjp2 wrapping functions.
Tests for libopenjp2 wrapping functions.
"""
# R0904: Seems like pylint is fooled in this situation
# W0142: using kwargs is ok in this context
@ -205,7 +205,7 @@ class TestOpenJP2(unittest.TestCase):
"""Runs test designated tte3 in OpenJPEG test suite."""
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
xtx3_setup(tfile.name)
self.assertTrue(True)
self.assertTrue(True)
def test_rta3(self):
"""Runs test designated rta3 in OpenJPEG test suite."""
@ -214,13 +214,13 @@ class TestOpenJP2(unittest.TestCase):
codec_format = openjp2.CODEC_J2K
self.j2k_random_tile_access(tfile.name, codec_format)
self.assertTrue(True)
self.assertTrue(True)
def test_tte4(self):
"""Runs test designated tte4 in OpenJPEG test suite."""
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
xtx4_setup(tfile.name)
self.assertTrue(True)
self.assertTrue(True)
def test_rta4(self):
"""Runs test designated rta4 in OpenJPEG test suite."""
@ -234,7 +234,7 @@ class TestOpenJP2(unittest.TestCase):
"""Runs test designated tte5 in OpenJPEG test suite."""
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
xtx5_setup(tfile.name)
self.assertTrue(True)
self.assertTrue(True)
def test_rta5(self):
"""Runs test designated rta5 in OpenJPEG test suite."""
@ -325,8 +325,8 @@ def tile_encoder(**kwargs):
def tile_decoder(**kwargs):
"""Fixture called with various configurations by many tests.
Reads a tile. That's all it does.
Reads a tile. That's all it does.
"""
stream = openjp2.stream_create_default_file_stream_v3(kwargs['filename'],
True)
@ -348,7 +348,7 @@ def tile_decoder(**kwargs):
openjp2.setup_decoder(codec, dparam)
image = openjp2.read_header(stream, codec)
openjp2.set_decode_area(codec, image,
openjp2.set_decode_area(codec, image,
kwargs['x0'], kwargs['y0'],
kwargs['x1'], kwargs['y1'])

View file

@ -4,6 +4,7 @@ Test fixtures common to more than one test point.
import os
import re
import sys
import textwrap
import warnings
import numpy as np
@ -11,6 +12,19 @@ import numpy as np
import glymur
# The Python XMP Toolkit may be used for XMP UUIDs, but only if available and
# if the version is at least 2.0.0.
try:
import libxmp
if hasattr(libxmp, 'version') and re.match(r'''[2-9].\d*.\d*''',
libxmp.version.VERSION):
from libxmp import XMPMeta
HAS_PYTHON_XMP_TOOLKIT = True
else:
HAS_PYTHON_XMP_TOOLKIT = False
except:
HAS_PYTHON_XMP_TOOLKIT = False
# Need to know of the libopenjp2 version is the official 2.0.0 release and NOT
# the 2.0+ development version.
OPENJP2_IS_V2_OFFICIAL = False
@ -31,6 +45,32 @@ except:
raise
def _indent(textstr):
"""
Indent a string.
Textwrap's indent method only exists for 3.3 or above. In 2.7 we have
to fake it.
Parameters
----------
textstring : str
String to be indented.
indent_level : str
Number of spaces of indentation to add.
Returns
-------
indented_string : str
Possibly multi-line string indented a certain bit.
"""
if sys.hexversion >= 0x03030000:
return textwrap.indent(textstr, ' ')
else:
lst = [(' ' + x) for x in textstr.split('\n')]
return '\n'.join(lst)
def opj_data_file(relative_file_name):
"""Compact way of forming a full filename from OpenJPEG's test suite."""
jfile = os.path.join(OPJ_DATA_ROOT, relative_file_name)
@ -167,3 +207,367 @@ def read_pgx_header(pgx_file):
header = header.rstrip()
return header, pos
nemo_xmp = """<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<ns0:xmpmeta xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ns0="adobe:ns:meta/" xmlns:ns2="http://ns.adobe.com/xap/1.0/" xmlns:ns3="http://ns.adobe.com/tiff/1.0/" xmlns:ns4="http://ns.adobe.com/exif/1.0/" xmlns:ns5="http://ns.adobe.com/photoshop/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="Exempi + XMP Core 5.1.2">
<rdf:RDF>
<rdf:Description rdf:about="">
<ns2:CreatorTool>Google</ns2:CreatorTool>
<ns2:CreateDate>2013-02-09T14:47:53</ns2:CreateDate>
</rdf:Description>
<rdf:Description rdf:about="">
<ns3:YCbCrPositioning>1</ns3:YCbCrPositioning>
<ns3:XResolution>72/1</ns3:XResolution>
<ns3:YResolution>72/1</ns3:YResolution>
<ns3:ResolutionUnit>2</ns3:ResolutionUnit>
<ns3:Make>HTC</ns3:Make>
<ns3:Model>HTC Glacier</ns3:Model>
<ns3:ImageWidth>2592</ns3:ImageWidth>
<ns3:ImageLength>1456</ns3:ImageLength>
<ns3:BitsPerSample>
<rdf:Seq>
<rdf:li>8</rdf:li>
<rdf:li>8</rdf:li>
<rdf:li>8</rdf:li>
</rdf:Seq>
</ns3:BitsPerSample>
<ns3:PhotometricInterpretation>2</ns3:PhotometricInterpretation>
<ns3:SamplesPerPixel>3</ns3:SamplesPerPixel>
<ns3:WhitePoint>
<rdf:Seq>
<rdf:li>1343036288/4294967295</rdf:li>
<rdf:li>1413044224/4294967295</rdf:li>
</rdf:Seq>
</ns3:WhitePoint>
<ns3:PrimaryChromaticities>
<rdf:Seq>
<rdf:li>2748779008/4294967295</rdf:li>
<rdf:li>1417339264/4294967295</rdf:li>
<rdf:li>1288490240/4294967295</rdf:li>
<rdf:li>2576980480/4294967295</rdf:li>
<rdf:li>644245120/4294967295</rdf:li>
<rdf:li>257698032/4294967295</rdf:li>
</rdf:Seq>
</ns3:PrimaryChromaticities>
</rdf:Description>
<rdf:Description rdf:about="">
<ns4:ColorSpace>1</ns4:ColorSpace>
<ns4:PixelXDimension>2528</ns4:PixelXDimension>
<ns4:PixelYDimension>1424</ns4:PixelYDimension>
<ns4:FocalLength>353/100</ns4:FocalLength>
<ns4:GPSAltitudeRef>0</ns4:GPSAltitudeRef>
<ns4:GPSAltitude>0/1</ns4:GPSAltitude>
<ns4:GPSMapDatum>WGS-84</ns4:GPSMapDatum>
<ns4:DateTimeOriginal>2013-02-09T14:47:53</ns4:DateTimeOriginal>
<ns4:ISOSpeedRatings>
<rdf:Seq>
<rdf:li>76</rdf:li>
</rdf:Seq>
</ns4:ISOSpeedRatings>
<ns4:ExifVersion>0220</ns4:ExifVersion>
<ns4:FlashpixVersion>0100</ns4:FlashpixVersion>
<ns4:ComponentsConfiguration>
<rdf:Seq>
<rdf:li>1</rdf:li>
<rdf:li>2</rdf:li>
<rdf:li>3</rdf:li>
<rdf:li>0</rdf:li>
</rdf:Seq>
</ns4:ComponentsConfiguration>
<ns4:GPSLatitude>42,20.56N</ns4:GPSLatitude>
<ns4:GPSLongitude>71,5.29W</ns4:GPSLongitude>
<ns4:GPSTimeStamp>2013-02-09T19:47:53Z</ns4:GPSTimeStamp>
<ns4:GPSProcessingMethod>NETWORK</ns4:GPSProcessingMethod>
</rdf:Description>
<rdf:Description rdf:about="">
<ns5:DateCreated>2013-02-09T14:47:53</ns5:DateCreated>
</rdf:Description>
<rdf:Description rdf:about="">
<dc:Creator>
<rdf:Seq>
<rdf:li>Glymur</rdf:li>
<rdf:li>Python XMP Toolkit</rdf:li>
</rdf:Seq>
</dc:Creator>
</rdf:Description>
</rdf:RDF>
</ns0:xmpmeta>
<?xpacket end="w"?>"""
nemo_xmp_box = """UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
{0}""".format(_indent(nemo_xmp))
nemo_xmp_box = """UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
{0}""".format(_indent(nemo_xmp))
SimpleRDF = """<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
<rdf:Description rdf:about='Test:XMPCoreCoverage/kSimpleRDF'
xmlns:ns1='ns:test1/' xmlns:ns2='ns:test2/'>
<ns1:SimpleProp>Simple value</ns1:SimpleProp>
<ns1:Distros>
<rdf:Bag>
<rdf:li>Suse</rdf:li>
<rdf:li>Fedora</rdf:li>
</rdf:Bag>
</ns1:Distros>
</rdf:Description>
</rdf:RDF>"""
text_gbr_27 = """Colour Specification Box (colr) @ (179, 1339)
Method: any ICC profile
Precedence: 2
Approximation: accurately represents correct colorspace definition
ICC Profile:
{'Color Space': 'RGB',
'Connection Space': 'XYZ',
'Creator': u'appl',
'Datetime': datetime.datetime(2009, 2, 25, 11, 26, 11),
'Device Attributes': 'reflective, glossy, positive media polarity, color media',
'Device Class': 'display device profile',
'Device Manufacturer': u'appl',
'Device Model': '',
'File Signature': u'acsp',
'Flags': 'not embedded, can be used independently',
'Illuminant': array([ 0.96420288, 1. , 0.8249054 ]),
'Platform': u'APPL',
'Preferred CMM Type': 1634758764,
'Rendering Intent': 'perceptual',
'Size': 1328,
'Version': '2.2.0'}"""
text_gbr_33 = """Colour Specification Box (colr) @ (179, 1339)
Method: any ICC profile
Precedence: 2
Approximation: accurately represents correct colorspace definition
ICC Profile:
{'Size': 1328,
'Preferred CMM Type': 1634758764,
'Version': '2.2.0',
'Device Class': 'display device profile',
'Color Space': 'RGB',
'Connection Space': 'XYZ',
'Datetime': datetime.datetime(2009, 2, 25, 11, 26, 11),
'File Signature': 'acsp',
'Platform': 'APPL',
'Flags': 'not embedded, can be used independently',
'Device Manufacturer': 'appl',
'Device Model': '',
'Device Attributes': 'reflective, glossy, positive media polarity, color media',
'Rendering Intent': 'perceptual',
'Illuminant': array([ 0.96420288, 1. , 0.8249054 ]),
'Creator': 'appl'}"""
text_gbr_34 = """Colour Specification Box (colr) @ (179, 1339)
Method: any ICC profile
Precedence: 2
Approximation: accurately represents correct colorspace definition
ICC Profile:
{'Size': 1328,
'Preferred CMM Type': 1634758764,
'Version': '2.2.0',
'Device Class': 'display device profile',
'Color Space': 'RGB',
'Connection Space': 'XYZ',
'Datetime': datetime.datetime(2009, 2, 25, 11, 26, 11),
'File Signature': 'acsp',
'Platform': 'APPL',
'Flags': 'not embedded, can be used independently',
'Device Manufacturer': 'appl',
'Device Model': '',
'Device Attributes': 'reflective, glossy, positive media polarity, color '
'media',
'Rendering Intent': 'perceptual',
'Illuminant': array([ 0.96420288, 1. , 0.8249054 ]),
'Creator': 'appl'}"""
# Metadata dump of nemo.
dump = r'''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: [1456 2592 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
{0}
Contiguous Codestream Box (jp2c) @ (3223, 1132296)
Main header:
SOC marker segment @ (3231, 0)
SIZ marker segment @ (3233, 47)
Profile: 2
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
Reference Tile Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Tile Offset: (0 x 0)
Bitdepth: (8, 8, 8)
Signed: (False, False, False)
Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))
COD marker segment @ (3282, 12)
Coding style:
Entropy coder, without partitions
SOP marker segments: False
EPH marker segments: False
Coding style parameters:
Progression order: LRCP
Number of layers: 2
Multiple component transformation usage: reversible
Number of resolutions: 2
Code block height, width: (64 x 64)
Wavelet transform: 5-3 reversible
Precinct size: default, 2^15 x 2^15
Code block context:
Selective arithmetic coding bypass: False
Reset context probabilities on coding pass boundaries: False
Termination on each coding pass: False
Vertically stripe causal context: False
Predictable termination: False
Segmentation symbols: False
QCD marker segment @ (3296, 7)
Quantization style: no quantization, 2 guard bits
Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]
CME marker segment @ (3305, 37)
"Created by OpenJPEG version 2.0.0"'''
nemo_dump_full = dump.format(_indent(nemo_xmp))
nemo_dump_short = r"""JPEG 2000 Signature Box (jP ) @ (0, 12)
File Type Box (ftyp) @ (12, 20)
JP2 Header Box (jp2h) @ (32, 45)
Image Header Box (ihdr) @ (40, 22)
Colour Specification Box (colr) @ (62, 15)
UUID Box (uuid) @ (77, 3146)
Contiguous Codestream Box (jp2c) @ (3223, 1132296)"""
nemo_dump_no_xml = '''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: [1456 2592 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
Contiguous Codestream Box (jp2c) @ (3223, 1132296)
Main header:
SOC marker segment @ (3231, 0)
SIZ marker segment @ (3233, 47)
Profile: 2
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
Reference Tile Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Tile Offset: (0 x 0)
Bitdepth: (8, 8, 8)
Signed: (False, False, False)
Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))
COD marker segment @ (3282, 12)
Coding style:
Entropy coder, without partitions
SOP marker segments: False
EPH marker segments: False
Coding style parameters:
Progression order: LRCP
Number of layers: 2
Multiple component transformation usage: reversible
Number of resolutions: 2
Code block height, width: (64 x 64)
Wavelet transform: 5-3 reversible
Precinct size: default, 2^15 x 2^15
Code block context:
Selective arithmetic coding bypass: False
Reset context probabilities on coding pass boundaries: False
Termination on each coding pass: False
Vertically stripe causal context: False
Predictable termination: False
Segmentation symbols: False
QCD marker segment @ (3296, 7)
Quantization style: no quantization, 2 guard bits
Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]
CME marker segment @ (3305, 37)
"Created by OpenJPEG version 2.0.0"'''
dump = r"""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: [1456 2592 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
UUID Data:
{0}
Contiguous Codestream Box (jp2c) @ (3223, 1132296)"""
nemo_dump_no_codestream = dump.format(_indent(nemo_xmp))
nemo_dump_no_codestream_no_xml = r"""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: [1456 2592 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
Contiguous Codestream Box (jp2c) @ (3223, 1132296)"""
# Output of reader requirement printing for file7.jp2
file7_rreq = r"""Reader Requirements Box (rreq) @ (44, 24)
Fully Understands Aspect Mask: 0xa0
Display Completely Mask: 0xc0
Standard Features and Masks:
Feature 005: 0x80 Unrestricted JPEG 2000 Part 1 codestream, ITU-T Rec. T.800 | ISO/IEC 15444-1
Feature 060: 0x60 e-sRGB enumerated colorspace
Feature 043: 0x40 Deprecated - compositing layer uses restricted ICC profile
Vendor Features:"""
file1_xml = """XML Box (xml ) @ (36, 439)
<IMAGE_CREATION xmlns="http://www.jpeg.org/jpx/1.0/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jpeg.org/jpx/1.0/xml http://www.jpeg.org/metadata/15444-2.xsd">
\t<GENERAL_CREATION_INFO>
\t\t<CREATION_TIME>2001-11-01T13:45:00.000-06:00</CREATION_TIME>
\t\t<IMAGE_SOURCE>Professional 120 Image</IMAGE_SOURCE>
\t</GENERAL_CREATION_INFO>
</IMAGE_CREATION>"""

View file

@ -11,6 +11,7 @@ import os
from os.path import join
import re
import sys
import tempfile
import unittest
import glymur
@ -27,6 +28,41 @@ except KeyError:
OPJ_DATA_ROOT = None
@unittest.skipIf(sys.hexversion < 0x03020000,
"Requires features introduced in 3.2 (assertWarns)")
class TestSuiteConformance(unittest.TestCase):
"""Test suite for conformance."""
def setUp(self):
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
pass
@unittest.skipIf(re.match(r"""1\.[0123]""",
glymur.version.openjpeg_version) is not None,
"Needs 1.3+ to catch this.")
def test_truncated_eoc(self):
"""Has one byte shaved off of EOC marker."""
with open(self.j2kfile, 'rb') as ifile:
data = ifile.read()
with tempfile.NamedTemporaryFile(suffix='.j2k') as ofile:
ofile.write(data[:-1])
ofile.flush()
j2k = Jp2k(ofile.name)
with self.assertWarns(UserWarning):
codestream = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC
# marker.
self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
# The codestream is not as long as claimed.
with self.assertRaises(OSError):
j2k.read(rlevel=-1)
@unittest.skipIf(FORMAT_CORPUS_DATA_ROOT is None,
"FORMAT_CORPUS_DATA_ROOT environment variable not set")
@unittest.skipIf(sys.hexversion < 0x03020000,
@ -34,24 +70,6 @@ except KeyError:
class TestSuiteFormatCorpus(unittest.TestCase):
"""Test suite for files in format corpus repository."""
@unittest.skipIf(re.match(r"""1\.[0123]""",
glymur.version.openjpeg_version) is not None,
"Needs 1.3+ to catch this.")
def test_balloon_trunc1(self):
"""Has one byte shaved off of EOC marker."""
jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
'jp2k-test/byteCorruption/balloon_trunc1.jp2')
j2k = Jp2k(jfile)
with self.assertWarns(UserWarning):
codestream = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC marker.
self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
# The codestream is not as long as claimed.
with self.assertRaises(OSError):
j2k.read(rlevel=-1)
@unittest.skipIf(re.match(r"""1\.[01234]""",
glymur.version.openjpeg_version) is not None,
"Needs 1.4+ to catch this.")

View file

@ -22,10 +22,10 @@ import sys
import tempfile
import uuid
from uuid import UUID
import xml.etree.cElementTree as ET
import unittest
import warnings
import lxml.etree as ET
import numpy as np
import glymur
@ -52,6 +52,54 @@ def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite('glymur.jp2box'))
return tests
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestDataEntryURL(unittest.TestCase):
"""Test suite for DataEntryURL boxes."""
def setUp(self):
self.jp2file = glymur.data.nemo()
def test_basic_url(self):
"""Just your most basic URL box."""
# Wrap our j2k file in a JP2 box along with an interior url box.
jp2 = Jp2k(self.jp2file)
url = 'http://glymur.readthedocs.org'
deurl = glymur.jp2box.DataEntryURLBox(0, (0, 0, 0), url)
boxes = [box for box in jp2.box if box.box_id != 'uuid']
boxes.append(deurl)
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
jp22 = jp2.wrap(tfile.name, boxes=boxes)
actdata = [box.box_id for box in jp22.box]
expdata = ['jP ', 'ftyp', 'jp2h', 'jp2c', 'url ']
self.assertEqual(actdata, expdata)
self.assertEqual(jp22.box[4].version, 0)
self.assertEqual(jp22.box[4].flag, (0, 0, 0))
self.assertEqual(jp22.box[4].url, url)
def test_null_termination(self):
"""I.9.3.2 specifies that the location field must be null terminated."""
jp2 = Jp2k(self.jp2file)
url = 'http://glymur.readthedocs.org'
deurl = glymur.jp2box.DataEntryURLBox(0, (0, 0, 0), url)
boxes = [box for box in jp2.box if box.box_id != 'uuid']
boxes.append(deurl)
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
jp22 = jp2.wrap(tfile.name, boxes=boxes)
self.assertEqual(jp22.box[-1].length, 42)
# Go to the last box. Seek past the L, T, version, and flag fields.
with open(tfile.name, 'rb') as fptr:
fptr.seek(jp22.box[-1].offset + 4 + 4 + 1 + 3)
nbytes = jp22.box[-1].offset + jp22.box[-1].length - fptr.tell()
read_buffer = fptr.read(nbytes)
read_url = read_buffer.decode('utf-8')
self.assertEqual(url + chr(0), read_url)
@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2 or
OPENJP2_IS_V2_OFFICIAL,
"Not supported until 2.0+.")
@ -113,7 +161,7 @@ class TestChannelDefinition(unittest.TestCase):
def test_cdef_no_inputs(self):
"""channel_type and association are required inputs."""
with self.assertRaises(IOError):
with self.assertRaises(TypeError):
glymur.jp2box.ChannelDefinitionBox()
def test_rgb_with_index(self):
@ -289,7 +337,7 @@ class TestChannelDefinition(unittest.TestCase):
boxes = [self.jp2b, self.ftyp, self.jp2h, cdef, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
with self.assertRaises((IOError, OSError)):
j2k.wrap(tfile.name, boxes=boxes)
def test_bad_type(self):
@ -314,6 +362,29 @@ class TestChannelDefinition(unittest.TestCase):
association=association)
class TestFileTypeBox(unittest.TestCase):
"""Test suite for ftyp box issues."""
def setUp(self):
pass
def tearDown(self):
pass
def test_brand_unknown(self):
"""A ftyp box brand must be 'jp2 ' or 'jpx '."""
ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftyp.write(tfile)
def test_cl_entry_unknown(self):
"""A ftyp box cl list can only contain 'jp2 ', 'jpx ', or 'jpxb'."""
ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftyp.write(tfile)
class TestColourSpecificationBox(unittest.TestCase):
"""Test suite for colr box instantiation."""
@ -345,7 +416,7 @@ class TestColourSpecificationBox(unittest.TestCase):
boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(NotImplementedError):
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@ -391,6 +462,46 @@ class TestColourSpecificationBox(unittest.TestCase):
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
approximation=approx)
def test_colr_with_bad_color(self):
"""colr must have a valid color, strange as though that may sound."""
colorspace = -1
approx = 0
colr = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
approximation=approx)
with tempfile.TemporaryFile() as tfile:
with self.assertRaises(IOError):
colr.write(tfile)
@unittest.skipIf(os.name == "nt",
"Problems using NamedTemporaryFile on windows.")
class TestPaletteBox(unittest.TestCase):
"""Test suite for pclr box instantiation."""
def setUp(self):
pass
def tearDown(self):
pass
def test_mismatched_bitdepth_signed(self):
"""bitdepth and signed arguments must have equal length"""
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
bps = (8, 8, 8)
signed = (False, False)
with self.assertRaises(IOError):
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
signed=signed)
def test_mismatched_signed_palette(self):
"""bitdepth and signed arguments must have equal length"""
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
bps = (8, 8, 8, 8)
signed = (False, False, False, False)
with self.assertRaises(IOError):
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
signed=signed)
class TestAppend(unittest.TestCase):
"""Tests for append method."""
@ -415,7 +526,7 @@ class TestAppend(unittest.TestCase):
# The sequence of box IDs should be the same as before, but with an
# xml box at the end.
box_ids = [box.box_id for box in jp2.box]
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'xml ']
self.assertEqual(box_ids, expected)
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
b'<data>0</data>')
@ -425,14 +536,14 @@ class TestAppend(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
shutil.copyfile(self.j2kfile, tfile.name)
jp2 = Jp2k(tfile.name)
j2k = Jp2k(tfile.name)
# Make a UUID box.
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
data = b'0123456789'
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
# Make an XML box. XML boxes should always be appendable to jp2
# files.
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
with self.assertRaises(IOError):
jp2.append(uuidbox)
j2k.append(xmlbox)
def test_length_field_is_zero(self):
"""L=0 (length field in box header) is handled.
@ -464,19 +575,19 @@ class TestAppend(unittest.TestCase):
# The sequence of box IDs should be the same as before, but with an
# xml box at the end.
box_ids = [box.box_id for box in jp2.box]
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'xml ']
self.assertEqual(box_ids, expected)
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
b'<data>0</data>')
def test_only_xml_allowed_to_append(self):
def test_append_allowable_boxes(self):
"""Only XML boxes are allowed to be appended."""
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
shutil.copyfile(self.jp2file, tfile.name)
jp2 = Jp2k(tfile.name)
# Make a UUID box.
# Make a UUID box. Only XMP UUID boxes can currently be appended.
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
data = b'0123456789'
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
@ -484,6 +595,7 @@ class TestAppend(unittest.TestCase):
jp2.append(uuidbox)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestWrap(unittest.TestCase):
"""Tests for wrap method."""
@ -544,7 +656,6 @@ class TestWrap(unittest.TestCase):
self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB)
self.assertIsNone(jp2.box[2].box[1].icc_profile)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_wrap(self):
"""basic test for rewrapping a j2c file, no specified boxes"""
j2k = Jp2k(self.j2kfile)
@ -552,13 +663,9 @@ 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_jpx_to_jp2(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)
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:
@ -576,7 +683,6 @@ class TestWrap(unittest.TestCase):
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"""
j2k = Jp2k(self.jp2file)
@ -585,7 +691,17 @@ class TestWrap(unittest.TestCase):
boxes = [box.box_id for box in jp2.box]
self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_empty_jp2h(self):
"""JP2H box list cannot be empty."""
jp2 = Jp2k(self.jp2file)
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
boxes = jp2.box
# Right here the jp2h superbox has two child boxes. Empty out that
# list to trigger the error.
boxes[2].box = []
with self.assertRaises(IOError):
jp22 = jp2.wrap(tfile.name, boxes=boxes)
def test_default_layout_with_boxes(self):
"""basic test for rewrapping a jp2 file, boxes specified"""
j2k = Jp2k(self.j2kfile)
@ -605,7 +721,6 @@ class TestWrap(unittest.TestCase):
j2k.wrap(tfile.name, boxes=boxes)
self.verify_wrapped_raw(tfile.name)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_ihdr_not_first_in_jp2h(self):
"""The specification says that ihdr must be the first box in jp2h."""
j2k = Jp2k(self.j2kfile)
@ -625,7 +740,6 @@ class TestWrap(unittest.TestCase):
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_first_boxes_jp_and_ftyp(self):
"""first two boxes must be jP followed by ftyp"""
j2k = Jp2k(self.j2kfile)
@ -647,7 +761,33 @@ class TestWrap(unittest.TestCase):
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_pclr_not_in_jp2h(self):
"""A palette box must reside in a JP2 header box."""
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.int32)
bps = (8, 8, 8)
signed = (True, False, True)
pclr = glymur.jp2box.PaletteBox(palette=palette, bits_per_component=bps,
signed=(True, False, True))
j2k = Jp2k(self.j2kfile)
codestream = j2k.get_codestream()
height = codestream.segment[1].ysiz
width = codestream.segment[1].xsiz
num_components = len(codestream.segment[1].xrsiz)
jp2b = JPEG2000SignatureBox()
ftyp = FileTypeBox()
jp2h = JP2HeaderBox()
jp2c = ContiguousCodestreamBox()
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
jp2h.box = [ihdr, colr]
boxes = [jp2b, ftyp, jp2h, jp2c, pclr]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
def test_jp2h_not_preceeding_jp2c(self):
"""jp2h must precede jp2c"""
j2k = Jp2k(self.j2kfile)
@ -669,7 +809,6 @@ class TestWrap(unittest.TestCase):
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_missing_codestream(self):
"""Need a codestream box in order to call wrap method."""
j2k = Jp2k(self.j2kfile)
@ -916,9 +1055,9 @@ class TestRepr(unittest.TestCase):
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]*)>\)"
regexp = r"""glymur.jp2box.XMLBox"""
regexp += r"""\(xml=<lxml.etree._ElementTree object """
regexp += """at 0x([a-f0-9]*)>\)"""
if sys.hexversion < 0x03000000:
self.assertRegexpMatches(repr(box), regexp)
@ -940,7 +1079,7 @@ class TestRepr(unittest.TestCase):
self.assertEqual(box.vendor_feature, newbox.vendor_feature)
self.assertEqual(box.vendor_mask, newbox.vendor_mask)
def test_uuid_box(self):
def test_uuid_box_generic(self):
"""Verify uuid repr method."""
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
data = b'0123456789'
@ -948,9 +1087,27 @@ class TestRepr(unittest.TestCase):
# 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=<byte\sarray\s10\selements>\)"
regexp = r"""glymur.jp2box.UUIDBox\("""
regexp += """the_uuid=UUID\('00000000-0000-0000-0000-000000000000'\),\s"""
regexp += """raw_data=<byte\sarray\s10\selements>\)"""
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_uuid_box_xmp(self):
"""Verify uuid repr method for XMP UUID box."""
jp2file = glymur.data.nemo()
j = Jp2k(jp2file)
box = j.box[3]
# 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 = r"""glymur.jp2box.UUIDBox\("""
regexp += """the_uuid=UUID\('be7acfcb-97a9-42e8-9c71-999491e3afac'\),\s"""
regexp += """raw_data=<byte\sarray\s3122\selements>\)"""
if sys.hexversion < 0x03000000:
self.assertRegexpMatches(repr(box), regexp)

View file

@ -8,12 +8,12 @@ import struct
import sys
import tempfile
import unittest
import warnings
import xml.etree.cElementTree as ET
import lxml.etree as ET
import glymur
from glymur import Jp2k
from glymur.jp2box import DataEntryURLBox, FileTypeBox, JPEG2000SignatureBox
from glymur.jp2box import DataReferenceBox, FragmentListBox, FragmentTableBox
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestJPXWrap(unittest.TestCase):
@ -21,6 +21,7 @@ class TestJPXWrap(unittest.TestCase):
def setUp(self):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
raw_xml = b"""<?xml version="1.0"?>
<data>
@ -40,14 +41,108 @@ class TestJPXWrap(unittest.TestCase):
def tearDown(self):
os.unlink(self.xmlfile)
def test_association_box(self):
"""Wrap JP2 to JPX with asoc(nlst, xml)"""
def test_jpx_ftbl_no_codestream(self):
"""Can have a jpx with no codestream."""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
with open(self.jp2file, 'rb') as fptr:
tfile1.write(fptr.read())
tfile1.flush()
jp2_1 = Jp2k(tfile1.name)
jp2c = [box for box in jp2_1.box if box.box_id == 'jp2c'][0]
# coff and clen will be the offset and length input arguments
# to the fragment list box. dr_idx is the data reference index.
coff = []
clen = []
dr_idx = []
coff.append(jp2c.offset + 8)
clen.append(jp2c.length - (coff[0] - jp2c.offset))
dr_idx.append(1)
# Make the url box for this codestream.
url1 = DataEntryURLBox(0, [0, 0, 0], 'file://' + tfile1.name)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2:
j2k = Jp2k(self.j2kfile)
jp2_2 = j2k.wrap(tfile2.name)
jp2c = [box for box in jp2_2.box if box.box_id == 'jp2c'][0]
coff.append(jp2c.offset + 8)
clen.append(jp2c.length - (coff[0] - jp2c.offset))
dr_idx.append(2)
# Make the url box for this codestream.
url2 = DataEntryURLBox(0, [0, 0, 0], 'file://' + tfile2.name)
boxes = [JPEG2000SignatureBox(),
FileTypeBox(brand='jpx ',
compatibility_list=['jpx ',
'jp2 ', 'jpxb']),
jp2_1.box[2]]
with tempfile.NamedTemporaryFile(suffix='.jpx') as tjpx:
for box in boxes:
box.write(tjpx)
flst = FragmentListBox(coff, clen, dr_idx)
ftbl = FragmentTableBox([flst])
ftbl.write(tjpx)
boxes = [url1, url2]
dtbl = DataReferenceBox(data_entry_url_boxes=boxes)
dtbl.write(tjpx)
tjpx.flush()
def test_jp2_with_jpx_box(self):
"""If the brand is jp2, then no jpx boxes are allowed."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]]
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
boxes = jp2.box
boxes.append(glymur.jp2box.AssociationBox())
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
def test_ftbl(self):
"""Write a fragment table box."""
# Add a negative test where offset < 0
# Add a negative test where length < 0
# Add a negative test where ref > 0 but no data reference box.
# Add a negative test where more than one flst
# Add negative test where ftbl contained in a superbox.
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# The ftyp box must be modified to jpx.
boxes[1].brand = 'jpx '
boxes[1].compatibility_list = ['jp2 ', 'jpxb']
offset = [89]
length = [1132288]
reference = [0]
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
ftbl = glymur.jp2box.FragmentTableBox(box=[flst])
boxes.append(ftbl)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
jpx = jp2.wrap(tfile.name, boxes=boxes)
self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpxb'])
self.assertEqual(jpx.box[-1].box_id, 'ftbl')
self.assertEqual(jpx.box[-1].box[0].box_id, 'flst')
def test_jpxb_compatibility(self):
"""Wrap JP2 to JPX, state jpxb compatibility"""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# The ftyp box must be modified to jpx with jp2 compatibility.
boxes[1].brand = 'jpx '
boxes[1].compatibility_list = ['jp2 ', 'jpx ']
boxes[1].compatibility_list = ['jp2 ', 'jpxb']
numbers = (0, 1)
nlst = glymur.jp2box.NumberListBox(numbers)
@ -59,7 +154,7 @@ class TestJPXWrap(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
jpx = jp2.wrap(tfile.name, boxes=boxes)
self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpx '])
self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpxb'])
self.assertEqual(jpx.box[-1].box_id, 'asoc')
self.assertEqual(jpx.box[-1].box[0].box_id, 'nlst')
self.assertEqual(jpx.box[-1].box[1].box_id, 'xml ')
@ -67,11 +162,137 @@ class TestJPXWrap(unittest.TestCase):
self.assertEqual(ET.tostring(jpx.box[-1].box[1].xml.getroot()),
b'<data>0</data>')
def test_association_label_box(self):
"""Wrap JP2 to JPX with asoc, label, and nlst boxes"""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# The ftyp box must be modified to jpx with jp2 compatibility.
boxes[1].brand = 'jpx '
boxes[1].compatibility_list = ['jp2 ', 'jpx ']
label = 'this is a test'
lblb = glymur.jp2box.LabelBox(label)
numbers = (0, 1)
nlst = glymur.jp2box.NumberListBox(numbers)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
asoc = glymur.jp2box.AssociationBox([nlst, xmlb, lblb])
boxes.append(asoc)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
jpx = jp2.wrap(tfile.name, boxes=boxes)
self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpx '])
self.assertEqual(jpx.box[-1].box_id, 'asoc')
self.assertEqual(jpx.box[-1].box[0].box_id, 'nlst')
self.assertEqual(jpx.box[-1].box[0].associations, numbers)
self.assertEqual(jpx.box[-1].box[1].box_id, 'xml ')
self.assertEqual(ET.tostring(jpx.box[-1].box[1].xml.getroot()),
b'<data>0</data>')
self.assertEqual(jpx.box[-1].box[2].box_id, 'lbl ')
self.assertEqual(jpx.box[-1].box[2].label, label)
def test_empty_data_reference(self):
"""Empty data reference boxes can be created, but not written."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
boxes[1].brand = 'jpx '
dref = glymur.jp2box.DataReferenceBox()
boxes.append(dref)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
def test_deurl_child_of_dtbl(self):
"""Data reference boxes can only contain data entry url boxes."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
ftyp = glymur.jp2box.FileTypeBox()
with self.assertRaises(IOError):
dref = glymur.jp2box.DataReferenceBox([ftyp])
# Try to get around it by appending the ftyp box after creation.
dref = glymur.jp2box.DataReferenceBox()
dref.DR.append(ftyp)
boxes.append(dref)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
def test_only_one_data_reference(self):
"""Data reference boxes cannot be inside a superbox ."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# Have to make the ftyp brand jpx.
boxes[1].brand = 'jpx '
flag = 0
version = (0, 0, 0)
url = 'file:////usr/local/bin'
deurl = glymur.jp2box.DataEntryURLBox(flag, version, url)
dref = glymur.jp2box.DataReferenceBox([deurl])
boxes.append(dref)
boxes.append(dref)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
def test_lbl_at_top_level(self):
"""Label boxes can only be inside a asoc box ."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# Have to make the ftyp brand jpx.
boxes[1].brand = 'jpx '
lblb = glymur.jp2box.LabelBox('hi there')
# Put it inside the jp2 header box.
boxes[2].box.append(lblb)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
def test_data_reference_in_subbox(self):
"""Data reference boxes cannot be inside a superbox ."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# Have to make the ftyp brand jpx.
boxes[1].brand = 'jpx '
flag = 0
version = (0, 0, 0)
url = 'file:////usr/local/bin'
deurl = glymur.jp2box.DataEntryURLBox(flag, version, url)
dref = glymur.jp2box.DataReferenceBox([deurl])
# Put it inside the jp2 header box.
boxes[2].box.append(dref)
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
def test_jp2_to_jpx_sans_jp2_compatibility(self):
"""jp2 wrapped to jpx not including jp2 compatibility is wrong."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]]
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# Have to make the ftyp brand jpx.
boxes[1].brand = 'jpx '
boxes[1].compatibility_list.append('jp2 ')
numbers = [0, 1]
nlst = glymur.jp2box.NumberListBox(numbers)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
@ -81,12 +302,12 @@ class TestJPXWrap(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(RuntimeError):
jpx = jp2.wrap(tfile.name, boxes=boxes)
jp2.wrap(tfile.name, boxes=boxes)
def test_jp2_to_jpx_sans_jpx_brand(self):
"""Verify error when jp2 wrapped to jpx does not include jpx brand."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]]
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
boxes[1].brand = 'jpx '
numbers = [0, 1]
nlst = glymur.jp2box.NumberListBox(numbers)
@ -97,82 +318,142 @@ class TestJPXWrap(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(RuntimeError):
jpx = jp2.wrap(tfile.name, boxes=boxes)
jp2.wrap(tfile.name, boxes=boxes)
@unittest.skipIf(sys.hexversion < 0x03000000, "Warning assert on 2.x.")
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestJPX(unittest.TestCase):
"""Test suite for other JPX boxes."""
def setUp(self):
self.jp2file = glymur.data.nemo()
self.jpxfile = glymur.data.jpxfile()
def tearDown(self):
pass
def test_rreq_box_strange_mask_length(self):
"""The standard says that the mask length should be 1, 2, 4, or 8."""
with warnings.catch_warnings():
# This file has a rreq mask length that we do not recognize.
warnings.simplefilter("ignore")
j = Jp2k(self.jpxfile)
self.assertEqual(j.box[2].box_id, 'rreq')
self.assertEqual(type(j.box[2]),
def test_flst_lens_not_the_same(self):
"""A fragment list box items must be the same length."""
offset = [89]
length = [1132288]
reference = [0, 0]
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
flst.write(tfile)
def test_flst_offsets_not_positive(self):
"""A fragment list box offsets must be positive."""
offset = [0]
length = [1132288]
reference = [0]
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
flst.write(tfile)
def test_flst_lengths_not_positive(self):
"""A fragment list box lengths must be positive."""
offset = [89]
length = [0]
reference = [0]
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
flst.write(tfile)
def test_ftbl_boxes_empty(self):
"""A fragment table box must have at least one child box."""
ftbl = glymur.jp2box.FragmentTableBox()
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftbl.write(tfile)
def test_ftbl_child_not_flst(self):
"""A fragment table box can only contain a fragment list."""
free = glymur.jp2box.FreeBox()
ftbl = glymur.jp2box.FragmentTableBox(box=[free])
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftbl.write(tfile)
def test_jpx_rreq_mask_length_3(self):
"""There are some JPX files with rreq mask length of 3."""
jpx = Jp2k(self.jpxfile)
self.assertEqual(jpx.box[2].box_id, 'rreq')
self.assertEqual(type(jpx.box[2]),
glymur.jp2box.ReaderRequirementsBox)
self.assertEqual(jpx.box[2].standard_flag,
(5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20))
def test_free_box(self):
"""Verify that we can handle a free box."""
with warnings.catch_warnings():
# This file has a rreq mask length that we do not recognize.
warnings.simplefilter("ignore")
j = Jp2k(self.jpxfile)
self.assertEqual(j.box[16].box[0].box_id, 'free')
self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox)
def test_dtbl(self):
"""Verify that we can interpret Data Reference boxes."""
# Copy the existing JPX file, add a data reference box onto the end.
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_unknown_superbox(self):
"""Verify that we can handle an unknown superbox."""
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
with open(self.jpxfile, 'rb') as ifile:
tfile.write(ifile.read())
# 8 + 2 + 20 + 36
boxlen = 66
write_buffer = struct.pack('>I4s', boxlen, b'dtbl')
# Add the header for an unknwon superbox.
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
tfile.write(write_buffer)
# Just two boxes.
write_buffer = struct.pack('>H', 2)
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
tfile.write(write_buffer)
# First data entry url box.
# This one will have a URL with 3 null chars at the end.
# They should be stripped off.
write_buffer = struct.pack('>I4s', 36, b'url ')
tfile.write(write_buffer)
url1 = 'file:////usr/local/bin'
write_buffer = struct.pack('>BBBB24s', 0, 0, 0, 0,
(url1 + chr(0) * 3).encode())
tfile.write(write_buffer)
# 2nd data entry url box.
write_buffer = struct.pack('>I4s', 20, b'url ')
tfile.write(write_buffer)
url2 = 'file:///'
write_buffer = struct.pack('>BBBB8s', 0, 0, 0, 0, url2.encode())
tfile.write(write_buffer)
tfile.flush()
with self.assertWarns(UserWarning):
jpx = Jp2k(tfile.name)
self.assertEqual(jpx.box[-1].box_id, 'grp ')
self.assertEqual(jpx.box[-1].box[0].box_id, 'free')
def test_free_box(self):
"""Verify that we can handle a free box."""
j = Jp2k(self.jpxfile)
self.assertEqual(j.box[16].box[0].box_id, 'free')
self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox)
def test_data_reference_requires_dtbl(self):
"""The existance of a data reference box requires a ftbl box as well."""
flag = 0
version = (0, 0, 0)
url1 = 'file:////usr/local/bin'
url2 = 'http://glymur.readthedocs.org'
jpx1 = glymur.Jp2k(self.jp2file)
boxes = jpx1.box
boxes[1].brand = 'jpx '
deurl1 = glymur.jp2box.DataEntryURLBox(flag, version, url1)
deurl2 = glymur.jp2box.DataEntryURLBox(flag, version, url2)
dref = glymur.jp2box.DataReferenceBox([deurl1, deurl2])
boxes.append(dref)
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
with self.assertRaises(IOError):
jpx1.wrap(tfile.name, boxes=boxes)
def test_dtbl(self):
"""Verify that we can interpret Data Reference boxes."""
# Copy the existing JPX file, add a data reference box onto the end.
flag = 0
version = (0, 0, 0)
url1 = 'file:////usr/local/bin'
url2 = 'http://glymur.readthedocs.org' + chr(0) * 3
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
with open(self.jpxfile, 'rb') as ifile:
tfile.write(ifile.read())
deurl1 = glymur.jp2box.DataEntryURLBox(flag, version, url1)
deurl2 = glymur.jp2box.DataEntryURLBox(flag, version, url2)
dref = glymur.jp2box.DataReferenceBox([deurl1, deurl2])
dref.write(tfile)
tfile.flush()
jpx = Jp2k(tfile.name)
self.assertEqual(jpx.box[-1].box_id, 'dtbl')
self.assertEqual(len(jpx.box[-1].DR), 2)
self.assertEqual(jpx.box[-1].DR[0].url, url1)
self.assertEqual(jpx.box[-1].DR[1].url, url2)
self.assertEqual(jpx.box[-1].DR[1].url, url2.rstrip('\0'))
def test_ftbl(self):
"""Verify that we can interpret Fragment Table boxes."""
@ -193,8 +474,7 @@ class TestJPX(unittest.TestCase):
tfile.flush()
with self.assertWarns(UserWarning):
jpx = Jp2k(tfile.name)
jpx = Jp2k(tfile.name)
self.assertEqual(jpx.box[-1].box_id, 'ftbl')
self.assertEqual(jpx.box[-1].box[0].box_id, 'flst')
@ -204,10 +484,7 @@ class TestJPX(unittest.TestCase):
def test_nlst(self):
"""Verify that we can handle a free box."""
with warnings.catch_warnings():
# This file has a rreq mask length that we do not recognize.
warnings.simplefilter("ignore")
j = Jp2k(self.jpxfile)
j = Jp2k(self.jpxfile)
self.assertEqual(j.box[16].box[1].box[0].box_id, 'nlst')
self.assertEqual(type(j.box[16].box[1].box[0]),
glymur.jp2box.NumberListBox)

View file

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
"""Test suite for printing.
"""
# C0302: don't care too much about having too many lines in a test module
# pylint: disable=C0302
# E061: unittest.mock introduced in 3.3 (python-2.7/pylint issue)
# pylint: disable=E0611,F0401
# R0904: Not too many methods in unittest.
# pylint: disable=R0904
import os
import re
import shutil
import struct
import sys
import tempfile
import uuid
import warnings
if sys.hexversion < 0x02070000:
import unittest2 as unittest
else:
import unittest
if sys.hexversion < 0x03000000:
from StringIO import StringIO
else:
from io import StringIO
if sys.hexversion <= 0x03030000:
from mock import patch
else:
from unittest.mock import patch
import lxml.etree
from .fixtures import HAS_PYTHON_XMP_TOOLKIT, OPJ_DATA_ROOT
if HAS_PYTHON_XMP_TOOLKIT:
from libxmp import XMPMeta
import glymur
from glymur import Jp2k
from .fixtures import OPJ_DATA_ROOT, opj_data_file, SimpleRDF
class TestUUIDXMP(unittest.TestCase):
"""Tests for UUIDs of XMP type."""
def setUp(self):
self.jp2file = glymur.data.nemo()
def tearDown(self):
pass
def test_append(self):
"""Should be able to append an XMP UUID box."""
the_uuid = uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
raw_data = SimpleRDF.encode('utf-8')
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
shutil.copyfile(self.jp2file, tfile.name)
jp2 = Jp2k(tfile.name)
ubox = glymur.jp2box.UUIDBox(the_uuid=the_uuid, raw_data=raw_data)
jp2.append(ubox)
# Should be two UUID boxes now.
expected_ids = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'uuid']
actual_ids = [b.box_id for b in jp2.box]
self.assertEqual(actual_ids, expected_ids)
# The data should be an XMP packet, which gets interpreted as
# an ElementTree.
self.assertTrue(isinstance(jp2.box[-1].data,
lxml.etree._ElementTree))
class TestUUIDExif(unittest.TestCase):
"""Tests for UUIDs of Exif type."""
def setUp(self):
self.jp2file = glymur.data.nemo()
def tearDown(self):
pass
@unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+")
def test_unrecognized_exif_tag(self):
"""Verify warning in case of unrecognized tag."""
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
with open(self.jp2file, 'rb') as ifptr:
tfile.write(ifptr.read())
# Write L, T, UUID identifier.
tfile.write(struct.pack('>I4s', 52, b'uuid'))
tfile.write(b'JpgTiffExif->JP2')
tfile.write(b'Exif\x00\x00')
xbuffer = struct.pack('<BBHI', 73, 73, 42, 8)
tfile.write(xbuffer)
# We will write just a single tag.
tfile.write(struct.pack('<H', 1))
# The "Make" tag is tag no. 271. Corrupt it to 171.
tfile.write(struct.pack('<HHI4s', 171, 2, 3, b'HTC\x00'))
tfile.flush()
with self.assertWarns(UserWarning):
j = glymur.Jp2k(tfile.name)
@unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+")
def test_bad_tag_datatype(self):
"""Only certain datatypes are allowable"""
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
with open(self.jp2file, 'rb') as ifptr:
tfile.write(ifptr.read())
# Write L, T, UUID identifier.
tfile.write(struct.pack('>I4s', 52, b'uuid'))
tfile.write(b'JpgTiffExif->JP2')
tfile.write(b'Exif\x00\x00')
xbuffer = struct.pack('<BBHI', 73, 73, 42, 8)
tfile.write(xbuffer)
# We will write just a single tag.
tfile.write(struct.pack('<H', 1))
# 2000 is not an allowable TIFF datatype.
tfile.write(struct.pack('<HHI4s', 271, 2000, 3, b'HTC\x00'))
tfile.flush()
with self.assertWarns(UserWarning):
j = glymur.Jp2k(tfile.name)
self.assertEqual(j.box[-1].box_id, 'uuid')
@unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+")
def test_bad_tiff_header_byte_order_indication(self):
"""Only b'II' and b'MM' are allowed."""
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
with open(self.jp2file, 'rb') as ifptr:
tfile.write(ifptr.read())
# Write L, T, UUID identifier.
tfile.write(struct.pack('>I4s', 52, b'uuid'))
tfile.write(b'JpgTiffExif->JP2')
tfile.write(b'Exif\x00\x00')
xbuffer = struct.pack('<BBHI', 74, 73, 42, 8)
tfile.write(xbuffer)
# We will write just a single tag.
tfile.write(struct.pack('<H', 1))
# 271 is the Make.
tfile.write(struct.pack('<HHI4s', 271, 2, 3, b'HTC\x00'))
tfile.flush()
with self.assertWarns(UserWarning):
j = glymur.Jp2k(tfile.name)
self.assertEqual(j.box[-1].box_id, 'uuid')
def test_big_endian(self):
"""Verify read of big-endian IFD."""
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
with open(self.jp2file, 'rb') as ifptr:
tfile.write(ifptr.read())
# Write L, T, UUID identifier.
tfile.write(struct.pack('>I4s', 52, b'uuid'))
tfile.write(b'JpgTiffExif->JP2')
tfile.write(b'Exif\x00\x00')
xbuffer = struct.pack('>BBHI', 77, 77, 42, 8)
tfile.write(xbuffer)
# We will write just a single tag.
tfile.write(struct.pack('>H', 1))
# The "Make" tag is tag no. 271.
tfile.write(struct.pack('>HHI4s', 271, 2, 3, b'HTC\x00'))
tfile.flush()
jp2 = glymur.Jp2k(tfile.name)
self.assertEqual(jp2.box[-1].data['Make'], "HTC")
if __name__ == "__main__":
unittest.main()

View file

@ -20,7 +20,6 @@ import sys
import tempfile
import unittest
import warnings
import xml.etree.cElementTree as ET
if sys.hexversion < 0x03000000:
from StringIO import StringIO
@ -32,6 +31,8 @@ if sys.hexversion <= 0x03030000:
else:
from unittest.mock import patch
import lxml.etree as ET
import glymur
from glymur import Jp2k
from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox

View file

@ -29,7 +29,11 @@ import pkg_resources
import glymur
from glymur import Jp2k
from .fixtures import OPENJP2_IS_V2_OFFICIAL
from .fixtures import HAS_PYTHON_XMP_TOOLKIT, OPENJP2_IS_V2_OFFICIAL
if HAS_PYTHON_XMP_TOOLKIT:
import libxmp
from libxmp import XMPMeta
from .fixtures import OPJ_DATA_ROOT, opj_data_file
@ -103,7 +107,7 @@ class TestJp2k(unittest.TestCase):
jp2k = Jp2k(self.jp2file)
# top-level boxes
self.assertEqual(len(jp2k.box), 6)
self.assertEqual(len(jp2k.box), 5)
self.assertEqual(jp2k.box[0].box_id, 'jP ')
self.assertEqual(jp2k.box[0].offset, 0)
@ -122,15 +126,11 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(jp2k.box[3].box_id, 'uuid')
self.assertEqual(jp2k.box[3].offset, 77)
self.assertEqual(jp2k.box[3].length, 638)
self.assertEqual(jp2k.box[3].length, 3146)
self.assertEqual(jp2k.box[4].box_id, 'uuid')
self.assertEqual(jp2k.box[4].offset, 715)
self.assertEqual(jp2k.box[4].length, 2412)
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
self.assertEqual(jp2k.box[5].offset, 3127)
self.assertEqual(jp2k.box[5].length, 1132296)
self.assertEqual(jp2k.box[4].box_id, 'jp2c')
self.assertEqual(jp2k.box[4].offset, 3223)
self.assertEqual(jp2k.box[4].length, 1132296)
# jp2h super box
self.assertEqual(len(jp2k.box[2].box), 2)
@ -172,7 +172,7 @@ class TestJp2k(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
write_buffer = ifile.read(3127)
write_buffer = ifile.read(3223)
tfile.write(write_buffer)
# The L field must be 1 in order to signal the presence of the
@ -193,9 +193,9 @@ class TestJp2k(unittest.TestCase):
jp2k = Jp2k(tfile.name)
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
self.assertEqual(jp2k.box[5].offset, 3127)
self.assertEqual(jp2k.box[5].length, 1133427 + 8)
self.assertEqual(jp2k.box[4].box_id, 'jp2c')
self.assertEqual(jp2k.box[4].offset, 3223)
self.assertEqual(jp2k.box[4].length, 1133427 + 8)
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_length_field_is_zero(self):
@ -357,23 +357,29 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()),
b'<test>this is a test</test>')
@unittest.skipIf(not HAS_PYTHON_XMP_TOOLKIT,
"Requires Python XMP Toolkit >= 2.0")
def test_xmp_attribute(self):
"""Verify the XMP packet in the shipping example file can be read."""
j = Jp2k(self.jp2file)
xmp = j.box[4].data
xmp = j.box[3].data
ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
ns1 = '{http://ns.adobe.com/xap/1.0/}'
name = '{0}RDF/{0}Description'.format(ns0)
name = '{0}RDF/{0}Description/{1}CreatorTool'.format(ns0, ns1)
elt = xmp.find(name)
attr_value = elt.attrib['{0}CreatorTool'.format(ns1)]
self.assertEqual(attr_value, 'glymur')
self.assertEqual(elt.text, 'Google')
xmp = XMPMeta()
xmp.parse_from_str(j.box[3].raw_data.decode('utf-8'),
xmpmeta_wrap=False)
creator_tool = xmp.get_property(libxmp.consts.XMP_NS_XMP, 'CreatorTool')
self.assertEqual(creator_tool, 'Google')
def test_jpx_mult_codestreams_jp2_brand(self):
"""Read JPX codestream when jp2-compatible."""
# The file in question has multiple codestreams.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
jpx = Jp2k(self.jpxfile)
jpx = Jp2k(self.jpxfile)
data = jpx.read()
if re.match(r"""1\.[0123]""", glymur.version.openjpeg_version):
# openjpeg 1.3 doesn't apply the palette, so it's a 2D image here
@ -381,38 +387,6 @@ class TestJp2k(unittest.TestCase):
else:
self.assertEqual(data.shape, (1024, 1024, 3))
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_unrecognized_exif_tag(self):
"""An unrecognized exif tag should be handled gracefully."""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
shutil.copyfile(self.jp2file, tfile.name)
# The Exif UUID starts at byte 77. There are 8 bytes for the L and
# T fields, then 16 bytes for the UUID identifier, then 6 exif
# header bytes, then 8 bytes for the TIFF header, then 2 bytes
# the the Image IFD number of tags, where we finally find the first
# tag, "Make" (271). We'll corrupt it by changing it into 171,
# which does not correspond to any known Exif Image tag.
with open(tfile.name, 'r+b') as fptr:
fptr.seek(117)
write_buffer = struct.pack('<H', int(171))
fptr.write(write_buffer)
# Verify that a warning is issued, but only on python3.
# On python2, just suppress the warning.
if sys.hexversion < 0x03030000:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
j = Jp2k(tfile.name)
else:
with self.assertWarns(UserWarning):
j = Jp2k(tfile.name)
exif = j.box[3].data
# Were the tag == 271, 'Make' would be in the keys instead.
self.assertTrue(171 in exif['Image'].keys())
self.assertFalse('Make' in exif['Image'].keys())
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
"Requires at least version 1.5")
@ -749,19 +723,22 @@ class TestJp2k_2_1(unittest.TestCase):
with open(self.jp2file, 'rb') as fptr:
data = fptr.read()
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
# Codestream starts at byte 3127. SIZ marker at 3137.
# COD marker at 3186. Subsampling at 3180.
tfile.write(data[0:3179])
# Codestream starts at byte 3323. SIZ marker at 3233.
# COD marker at 3282. Subsampling at 3276.
offset = 3223
tfile.write(data[0:offset+52])
# Make the DY bytes of the SIZ segment zero. That means that
# a subsampling factor is zero, which is illegal.
tfile.write(b'\x00')
tfile.write(data[3180:3182])
tfile.write(data[offset+53:offset+55])
tfile.write(b'\x00')
tfile.write(data[3184:3186])
tfile.write(data[offset+57:offset+59])
#tfile.write(data[3184:3186])
tfile.write(b'\x00')
tfile.write(data[3186:])
tfile.write(data[offset+59:])
#tfile.write(data[3186:])
tfile.flush()
with warnings.catch_warnings():
warnings.simplefilter("ignore")

View file

@ -12,13 +12,19 @@ import sys
import tempfile
import unittest
try:
import skimage.io
skimage.io.use_plugin('freeimage', 'imread')
_HAS_SKIMAGE_FREEIMAGE_SUPPORT = True
except ((ImportError, RuntimeError)):
_HAS_SKIMAGE_FREEIMAGE_SUPPORT = False
from .fixtures import read_image, NO_READ_BACKEND, NO_READ_BACKEND_MSG
from .fixtures import OPJ_DATA_ROOT, opj_data_file
from glymur import Jp2k
import glymur
@unittest.skipIf(os.name == "nt", "no write support on windows, period")
@unittest.skipIf(re.match(r"""1\.[01234]\.\d""",
glymur.version.openjpeg_version) is not None,
@ -38,6 +44,200 @@ class TestSuiteWrite(unittest.TestCase):
def tearDown(self):
pass
def check_cinema4k_codestream(self, codestream, image_size):
"""Common out for cinema2k tests."""
# SIZ: Image and tile size
# Profile: "3" means cinema2K
self.assertEqual(codestream.segment[1].rsiz, 4)
# Reference grid size
self.assertEqual((codestream.segment[1].xsiz,
codestream.segment[1].ysiz),
image_size)
# Reference grid offset
self.assertEqual((codestream.segment[1].xosiz,
codestream.segment[1].yosiz), (0, 0))
# Tile size
self.assertEqual((codestream.segment[1].xtsiz,
codestream.segment[1].ytsiz),
image_size)
# Tile offset
self.assertEqual((codestream.segment[1].xtosiz,
codestream.segment[1].ytosiz),
(0, 0))
# bitdepth
self.assertEqual(codestream.segment[1].bitdepth, (12, 12, 12))
# signed
self.assertEqual(codestream.segment[1].signed,
(False, False, False))
# subsampling
self.assertEqual(list(zip(codestream.segment[1].xrsiz,
codestream.segment[1].yrsiz)),
[(1, 1)] * 3)
# COD: Coding style default
self.assertFalse(codestream.segment[2].scod & 2) # no sop
self.assertFalse(codestream.segment[2].scod & 4) # no eph
self.assertEqual(codestream.segment[2].spcod[0], glymur.core.CPRL)
self.assertEqual(codestream.segment[2].layers, 1)
self.assertEqual(codestream.segment[2].spcod[3], 1) # mct
self.assertEqual(codestream.segment[2].spcod[4], 5) # levels
self.assertEqual(tuple(codestream.segment[2].code_block_size),
(32, 32)) # cblksz
def check_cinema2k_codestream(self, codestream, image_size):
"""Common out for cinema2k tests."""
# SIZ: Image and tile size
# Profile: "3" means cinema2K
self.assertEqual(codestream.segment[1].rsiz, 3)
# Reference grid size
self.assertEqual((codestream.segment[1].xsiz,
codestream.segment[1].ysiz),
image_size)
# Reference grid offset
self.assertEqual((codestream.segment[1].xosiz,
codestream.segment[1].yosiz), (0, 0))
# Tile size
self.assertEqual((codestream.segment[1].xtsiz,
codestream.segment[1].ytsiz),
image_size)
# Tile offset
self.assertEqual((codestream.segment[1].xtosiz,
codestream.segment[1].ytosiz),
(0, 0))
# bitdepth
self.assertEqual(codestream.segment[1].bitdepth, (12, 12, 12))
# signed
self.assertEqual(codestream.segment[1].signed,
(False, False, False))
# subsampling
self.assertEqual(list(zip(codestream.segment[1].xrsiz,
codestream.segment[1].yrsiz)),
[(1, 1)] * 3)
# COD: Coding style default
self.assertFalse(codestream.segment[2].scod & 2) # no sop
self.assertFalse(codestream.segment[2].scod & 4) # no eph
self.assertEqual(codestream.segment[2].spcod[0], glymur.core.CPRL)
self.assertEqual(codestream.segment[2].layers, 1)
self.assertEqual(codestream.segment[2].spcod[3], 1) # mct
self.assertEqual(codestream.segment[2].spcod[4], 5) # levels
self.assertEqual(tuple(codestream.segment[2].code_block_size),
(32, 32)) # cblksz
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_ElephantDream_4K_tif_21_encode(self):
relfile = 'input/nonregression/ElephantDream_4K.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema4k=True)
codestream = j.get_codestream()
self.check_cinema4k_codestream(codestream, (4096, 2160))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_X_5_2K_24_235_CBR_STEM24_000_tif_19_encode(self):
relfile = 'input/nonregression/X_5_2K_24_235_CBR_STEM24_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema2k=48)
codestream = j.get_codestream()
self.check_cinema2k_codestream(codestream, (2048, 857))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_X_6_2K_24_FULL_CBR_CIRCLE_000_tif_20_encode(self):
relfile = 'input/nonregression/X_6_2K_24_FULL_CBR_CIRCLE_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema2k=48)
codestream = j.get_codestream()
self.check_cinema2k_codestream(codestream, (2048, 1080))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_X_6_2K_24_FULL_CBR_CIRCLE_000_tif_17_encode(self):
relfile = 'input/nonregression/X_6_2K_24_FULL_CBR_CIRCLE_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema2k=24)
codestream = j.get_codestream()
self.check_cinema2k_codestream(codestream, (2048, 1080))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_X_5_2K_24_235_CBR_STEM24_000_tif_16_encode(self):
relfile = 'input/nonregression/X_5_2K_24_235_CBR_STEM24_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema2k=24)
codestream = j.get_codestream()
self.check_cinema2k_codestream(codestream, (2048, 857))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_X_4_2K_24_185_CBR_WB_000_tif_18_encode(self):
relfile = 'input/nonregression/X_4_2K_24_185_CBR_WB_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema2k=48)
codestream = j.get_codestream()
self.check_cinema2k_codestream(codestream, (1998, 1080))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_NR_ENC_X_4_2K_24_185_CBR_WB_000_tif_15_encode(self):
relfile = 'input/nonregression/X_4_2K_24_185_CBR_WB_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data, cinema2k=24)
codestream = j.get_codestream()
self.check_cinema2k_codestream(codestream, (1998, 1080))
@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
"Cannot read input image without scikit-image/freeimage")
def test_cinema2k_bad_frame_rate(self):
relfile = 'input/nonregression/X_4_2K_24_185_CBR_WB_000.tif'
infile = opj_data_file(relfile)
data = skimage.io.imread(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
j.write(data, cinema2k=36)
def test_NR_ENC_Bretagne1_ppm_1_encode(self):
"""NR-ENC-Bretagne1.ppm-1-encode"""
infile = opj_data_file('input/nonregression/Bretagne1.ppm')

View file

@ -11,12 +11,10 @@
# pylint: disable=R0904
import os
import re
import struct
import sys
import tempfile
import warnings
from xml.etree import cElementTree as ET
import unittest
if sys.hexversion < 0x03000000:
@ -29,105 +27,127 @@ if sys.hexversion <= 0x03030000:
else:
from unittest.mock import patch
import lxml.etree as ET
import glymur
from glymur import Jp2k
from . import fixtures
from .fixtures import OPJ_DATA_ROOT, opj_data_file
from .fixtures import text_gbr_27, text_gbr_33, text_gbr_34
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
"Need at least 1.5 in order to write jp2 files.")
class TestPrintingNeedsLib(unittest.TestCase):
"""These tests require the library, mostly in order to just setup the test.
"""
@classmethod
def setUpClass(cls):
# Setup a plain JP2 file without the two UUID boxes.
jp2file = glymur.data.nemo()
with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile:
cls._plain_nemo_file = tfile.name
ijfile = Jp2k(jp2file)
data = ijfile.read(rlevel=1)
ojfile = Jp2k(cls._plain_nemo_file, 'wb')
ojfile.write(data)
@classmethod
def tearDownClass(cls):
os.unlink(cls._plain_nemo_file)
class TestPrinting(unittest.TestCase):
"""Tests for verifying how printing works."""
def setUp(self):
self.jpxfile = glymur.data.jpxfile()
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
# Save the output of dumping nemo.jp2 for more than one test.
lines = ['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: [728 1296 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, 1632355)',
' Main header:',
' SOC marker segment @ (85, 0)',
' SIZ marker segment @ (87, 47)',
' Profile: 2',
' Reference Grid Height, Width: (728 x 1296)',
' Vertical, Horizontal Reference Grid Offset: '
+ '(0 x 0)',
' Reference Tile Height, Width: (728 x 1296)',
' Vertical, Horizontal Reference Tile Offset: '
+ '(0 x 0)',
' Bitdepth: (8, 8, 8)',
' Signed: (False, False, False)',
' Vertical, Horizontal Subsampling: '
+ '((1, 1), (1, 1), (1, 1))',
' COD marker segment @ (136, 12)',
' Coding style:',
' Entropy coder, without partitions',
' SOP marker segments: False',
' EPH marker segments: False',
' Coding style parameters:',
' Progression order: LRCP',
' Number of layers: 1',
' Multiple component transformation usage: '
+ 'reversible',
' Number of resolutions: 6',
' Code block height, width: (64 x 64)',
' Wavelet transform: 5-3 reversible',
' Precinct size: default, 2^15 x 2^15',
' Code block context:',
' Selective arithmetic coding bypass: '
+ 'False',
' Reset context probabilities on '
+ 'coding pass boundaries: False',
' Termination on each coding pass: False',
' Vertically stripe causal context: '
+ 'False',
' Predictable termination: False',
' Segmentation symbols: False',
' QCD marker segment @ (150, 19)',
' Quantization style: no quantization, '
+ '2 guard bits',
' Step size: [(0, 8), (0, 9), (0, 9), '
+ '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), '
+ '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), '
+ '(0, 10)]']
self.expected_plain = '\n'.join(lines)
# Reset printoptions for every test.
glymur.set_printoptions(short=False, xml=True, codestream=True)
def tearDown(self):
pass
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_unknown_superbox(self):
"""Verify that we can handle an unknown superbox."""
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
with open(self.jpxfile, 'rb') as ifile:
tfile.write(ifile.read())
# Add the header for an unknwon superbox.
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
tfile.write(write_buffer)
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
tfile.write(write_buffer)
tfile.flush()
with self.assertWarns(UserWarning):
jpx = Jp2k(tfile.name)
glymur.set_printoptions(short=True)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(jpx.box[-1])
actual = fake_out.getvalue().strip()
lines = ['Unknown Box (grp ) @ (695609, 20)',
' Free Box (free) @ (695617, 12)']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
def test_printoptions_bad_argument(self):
"""Verify error when bad parameter to set_printoptions"""
with self.assertRaises(TypeError):
glymur.set_printoptions(hi='low')
def test_propts_no_codestream_then_no_xml(self):
"""Verify printed output when codestream=False and xml=False, #162"""
# The print options should be persistent across invocations.
glymur.set_printoptions(codestream=False)
glymur.set_printoptions(xml=False)
with patch('sys.stdout', new=StringIO()) as fake_out:
glymur.jp2dump(self.jp2file)
actual = fake_out.getvalue().strip()
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, fixtures.nemo_dump_no_codestream_no_xml)
def test_printopt_no_codestr_or_xml(self):
"""Verify printed output when codestream=False and xml=False"""
glymur.set_printoptions(codestream=False, xml=False)
with patch('sys.stdout', new=StringIO()) as fake_out:
glymur.jp2dump(self.jp2file)
actual = fake_out.getvalue().strip()
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, fixtures.nemo_dump_no_codestream_no_xml)
def test_printoptions_no_codestream(self):
"""Verify printed output when codestream=False"""
glymur.set_printoptions(codestream=False)
with patch('sys.stdout', new=StringIO()) as fake_out:
glymur.jp2dump(self.jp2file)
actual = fake_out.getvalue().strip()
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, fixtures.nemo_dump_no_codestream)
def test_printoptions_no_xml(self):
"""Verify printed output when xml=False"""
glymur.set_printoptions(xml=False)
with patch('sys.stdout', new=StringIO()) as fake_out:
glymur.jp2dump(self.jp2file)
actual = fake_out.getvalue().strip()
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
expected = fixtures.nemo_dump_no_xml
self.assertEqual(actual, expected)
def test_printoptions_short(self):
"""Verify printed output when short=True"""
glymur.set_printoptions(short=True)
with patch('sys.stdout', new=StringIO()) as fake_out:
glymur.jp2dump(self.jp2file)
actual = fake_out.getvalue().strip()
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, fixtures.nemo_dump_short)
def test_asoc_label_box(self):
"""verify printing of asoc, label boxes"""
# Construct a fake file with an asoc and a label box, as
@ -184,18 +204,18 @@ class TestPrintingNeedsLib(unittest.TestCase):
def test_jp2dump(self):
"""basic jp2dump test"""
with patch('sys.stdout', new=StringIO()) as fake_out:
glymur.jp2dump(self._plain_nemo_file)
glymur.jp2dump(self.jp2file)
actual = fake_out.getvalue().strip()
# Get rid of the filename line, as it is not set in stone.
lst = actual.split('\n')
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, self.expected_plain)
self.assertEqual(actual, fixtures.nemo_dump_full)
def test_entire_file(self):
"""verify output from printing entire file"""
j = glymur.Jp2k(self._plain_nemo_file)
j = glymur.Jp2k(self.jp2file)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(j)
actual = fake_out.getvalue().strip()
@ -205,18 +225,7 @@ class TestPrintingNeedsLib(unittest.TestCase):
lst = lst[1:]
actual = '\n'.join(lst)
self.assertEqual(actual, self.expected_plain)
class TestPrinting(unittest.TestCase):
"""Test suite for printing where the libraries are not needed"""
def setUp(self):
# Save sys.stdout.
self.jp2file = glymur.data.nemo()
def tearDown(self):
pass
self.assertEqual(actual, fixtures.nemo_dump_full)
def test_coc_segment(self):
"""verify printing of COC segment"""
@ -226,7 +235,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[6])
actual = fake_out.getvalue().strip()
lines = ['COC marker segment @ (3260, 9)',
lines = ['COC marker segment @ (3356, 9)',
' Associated component: 1',
' Coding style for this component: '
+ 'Entropy coder, PARTITION = 0',
@ -254,7 +263,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[2])
actual = fake_out.getvalue().strip()
lines = ['COD marker segment @ (3186, 12)',
lines = ['COD marker segment @ (3282, 12)',
' Coding style:',
' Entropy coder, without partitions',
' SOP marker segments: False',
@ -280,75 +289,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_icc_profile(self):
"""verify printing of colr box with ICC profile"""
filename = opj_data_file('input/nonregression/text_GBR.jp2')
with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(jp2.box[3].box[1])
actual = fake_out.getvalue().strip()
lin27 = ["Colour Specification Box (colr) @ (179, 1339)",
" Method: any ICC profile",
" Precedence: 2",
" Approximation: accurately represents correct "
+ "colorspace definition",
" ICC Profile:",
" {'Color Space': 'RGB',",
" 'Connection Space': 'XYZ',",
" 'Creator': u'appl',",
" 'Datetime': "
+ "datetime.datetime(2009, 2, 25, 11, 26, 11),",
" 'Device Attributes': 'reflective, glossy, "
+ "positive media polarity, color media',",
" 'Device Class': 'display device profile',",
" 'Device Manufacturer': u'appl',",
" 'Device Model': '',",
" 'File Signature': u'acsp',",
" 'Flags': "
+ "'not embedded, can be used independently',",
" 'Illuminant': "
+ "array([ 0.96420288, 1. , 0.8249054 ]),",
" 'Platform': u'APPL',",
" 'Preferred CMM Type': 1634758764,",
" 'Rendering Intent': 'perceptual',",
" 'Size': 1328,",
" 'Version': '2.2.0'}"]
lin33 = ["Colour Specification Box (colr) @ (179, 1339)",
" Method: any ICC profile",
" Precedence: 2",
" Approximation: accurately represents correct "
+ "colorspace definition",
" ICC Profile:",
" {'Size': 1328,",
" 'Preferred CMM Type': 1634758764,",
" 'Version': '2.2.0',",
" 'Device Class': 'display device profile',",
" 'Color Space': 'RGB',",
" 'Connection Space': 'XYZ',",
" 'Datetime': "
+ "datetime.datetime(2009, 2, 25, 11, 26, 11),",
" 'File Signature': 'acsp',",
" 'Platform': 'APPL',",
" 'Flags': 'not embedded, can be used "
+ "independently',",
" 'Device Manufacturer': 'appl',",
" 'Device Model': '',",
" 'Device Attributes': 'reflective, glossy, "
+ "positive media polarity, color media',",
" 'Rendering Intent': 'perceptual',",
" 'Illuminant': "
+ "array([ 0.96420288, 1. , 0.8249054 ]),",
" 'Creator': 'appl'}"]
lines = lin27 if sys.hexversion < 0x03000000 else lin33
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_crg(self):
@ -420,7 +360,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[-1])
actual = fake_out.getvalue().strip()
lines = ['EOC marker segment @ (1135421, 0)']
lines = ['EOC marker segment @ (1135517, 0)']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@ -517,7 +457,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[7])
actual = fake_out.getvalue().strip()
lines = ['QCC marker segment @ (3271, 8)',
lines = ['QCC marker segment @ (3367, 8)',
' Associated Component: 1',
' Quantization style: no quantization, 2 guard bits',
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
@ -533,7 +473,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[3])
actual = fake_out.getvalue().strip()
lines = ['QCD marker segment @ (3200, 7)',
lines = ['QCD marker segment @ (3296, 7)',
' Quantization style: no quantization, 2 guard bits',
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
@ -548,7 +488,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[1])
actual = fake_out.getvalue().strip()
lines = ['SIZ marker segment @ (3137, 47)',
lines = ['SIZ marker segment @ (3233, 47)',
' Profile: 2',
' Reference Grid Height, Width: (1456 x 2592)',
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
@ -570,7 +510,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[0])
actual = fake_out.getvalue().strip()
lines = ['SOC marker segment @ (3135, 0)']
lines = ['SOC marker segment @ (3231, 0)']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@ -582,7 +522,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[10])
actual = fake_out.getvalue().strip()
lines = ['SOD marker segment @ (3302, 0)']
lines = ['SOD marker segment @ (3398, 0)']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@ -594,7 +534,7 @@ class TestPrinting(unittest.TestCase):
print(codestream.segment[5])
actual = fake_out.getvalue().strip()
lines = ['SOT marker segment @ (3248, 10)',
lines = ['SOT marker segment @ (3344, 10)',
' Tile part index: 0',
' Tile part length: 1132173',
' Tile part instance: 0',
@ -626,22 +566,10 @@ class TestPrinting(unittest.TestCase):
"""Verify the printing of a UUID/XMP box."""
j = glymur.Jp2k(self.jp2file)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[4])
print(j.box[3])
actual = fake_out.getvalue().strip()
lst = ['UUID Box (uuid) @ (715, 2412)',
' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)',
' UUID Data: ',
' <ns0:xmpmeta xmlns:ns0="adobe:ns:meta/" '
+ 'xmlns:ns2="http://ns.adobe.com/xap/1.0/" '
+ 'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '
+ 'ns0:xmptk="XMP Core 4.4.0-Exiv2">',
' <rdf:RDF>',
' <rdf:Description ns2:CreatorTool="glymur" '
+ 'rdf:about="" />',
' </rdf:RDF>',
' </ns0:xmpmeta>']
expected = '\n'.join(lst)
expected = fixtures.nemo_xmp_box
self.assertEqual(actual, expected)
def test_codestream(self):
@ -651,8 +579,8 @@ class TestPrinting(unittest.TestCase):
print(j.get_codestream())
actual = fake_out.getvalue().strip()
lst = ['Codestream:',
' SOC marker segment @ (3135, 0)',
' SIZ marker segment @ (3137, 47)',
' SOC marker segment @ (3231, 0)',
' SIZ marker segment @ (3233, 47)',
' Profile: 2',
' Reference Grid Height, Width: (1456 x 2592)',
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
@ -662,7 +590,7 @@ class TestPrinting(unittest.TestCase):
' Signed: (False, False, False)',
' Vertical, Horizontal Subsampling: '
+ '((1, 1), (1, 1), (1, 1))',
' COD marker segment @ (3186, 12)',
' COD marker segment @ (3282, 12)',
' Coding style:',
' Entropy coder, without partitions',
' SOP marker segments: False',
@ -684,11 +612,11 @@ class TestPrinting(unittest.TestCase):
' Vertically stripe causal context: False',
' Predictable termination: False',
' Segmentation symbols: False',
' QCD marker segment @ (3200, 7)',
' QCD marker segment @ (3296, 7)',
' Quantization style: no quantization, '
+ '2 guard bits',
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]',
' CME marker segment @ (3209, 37)',
' CME marker segment @ (3305, 37)',
' "Created by OpenJPEG version 2.0.0"']
expected = '\n'.join(lst)
self.assertEqual(actual, expected)
@ -702,39 +630,18 @@ class TestPrinting(unittest.TestCase):
with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[2])
actual = fake_out.getvalue().strip()
lines = ['XML Box (xml ) @ (36, 439)',
' <ns0:IMAGE_CREATION '
+ 'xmlns:ns0="http://www.jpeg.org/jpx/1.0/xml" '
+ 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
+ 'xsi:schemaLocation="http://www.jpeg.org/jpx/1.0/xml '
+ 'http://www.jpeg.org/metadata/15444-2.xsd">',
' <ns0:GENERAL_CREATION_INFO>',
' <ns0:CREATION_TIME>'
+ '2001-11-01T13:45:00.000-06:00'
+ '</ns0:CREATION_TIME>',
' <ns0:IMAGE_SOURCE>'
+ 'Professional 120 Image'
+ '</ns0:IMAGE_SOURCE>',
' </ns0:GENERAL_CREATION_INFO>',
' </ns0:IMAGE_CREATION>']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
self.assertEqual(actual, fixtures.file1_xml)
@unittest.skipIf(sys.hexversion < 0x03000000,
"Only trusting python3 for printing non-ascii chars")
def test_xml_latin1(self):
"""Should be able to print an XMLBox with utf-8 encoding (latin1)."""
# Seems to be inconsistencies between different versions of python2.x
# as to what gets printed.
# as to what gets printed.
#
# 2.7.5 (fedora 19) prints xml entities.
# 2.7.3 seems to want to print hex escapes.
text = u"""<?xml version="1.0" encoding="utf-8"?>
<flow>Strömung</flow>"""
text = u"""<flow>Strömung</flow>"""
if sys.hexversion < 0x03000000:
xml = ET.parse(StringIO(text.encode('utf-8')))
else:
@ -758,12 +665,11 @@ class TestPrinting(unittest.TestCase):
def test_xml_cyrrilic(self):
"""Should be able to print an XMLBox with utf-8 encoding (cyrrillic)."""
# Seems to be inconsistencies between different versions of python2.x
# as to what gets printed.
# as to what gets printed.
#
# 2.7.5 (fedora 19) prints xml entities.
# 2.7.3 seems to want to print hex escapes.
text = u"""<?xml version="1.0" encoding="utf-8"?>
<country>Россия</country>"""
text = u"""<country>Россия</country>"""
if sys.hexversion < 0x03000000:
xml = ET.parse(StringIO(text.encode('utf-8')))
else:
@ -838,19 +744,8 @@ class TestPrinting(unittest.TestCase):
with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[2])
actual = fake_out.getvalue().strip()
lines = ['Reader Requirements Box (rreq) @ (44, 24)',
' Standard Features:',
' Feature 005: '
+ 'Unrestricted JPEG 2000 Part 1 codestream, '
+ 'ITU-T Rec. T.800 | ISO/IEC 15444-1',
' Feature 060: e-sRGB enumerated colorspace',
' Feature 043: '
+ '(Deprecated) '
+ 'compositing layer uses restricted ICC profile',
' Vendor Features:']
expected = '\n'.join(lines)
self.maxDiff = None
expected = fixtures.file7_rreq
self.assertEqual(actual, expected)
@unittest.skipIf(OPJ_DATA_ROOT is None,
@ -967,12 +862,10 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@unittest.skipIf(sys.hexversion < 0x03000000,
"Ordered dicts not printing well in 2.7")
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_jpx_approx_icc_profile(self):
"""verify jpx with approx field equal to zero"""
def test_icc_profile(self):
"""verify icc profile printing with a jpx"""
# ICC profiles may be used in JP2, but the approximation field should
# be zero unless we have jpx. This file does both.
filename = opj_data_file('input/nonregression/text_GBR.jp2')
@ -984,34 +877,13 @@ class TestPrinting(unittest.TestCase):
with patch('sys.stdout', new=StringIO()) as fake_out:
print(jp2.box[3].box[1])
actual = fake_out.getvalue().strip()
lines = ["Colour Specification Box (colr) @ (179, 1339)",
" Method: any ICC profile",
" Precedence: 2",
" Approximation: accurately represents "
+ "correct colorspace definition",
" ICC Profile:",
" {'Size': 1328,",
" 'Preferred CMM Type': 1634758764,",
" 'Version': '2.2.0',",
" 'Device Class': 'display device profile',",
" 'Color Space': 'RGB',",
" 'Connection Space': 'XYZ',",
" 'Datetime': "
+ "datetime.datetime(2009, 2, 25, 11, 26, 11),",
" 'File Signature': 'acsp',",
" 'Platform': 'APPL',",
" 'Flags': 'not embedded, "
+ "can be used independently',",
" 'Device Manufacturer': 'appl',",
" 'Device Model': '',",
" 'Device Attributes': 'reflective, glossy, "
+ "positive media polarity, color media',",
" 'Rendering Intent': 'perceptual',",
" 'Illuminant': array([ 0.96420288, 1. ,"
+ " 0.8249054 ]),",
" 'Creator': 'appl'}"]
if sys.hexversion < 0x03000000:
expected = text_gbr_27
elif sys.hexversion < 0x03040000:
expected = text_gbr_33
else:
expected = text_gbr_34
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@unittest.skipIf(OPJ_DATA_ROOT is None,
@ -1028,7 +900,7 @@ class TestPrinting(unittest.TestCase):
print(jp2.box[4])
actual = fake_out.getvalue().strip()
lines = ['UUID Box (uuid) @ (1544, 25)',
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71',
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)',
' UUID Data: 1 bytes']
expected = '\n'.join(lines)
@ -1038,60 +910,37 @@ class TestPrinting(unittest.TestCase):
"Ordered dicts not printing well in 2.7")
def test_exif_uuid(self):
"""Verify printing of exif information"""
j = glymur.Jp2k(self.jp2file)
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[3])
actual = fake_out.getvalue().strip()
with open(self.jp2file, 'rb') as ifptr:
tfile.write(ifptr.read())
lines = ["UUID Box (uuid) @ (77, 638)",
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)",
" UUID Data: ",
"{'Image': {'Make': 'HTC',",
" 'Model': 'HTC Glacier',",
" 'XResolution': 72.0,",
" 'YResolution': 72.0,",
" 'ResolutionUnit': 2,",
" 'YCbCrPositioning': 1,",
" 'ExifTag': 138,",
" 'GPSTag': 354},",
" 'Photo': {'ISOSpeedRatings': 76,",
" 'ExifVersion': (48, 50, 50, 48),",
" 'DateTimeOriginal': '2013:02:09 14:47:53',",
" 'DateTimeDigitized': '2013:02:09 14:47:53',",
" 'ComponentsConfiguration': (1, 2, 3, 0),",
" 'FocalLength': 3.53,",
" 'FlashpixVersion': (48, 49, 48, 48),",
" 'ColorSpace': 1,",
" 'PixelXDimension': 2528,",
" 'PixelYDimension': 1424,",
" 'InteroperabilityTag': 324},",
" 'GPSInfo': {'GPSVersionID': (2, 2, 0),",
" 'GPSLatitudeRef': 'N',",
" 'GPSLatitude': [42.0, 20.0, 33.61],",
" 'GPSLongitudeRef': 'W',",
" 'GPSLongitude': [71.0, 5.0, 17.32],",
" 'GPSAltitudeRef': 0,",
" 'GPSAltitude': 0.0,",
" 'GPSTimeStamp': [19.0, 47.0, 53.0],",
" 'GPSMapDatum': 'WGS-84',",
" 'GPSProcessingMethod': (65,",
" 83,",
" 67,",
" 73,",
" 73,",
" 0,",
" 0,",
" 0,",
" 78,",
" 69,",
" 84,",
" 87,",
" 79,",
" 82,",
" 75),",
" 'GPSDateStamp': '2013:02:09'},",
" 'Iop': None}"]
# Write L, T, UUID identifier.
tfile.write(struct.pack('>I4s', 76, b'uuid'))
tfile.write(b'JpgTiffExif->JP2')
tfile.write(b'Exif\x00\x00')
xbuffer = struct.pack('<BBHI', 73, 73, 42, 8)
tfile.write(xbuffer)
# We will write just three tags.
tfile.write(struct.pack('<H', 3))
# The "Make" tag is tag no. 271.
tfile.write(struct.pack('<HHII', 256, 4, 1, 256))
tfile.write(struct.pack('<HHII', 257, 4, 1, 512))
tfile.write(struct.pack('<HHI4s', 271, 2, 3, b'HTC\x00'))
tfile.flush()
j = glymur.Jp2k(tfile.name)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[5])
actual = fake_out.getvalue().strip()
lines = ["UUID Box (uuid) @ (1135519, 76)",
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (EXIF)",
" UUID Data: OrderedDict([('ImageWidth', 256), ('ImageLength', 512), ('Make', 'HTC')])"]
expected = '\n'.join(lines)

View file

@ -1,10 +1,12 @@
# This file is part of glymur, a Python interface for accessing JPEG 2000.
#
# http://glymur.readthedocs.org
#
# Copyright 2013 John Evans
#
# License: MIT
"""
This file is part of glymur, a Python interface for accessing JPEG 2000.
http://glymur.readthedocs.org
Copyright 2013 John Evans
License: MIT
"""
import sys
import numpy as np

View file

@ -16,7 +16,7 @@ kwargs = {'name': 'Glymur',
'license': 'MIT',
'test_suite': 'glymur.test'}
instllrqrs = ['numpy>=1.4.1']
instllrqrs = ['numpy>=1.4.1', 'lxml>=2.3.2']
if sys.hexversion < 0x03030000:
instllrqrs.append('contextlib2>=0.4')
instllrqrs.append('mock>=1.0.1')
@ -38,7 +38,7 @@ kwargs['classifiers'] = clssfrs
# Get the version string. Cannot do this by importing glymur!
version_file = os.path.join('glymur', 'version.py')
with open('glymur/version.py', 'rt') as fptr:
with open(version_file, 'rt') as fptr:
contents = fptr.read()
match = re.search('version\s*=\s*"(?P<version>\d*.\d*.\d*.*)"\n', contents)
kwargs['version'] = match.group('version')