Merge branch 'issue139' into devel
This commit is contained in:
commit
d74c514302
29 changed files with 3646 additions and 1499 deletions
|
|
@ -11,8 +11,8 @@ before_install:
|
||||||
|
|
||||||
# command to install dependencies
|
# command to install dependencies
|
||||||
install:
|
install:
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors contextlib2 mock; 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 numpy; fi
|
- if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install --use-mirrors lxml numpy; fi
|
||||||
|
|
||||||
# command to run tests
|
# command to run tests
|
||||||
script:
|
script:
|
||||||
|
|
|
||||||
20
CHANGES.txt
20
CHANGES.txt
|
|
@ -1,11 +1,15 @@
|
||||||
Feb 03, 2014 - Removed support for Python 2.6. Added write support for
|
Mar 06, 2014 - Added Cinema2K, Cinema4K write support.
|
||||||
JP2 Palette and Component Mapping boxes, JPX Association and
|
Changed constructor for ChannelDefinition box. Removed support
|
||||||
NumberList boxes. Added read support for JPX free, number list,
|
for Python 2.6. Added write support for JP2 UUID, DataEntryURL,
|
||||||
data reference, fragment table, and fragment list boxes. Palette
|
Palette and Component Mapping boxes, JPX Association, NumberList
|
||||||
box now a 2D numpy array instead of a list of 1D arrays. JP2
|
and DataReference boxes. Added read support for JPX free,
|
||||||
super box constructors now take optional box list argument.
|
number list, data reference, fragment table, and fragment list
|
||||||
Fixed bug where JPX files with more than one codestream but
|
boxes. Improved JPX Reader Requirements box support. Added
|
||||||
advertising jp2 compatibility were not being read.
|
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
|
Jan 28, 2014 - v0.5.10 Fixed bad warning when reader requirements box mask
|
||||||
length is unsupported.
|
length is unsupported.
|
||||||
|
|
|
||||||
23
bin/jp2dump
23
bin/jp2dump
|
|
@ -6,10 +6,29 @@ import glymur
|
||||||
|
|
||||||
description='Print JPEG2000 metadata.'
|
description='Print JPEG2000 metadata.'
|
||||||
parser = argparse.ArgumentParser(description=description)
|
parser = argparse.ArgumentParser(description=description)
|
||||||
parser.add_argument('-c', '--codestream', help='dump codestream',
|
parser.add_argument('-x', '--noxml', help='Suppress XML.',
|
||||||
action='store_true')
|
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')
|
parser.add_argument('filename')
|
||||||
args = parser.parse_args()
|
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
|
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
45
docs/source/changelog.rst
Normal 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
|
||||||
|
|
@ -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.
|
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*
|
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
|
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
|
You should also download the test data for the purpose of configuring
|
||||||
and running OpenJPEG's test suite, check their instructions for all this.
|
and running OpenJPEG's test suite, check their instructions for all this.
|
||||||
You should set the **OPJ_DATA_ROOT** environment variable for the purpose
|
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
|
$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
|
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. ::
|
installed in a non-standard place, i.e. ::
|
||||||
|
|
||||||
|
|
@ -70,14 +63,13 @@ packages/RPMs/ports/whatever without going through pip.
|
||||||
|
|
||||||
Mac OS X
|
Mac OS X
|
||||||
--------
|
--------
|
||||||
All the necessary packages are available to use glymur with Python 2.6, 2.7,
|
All the necessary packages are available to use glymur with MacPorts.
|
||||||
and 3.3 via MacPorts. For python 3.3, you should install the following set of
|
For python 3.3, you should install the following set of ports:
|
||||||
ports:
|
|
||||||
|
|
||||||
* python33
|
* python33
|
||||||
* py33-numpy
|
* py33-numpy
|
||||||
|
* py33-lxml
|
||||||
* py33-distribute
|
* py33-distribute
|
||||||
* py33-matplotlib (optional, for running certain tests)
|
|
||||||
* py33-Pillow (optional, for running certain tests)
|
* py33-Pillow (optional, for running certain tests)
|
||||||
|
|
||||||
MacPorts supplies both OpenJPEG 1.5.0 and OpenJPEG 2.0.0.
|
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.
|
documentation or use pip.
|
||||||
|
|
||||||
* setuptools
|
* setuptools
|
||||||
|
* python-lxml
|
||||||
* matplotlib
|
* matplotlib
|
||||||
* pillow
|
* pillow
|
||||||
* contextlib2 (2.7 only)
|
* contextlib2 (2.7 only)
|
||||||
|
|
@ -108,12 +101,8 @@ difficulties:
|
||||||
|
|
||||||
Windows
|
Windows
|
||||||
-------
|
-------
|
||||||
32-bit WinPython 2.7.5 seemed to work with OpenJPEG 1.X, 2.0, and the
|
The 0.6.x series of Glymur is untested on windows and I make no promises here.
|
||||||
development version, but still required contextlib2 and mock to be
|
I suggest that windows users check the 0.5.x series.
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
'''''''
|
'''''''
|
||||||
|
|
@ -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
|
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
|
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
|
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.
|
errors are reported at the end.
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,15 @@ How do I...?
|
||||||
|
|
||||||
... read the lowest resolution thumbnail?
|
... read the lowest resolution thumbnail?
|
||||||
=========================================
|
=========================================
|
||||||
Printing the Jp2k object should reveal the number of resolutions (look in the
|
Printing the Jp2k object should reveal the number of resolutions
|
||||||
COD segment section), but you can take a shortcut by supplying -1 as the
|
(look in the COD segment section of the codestream), but you can
|
||||||
|
take a shortcut by supplying -1 as the
|
||||||
resolution level. ::
|
resolution level. ::
|
||||||
|
|
||||||
>>> import glymur
|
>>> import glymur
|
||||||
>>> file = glymur.data.nemo()
|
>>> file = glymur.data.nemo()
|
||||||
>>> j = glymur.Jp2k(file)
|
>>> jp2 = glymur.Jp2k(file)
|
||||||
>>> thumbnail = j.read(rlevel=-1)
|
>>> thumbnail = jp2.read(rlevel=-1)
|
||||||
|
|
||||||
... display metadata?
|
... display metadata?
|
||||||
=====================
|
=====================
|
||||||
|
|
@ -23,16 +24,182 @@ available. ::
|
||||||
|
|
||||||
From within Python, it is as simple as printing the Jp2k object, i.e. ::
|
From within Python, it is as simple as printing the Jp2k object, i.e. ::
|
||||||
|
|
||||||
>>> from glymur import Jp2k
|
>>> import glymur
|
||||||
>>> file = glymur.data.nemo()
|
>>> file = glymur.data.nemo()
|
||||||
>>> j = Jp2k(file)
|
>>> jp2 = glymur.Jp2k(file)
|
||||||
>>> print(j)
|
>>> 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"
|
||||||
|
|
||||||
This prints the metadata found in the JP2 boxes, but in the case of the
|
That's fairly overwhelming, and perhaps lost in the flood of information
|
||||||
codestream box, only the main header is printed. It is possible to print
|
is the fact that the codestream metadata is limited to just what's in the
|
||||||
**only** the codestream information as well, i.e. ::
|
main codestream header. You can suppress the codestream and XML details by
|
||||||
|
making use of the :py:meth:`set_printoptions` function::
|
||||||
|
|
||||||
>>> print(j.get_codestream())
|
>>> 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)
|
||||||
|
|
||||||
|
It is possible to print all the gory codestream details as well, i.e. ::
|
||||||
|
|
||||||
|
>>> print(j.get_codestream()) # details not shown
|
||||||
|
|
||||||
... add XML metadata?
|
... add XML metadata?
|
||||||
=====================
|
=====================
|
||||||
|
|
@ -55,12 +222,11 @@ Consider the following XML file `data.xml` : ::
|
||||||
</locality>
|
</locality>
|
||||||
</info>
|
</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 shutil
|
||||||
>>> import glymur
|
>>> import glymur
|
||||||
>>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2')
|
>>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2')
|
||||||
>>> from xml.etree import cElementTree as ET
|
|
||||||
>>> jp2 = glymur.Jp2k('myfile.jp2')
|
>>> jp2 = glymur.Jp2k('myfile.jp2')
|
||||||
>>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml')
|
>>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml')
|
||||||
>>> jp2.append(xmlbox)
|
>>> 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
|
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
|
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),
|
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
|
>>> import glymur
|
||||||
|
>>> glymur.set_printoptions(codestream=False)
|
||||||
>>> jfile = glymur.data.goodstuff()
|
>>> jfile = glymur.data.goodstuff()
|
||||||
>>> j2k = glymur.Jp2k(jfile)
|
>>> j2k = glymur.Jp2k(jfile)
|
||||||
>>> jp2 = j2k.wrap("newfile.jp2")
|
>>> jp2 = j2k.wrap("newfile.jp2")
|
||||||
|
|
@ -96,10 +263,6 @@ you can use the **wrap** method with no box argument: ::
|
||||||
Precedence: 0
|
Precedence: 0
|
||||||
Colorspace: sRGB
|
Colorspace: sRGB
|
||||||
Contiguous Codestream Box (jp2c) @ (77, 115228)
|
Contiguous Codestream Box (jp2c) @ (77, 115228)
|
||||||
Main header:
|
|
||||||
.
|
|
||||||
. (truncated)
|
|
||||||
.
|
|
||||||
|
|
||||||
The raw codestream was wrapped in a JP2 jacket with four boxes in the outer
|
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
|
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
|
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
|
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,
|
re-specify all of the boxes. If you already have a JP2 jacket in place,
|
||||||
though. Take the following example content in an XML file `favorites.xml` : ::
|
you can just reuse that, though. Take the following example content in
|
||||||
|
an XML file `favorites.xml` : ::
|
||||||
|
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<favorite_things>
|
<favorite_things>
|
||||||
|
|
@ -145,17 +309,12 @@ the following will work. ::
|
||||||
<favorite_things>
|
<favorite_things>
|
||||||
<category>Light Ale</category>
|
<category>Light Ale</category>
|
||||||
</favorite_things>
|
</favorite_things>
|
||||||
|
|
||||||
Contiguous Codestream Box (jp2c) @ (153, 115236)
|
Contiguous Codestream Box (jp2c) @ (153, 115236)
|
||||||
Main header:
|
|
||||||
.
|
|
||||||
. (truncated)
|
|
||||||
.
|
|
||||||
|
|
||||||
As to the question of which method you should use, **append** or **wrap**,
|
As to the question of which method you should use, :py:meth:`append` or
|
||||||
to add metadata, you should keep in mind that **wrap** produces a new JP2 file,
|
:py:meth:`wrap`, to add metadata, you should keep in mind that :py:meth:`wrap`
|
||||||
while **append** modifies an existing file and is currently limited to XML
|
produces a new JP2 file, while :py:meth:`append` modifies an existing file and
|
||||||
boxes.
|
is currently limited to XML and UUID boxes.
|
||||||
|
|
||||||
... create an image with an alpha layer?
|
... 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
|
>>> from glymur.core import RED, GREEN, BLUE, WHOLE_IMAGE
|
||||||
>>> asoc = [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)
|
>>> print(cdef)
|
||||||
Channel Definition Box (cdef) @ (0, 0)
|
Channel Definition Box (cdef) @ (0, 0)
|
||||||
Channel 0 (color) ==> (1)
|
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
|
.. 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. ::
|
The example JP2 file shipped with glymur has an XMP UUID. ::
|
||||||
|
|
||||||
>>> import glymur
|
>>> import glymur
|
||||||
>>> j = glymur.Jp2k(glymur.data.nemo())
|
>>> j = glymur.Jp2k(glymur.data.nemo())
|
||||||
>>> print(j.box[4])
|
>>> print(j.box[3]) # formatting added to the XML below
|
||||||
UUID Box (uuid) @ (715, 2412)
|
<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: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
|
||||||
UUID Data:
|
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: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>
|
</rdf:RDF>
|
||||||
</ns0:xmpmeta>
|
</ns0:xmpmeta>
|
||||||
|
|
||||||
Since the UUID data in this case is returned as an ElementTree instance, one can
|
You can also build up XMP metadata from scratch. For instance, if we try to
|
||||||
use ElementTree to access the data. For example, to extract the
|
wrap `goodstuff.j2k` again::
|
||||||
**CreatorTool** attribute value, the following would work::
|
|
||||||
|
>>> 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'
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ Contents:
|
||||||
detailed_installation
|
detailed_installation
|
||||||
how_do_i
|
how_do_i
|
||||||
roadmap
|
roadmap
|
||||||
|
changelog
|
||||||
api
|
api
|
||||||
|
|
||||||
------------------
|
------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
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,
|
In regards to metadata, most JP2 boxes are properly interpreted.
|
||||||
as is reading XMP UUIDs as the XMP data packet is just XML. There is
|
Certain optional JP2 boxes can also be written, including XML boxes and
|
||||||
some very limited support for reading JPX metadata. For instance,
|
XMP UUIDs. There is some very limited support for reading JPX metadata.
|
||||||
**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can
|
|
||||||
be retrieved from such JPX files.
|
|
||||||
|
|
||||||
Glymur 0.6 works on Python versions 2.7, 3.3 and 3.4. If you have 2.6, you should
|
Glymur 0.6 works on Python versions 2.7, 3.3 and 3.4. If you have Python 2.6,
|
||||||
use the 0.5 series.
|
you should use the 0.5 series of Glymur.
|
||||||
|
|
||||||
OpenJPEG Installation
|
OpenJPEG Installation
|
||||||
=====================
|
=====================
|
||||||
Glymur will read JPEG 2000 images with versions 1.3, 1.4, 1.5, 2.0,
|
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
|
and the trunk/development version of OpenJPEG. Writing images is
|
||||||
only supported with the 1.5 or better, however, and the trunk/development
|
only supported with the 1.5 or better, however, and the trunk/development
|
||||||
version is strongly recommended. For more information about OpenJPEG,
|
version of OpenJPEG is strongly recommended. For more information about
|
||||||
please consult http://www.openjpeg.org.
|
OpenJPEG, please consult http://www.openjpeg.org.
|
||||||
|
|
||||||
If you use MacPorts or if you have a sufficiently recent version of
|
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
|
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,
|
OpenJPEG 1.X which glymur can already use.
|
||||||
I suggest using the windows installers provided to you by the OpenJPEG
|
|
||||||
folks at https://code.google.com/p/openjpeg/downloads/list .
|
|
||||||
|
|
||||||
Glymur Installation
|
Glymur Installation
|
||||||
===================
|
===================
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,4 @@ Roadmap
|
||||||
Here's an incomplete list of what I'd like to focus on in the future.
|
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
|
* continue to monitor upstream changes in the openjp2 library
|
||||||
* investigate using CFFI or cython instead of ctypes to wrap openjp2
|
* investigate JPIP (likely to be a big project)
|
||||||
* investigate adding write support for UUID/XMP boxes (potentially a big project)
|
|
||||||
* investigate JPIP (likely to be an even bigger project)
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ __version__ = version.version
|
||||||
|
|
||||||
from .jp2k import Jp2k
|
from .jp2k import Jp2k
|
||||||
from .jp2dump import jp2dump
|
from .jp2dump import jp2dump
|
||||||
|
from .jp2box import get_printoptions, set_printoptions
|
||||||
|
|
||||||
from . import data
|
from . import data
|
||||||
|
|
||||||
|
|
|
||||||
507
glymur/_uuid_io.py
Normal file
507
glymur/_uuid_io.py
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -663,7 +663,6 @@ class Codestream(object):
|
||||||
|
|
||||||
bitdepth = tuple(((x & 0x7f) + 1) for x in data[0::3])
|
bitdepth = tuple(((x & 0x7f) + 1) for x in data[0::3])
|
||||||
signed = tuple(((x & 0x80) > 0) for x in data[0::3])
|
signed = tuple(((x & 0x80) > 0) for x in data[0::3])
|
||||||
|
|
||||||
xrsiz = data[1::3]
|
xrsiz = data[1::3]
|
||||||
yrsiz = data[2::3]
|
yrsiz = data[2::3]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
"""Core definitions to be shared amongst the modules.
|
"""Core definitions to be shared amongst the modules.
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
|
import lxml.etree as ET
|
||||||
|
|
||||||
# Progression order
|
# Progression order
|
||||||
LRCP = 0
|
LRCP = 0
|
||||||
RLCP = 1
|
RLCP = 1
|
||||||
|
|
@ -7,6 +10,26 @@ RPCL = 2
|
||||||
PCRL = 3
|
PCRL = 3
|
||||||
CPRL = 4
|
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 = {
|
PROGRESSION_ORDER = {
|
||||||
'LRCP': LRCP,
|
'LRCP': LRCP,
|
||||||
'RLCP': RLCP,
|
'RLCP': RLCP,
|
||||||
|
|
|
||||||
Binary file not shown.
1405
glymur/jp2box.py
1405
glymur/jp2box.py
File diff suppressed because it is too large
Load diff
370
glymur/jp2k.py
370
glymur/jp2k.py
|
|
@ -16,17 +16,19 @@ if sys.hexversion >= 0x03030000:
|
||||||
else:
|
else:
|
||||||
from contextlib2 import ExitStack
|
from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
import ctypes
|
import ctypes
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
|
from uuid import UUID
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .codestream import Codestream
|
from .codestream import Codestream
|
||||||
from .core import SRGB, GREYSCALE
|
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 .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
|
||||||
from .jp2box import Jp2kBox
|
from .jp2box import Jp2kBox
|
||||||
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
|
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
|
||||||
|
|
@ -37,6 +39,16 @@ from .lib import openjp2 as opj2
|
||||||
from . import version
|
from . import version
|
||||||
from .lib import c as libc
|
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 ',
|
JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
|
||||||
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
|
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
|
||||||
'uuid']
|
'uuid']
|
||||||
|
|
@ -151,6 +163,111 @@ class Jp2k(Jp2kBox):
|
||||||
msg += "profile if the file type box brand is 'jp2 '."
|
msg += "profile if the file type box brand is 'jp2 '."
|
||||||
warnings.warn(msg)
|
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):
|
def _populate_cparams(self, **kwargs):
|
||||||
"""Populate compression parameters structure from input arguments.
|
"""Populate compression parameters structure from input arguments.
|
||||||
|
|
||||||
|
|
@ -217,6 +334,14 @@ class Jp2k(Jp2kBox):
|
||||||
cparams.tcp_numlayers = 1
|
cparams.tcp_numlayers = 1
|
||||||
cparams.cp_disto_alloc = 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:
|
if 'cbsize' in kwargs:
|
||||||
cparams.cblockw_init = kwargs['cbsize'][1]
|
cparams.cblockw_init = kwargs['cbsize'][1]
|
||||||
cparams.cblockh_init = kwargs['cbsize'][0]
|
cparams.cblockh_init = kwargs['cbsize'][0]
|
||||||
|
|
@ -338,6 +463,8 @@ class Jp2k(Jp2kBox):
|
||||||
Image data to be written to file.
|
Image data to be written to file.
|
||||||
cbsize : tuple, optional
|
cbsize : tuple, optional
|
||||||
Code block size (DY, DX).
|
Code block size (DY, DX).
|
||||||
|
cinema2k : int, optional
|
||||||
|
frames per second, either 24 or 48
|
||||||
colorspace : str, optional
|
colorspace : str, optional
|
||||||
Either 'rgb' or 'gray'.
|
Either 'rgb' or 'gray'.
|
||||||
cratios : iterable
|
cratios : iterable
|
||||||
|
|
@ -469,9 +596,51 @@ class Jp2k(Jp2kBox):
|
||||||
self.parse()
|
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):
|
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)
|
cparams, colorspace = self._process_write_inputs(img_array, **kwargs)
|
||||||
|
|
||||||
|
|
@ -488,6 +657,11 @@ class Jp2k(Jp2kBox):
|
||||||
|
|
||||||
_populate_image_struct(cparams, image, img_array)
|
_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)
|
codec = opj2.create_compress(cparams.codec_fmt)
|
||||||
stack.callback(opj2.destroy_codec, codec)
|
stack.callback(opj2.destroy_codec, codec)
|
||||||
|
|
||||||
|
|
@ -522,14 +696,18 @@ class Jp2k(Jp2kBox):
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
box : Jp2Box
|
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:
|
if self._codec_format == opj2.CODEC_J2K:
|
||||||
msg = "Only JP2 files can currently have boxes appended to them."
|
msg = "Only JP2 files can currently have boxes appended to them."
|
||||||
raise IOError(msg)
|
raise IOError(msg)
|
||||||
|
|
||||||
if box.box_id != 'xml ':
|
if not ((box.box_id == 'xml ') or
|
||||||
raise IOError("Only XML boxes can currently be appended.")
|
(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
|
# Check the last box. If the length field is zero, then rewrite
|
||||||
# the length field to reflect the true length of the box.
|
# the length field to reflect the true length of the box.
|
||||||
|
|
@ -1018,7 +1196,7 @@ class Jp2k(Jp2kBox):
|
||||||
>>> jp2 = glymur.Jp2k(jfile)
|
>>> jp2 = glymur.Jp2k(jfile)
|
||||||
>>> codestream = jp2.get_codestream()
|
>>> codestream = jp2.get_codestream()
|
||||||
>>> print(codestream.segment[1])
|
>>> print(codestream.segment[1])
|
||||||
SIZ marker segment @ (3137, 47)
|
SIZ marker segment @ (3233, 47)
|
||||||
Profile: 2
|
Profile: 2
|
||||||
Reference Grid Height, Width: (1456 x 2592)
|
Reference Grid Height, Width: (1456 x 2592)
|
||||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||||
|
|
@ -1104,11 +1282,38 @@ def _validate_nonzero_image_size(nrows, ncols, component_index):
|
||||||
raise IOError(msg)
|
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):
|
def _validate_jp2_box_sequence(boxes):
|
||||||
"""Run through series of tests for JP2 box legality.
|
"""Run through series of tests for JP2 box legality.
|
||||||
|
|
||||||
This is non-exhaustive.
|
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.
|
# Check for a bad sequence of boxes.
|
||||||
# 1st two boxes must be 'jP ' and 'ftyp'
|
# 1st two boxes must be 'jP ' and 'ftyp'
|
||||||
if boxes[0].box_id != 'jP ' or boxes[1].box_id != '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."
|
msg += "must be the file type box."
|
||||||
raise IOError(msg)
|
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
|
# jp2c must be preceeded by jp2h
|
||||||
jp2h_lst = [idx for (idx, box) in enumerate(boxes)
|
jp2h_lst = [idx for (idx, box) in enumerate(boxes)
|
||||||
if box.box_id == 'jp2h']
|
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."
|
msg = "The codestream box must be preceeded by a jp2 header box."
|
||||||
raise IOError(msg)
|
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
|
# 1st jp2 header box must be ihdr
|
||||||
jp2h = boxes[jp2h_idx]
|
|
||||||
if jp2h.box[0].box_id != 'ihdr':
|
if jp2h.box[0].box_id != 'ihdr':
|
||||||
msg = "The first box in the jp2 header box must be the image "
|
msg = "The first box in the jp2 header box must be the image "
|
||||||
msg += "header box."
|
msg += "header box."
|
||||||
|
|
@ -1147,15 +1372,12 @@ def _validate_jp2_box_sequence(boxes):
|
||||||
raise IOError(msg)
|
raise IOError(msg)
|
||||||
colr = jp2h.box[colr_lst[0]]
|
colr = jp2h.box[colr_lst[0]]
|
||||||
|
|
||||||
# Any cdef box must be in the jp2 header following the image header.
|
_validate_channel_definition(jp2h, colr)
|
||||||
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)
|
|
||||||
|
|
||||||
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:
|
if len(cdef_lst) > 1:
|
||||||
msg = "Only one channel definition box is allowed in the "
|
msg = "Only one channel definition box is allowed in the "
|
||||||
msg += "JP2 header."
|
msg += "JP2 header."
|
||||||
|
|
@ -1175,17 +1397,78 @@ def _validate_jp2_box_sequence(boxes):
|
||||||
msg += "channel definition box."
|
msg += "channel definition box."
|
||||||
raise IOError(msg)
|
raise IOError(msg)
|
||||||
|
|
||||||
# The compatibility list must contain at a minimum 'jp2 '.
|
|
||||||
if 'jp2 ' not in boxes[1].compatibility_list:
|
JP2H_CHILDREN = set(['bpcc', 'cdef', 'cmap', 'ihdr', 'pclr'])
|
||||||
msg = "The ftyp box must contain 'jp2 ' in the compatibility list."
|
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)
|
raise IOError(msg)
|
||||||
|
|
||||||
# JPX checks.
|
def _validate_singletons(boxes):
|
||||||
_asoc_check(boxes)
|
"""Several boxes can only occur once."""
|
||||||
_jpx_brand(boxes, boxes[1].brand)
|
count = _collect_box_count(boxes)
|
||||||
_jpx_compatibility(boxes, boxes[1].compatibility_list)
|
# 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 '.
|
If there is a JPX box then the brand must be 'jpx '.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1197,39 +1480,38 @@ def _jpx_brand(boxes, brand):
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
if hasattr(box, 'box') != 0:
|
if hasattr(box, 'box') != 0:
|
||||||
# Same set of checks on any child boxes.
|
# 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 '.
|
If there is a JPX box then the compatibility list must also contain 'jpx '.
|
||||||
"""
|
"""
|
||||||
|
jpx_cl = set(compatibility_list)
|
||||||
for box in boxes:
|
for box in boxes:
|
||||||
if box.box_id in JPX_IDS:
|
if box.box_id in JPX_IDS:
|
||||||
if 'jpx ' not in compatibility_list:
|
if len(set(['jpx ', 'jpxb']).intersection(jpx_cl)) == 0:
|
||||||
msg = "A JPX box requires that 'jpx ' be present in the "
|
msg = "A JPX box requires that either 'jpx ' or 'jpxb' be "
|
||||||
msg += "ftype compatibility list."
|
msg += "present in the ftype compatibility list."
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
if hasattr(box, 'box') != 0:
|
if hasattr(box, 'box') != 0:
|
||||||
# Same set of checks on any child boxes.
|
# Same set of checks on any child boxes.
|
||||||
_jpx_compatibility(box.box, compatibility_list)
|
_validate_jpx_compatibility(box.box, compatibility_list)
|
||||||
|
|
||||||
|
def _validate_label(boxes):
|
||||||
def _asoc_check(boxes):
|
|
||||||
"""
|
"""
|
||||||
Association boxes can only contain number list boxes and xml boxes, as far
|
Label boxes can only be inside association, codestream headers, or
|
||||||
as we know.
|
compositing layer header boxes.
|
||||||
"""
|
"""
|
||||||
for box in boxes:
|
for box in boxes:
|
||||||
if box.box_id == 'asoc':
|
if box.box_id != 'asoc':
|
||||||
if box.box[0].box_id != 'nlst' or box.box[1].box_id != 'xml ':
|
if hasattr(box, 'box'):
|
||||||
msg = "An Association box can only contain a NumberList box "
|
for boxi in box.box:
|
||||||
msg += "followed by an XML box."
|
if boxi.box_id == 'lbl ':
|
||||||
raise RuntimeError(msg)
|
msg = "A label box cannot be nested inside a {0} box."
|
||||||
if hasattr(box, 'box') != 0:
|
msg = msg.format(box.box_id)
|
||||||
|
raise IOError(msg)
|
||||||
# Same set of checks on any child boxes.
|
# Same set of checks on any child boxes.
|
||||||
_asoc_check(box.box)
|
_validate_label(box.box)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extract_image_cube(image):
|
def extract_image_cube(image):
|
||||||
"""Extract 3D image from openjpeg data structure.
|
"""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.
|
# Stage the image data to the openjpeg data structure.
|
||||||
for k in range(0, num_comps):
|
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)
|
layer = np.ascontiguousarray(imgdata[:, :, k], dtype=np.int32)
|
||||||
dest = image.contents.comps[k].data
|
dest = image.contents.comps[k].data
|
||||||
src = layer.ctypes.data
|
src = layer.ctypes.data
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ Test fixtures common to more than one test point.
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -11,6 +12,19 @@ import numpy as np
|
||||||
import glymur
|
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
|
# Need to know of the libopenjp2 version is the official 2.0.0 release and NOT
|
||||||
# the 2.0+ development version.
|
# the 2.0+ development version.
|
||||||
OPENJP2_IS_V2_OFFICIAL = False
|
OPENJP2_IS_V2_OFFICIAL = False
|
||||||
|
|
@ -31,6 +45,32 @@ except:
|
||||||
raise
|
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):
|
def opj_data_file(relative_file_name):
|
||||||
"""Compact way of forming a full filename from OpenJPEG's test suite."""
|
"""Compact way of forming a full filename from OpenJPEG's test suite."""
|
||||||
jfile = os.path.join(OPJ_DATA_ROOT, relative_file_name)
|
jfile = os.path.join(OPJ_DATA_ROOT, relative_file_name)
|
||||||
|
|
@ -167,3 +207,367 @@ def read_pgx_header(pgx_file):
|
||||||
|
|
||||||
header = header.rstrip()
|
header = header.rstrip()
|
||||||
return header, pos
|
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>"""
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import os
|
||||||
from os.path import join
|
from os.path import join
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
|
|
@ -27,6 +28,41 @@ except KeyError:
|
||||||
OPJ_DATA_ROOT = None
|
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,
|
@unittest.skipIf(FORMAT_CORPUS_DATA_ROOT is None,
|
||||||
"FORMAT_CORPUS_DATA_ROOT environment variable not set")
|
"FORMAT_CORPUS_DATA_ROOT environment variable not set")
|
||||||
@unittest.skipIf(sys.hexversion < 0x03020000,
|
@unittest.skipIf(sys.hexversion < 0x03020000,
|
||||||
|
|
@ -34,24 +70,6 @@ except KeyError:
|
||||||
class TestSuiteFormatCorpus(unittest.TestCase):
|
class TestSuiteFormatCorpus(unittest.TestCase):
|
||||||
"""Test suite for files in format corpus repository."""
|
"""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]""",
|
@unittest.skipIf(re.match(r"""1\.[01234]""",
|
||||||
glymur.version.openjpeg_version) is not None,
|
glymur.version.openjpeg_version) is not None,
|
||||||
"Needs 1.4+ to catch this.")
|
"Needs 1.4+ to catch this.")
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
import lxml.etree as ET
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
|
|
@ -52,6 +52,54 @@ def load_tests(loader, tests, ignore):
|
||||||
tests.addTests(doctest.DocTestSuite('glymur.jp2box'))
|
tests.addTests(doctest.DocTestSuite('glymur.jp2box'))
|
||||||
return tests
|
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
|
@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2 or
|
||||||
OPENJP2_IS_V2_OFFICIAL,
|
OPENJP2_IS_V2_OFFICIAL,
|
||||||
"Not supported until 2.0+.")
|
"Not supported until 2.0+.")
|
||||||
|
|
@ -113,7 +161,7 @@ class TestChannelDefinition(unittest.TestCase):
|
||||||
|
|
||||||
def test_cdef_no_inputs(self):
|
def test_cdef_no_inputs(self):
|
||||||
"""channel_type and association are required inputs."""
|
"""channel_type and association are required inputs."""
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(TypeError):
|
||||||
glymur.jp2box.ChannelDefinitionBox()
|
glymur.jp2box.ChannelDefinitionBox()
|
||||||
|
|
||||||
def test_rgb_with_index(self):
|
def test_rgb_with_index(self):
|
||||||
|
|
@ -289,7 +337,7 @@ class TestChannelDefinition(unittest.TestCase):
|
||||||
boxes = [self.jp2b, self.ftyp, self.jp2h, cdef, self.jp2c]
|
boxes = [self.jp2b, self.ftyp, self.jp2h, cdef, self.jp2c]
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises((IOError, OSError)):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
j2k.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
def test_bad_type(self):
|
def test_bad_type(self):
|
||||||
|
|
@ -314,6 +362,29 @@ class TestChannelDefinition(unittest.TestCase):
|
||||||
association=association)
|
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):
|
class TestColourSpecificationBox(unittest.TestCase):
|
||||||
"""Test suite for colr box instantiation."""
|
"""Test suite for colr box instantiation."""
|
||||||
|
|
||||||
|
|
@ -345,7 +416,7 @@ class TestColourSpecificationBox(unittest.TestCase):
|
||||||
boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
|
boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
|
||||||
boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
|
boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(IOError):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
j2k.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
|
|
@ -391,6 +462,46 @@ class TestColourSpecificationBox(unittest.TestCase):
|
||||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||||
approximation=approx)
|
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):
|
class TestAppend(unittest.TestCase):
|
||||||
"""Tests for append method."""
|
"""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
|
# The sequence of box IDs should be the same as before, but with an
|
||||||
# xml box at the end.
|
# xml box at the end.
|
||||||
box_ids = [box.box_id for box in jp2.box]
|
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(box_ids, expected)
|
||||||
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
||||||
b'<data>0</data>')
|
b'<data>0</data>')
|
||||||
|
|
@ -425,14 +536,14 @@ class TestAppend(unittest.TestCase):
|
||||||
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
|
||||||
shutil.copyfile(self.j2kfile, tfile.name)
|
shutil.copyfile(self.j2kfile, tfile.name)
|
||||||
|
|
||||||
jp2 = Jp2k(tfile.name)
|
j2k = Jp2k(tfile.name)
|
||||||
|
|
||||||
# Make a UUID box.
|
# Make an XML box. XML boxes should always be appendable to jp2
|
||||||
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
|
# files.
|
||||||
data = b'0123456789'
|
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||||
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
|
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
jp2.append(uuidbox)
|
j2k.append(xmlbox)
|
||||||
|
|
||||||
def test_length_field_is_zero(self):
|
def test_length_field_is_zero(self):
|
||||||
"""L=0 (length field in box header) is handled.
|
"""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
|
# The sequence of box IDs should be the same as before, but with an
|
||||||
# xml box at the end.
|
# xml box at the end.
|
||||||
box_ids = [box.box_id for box in jp2.box]
|
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(box_ids, expected)
|
||||||
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
||||||
b'<data>0</data>')
|
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."""
|
"""Only XML boxes are allowed to be appended."""
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||||
shutil.copyfile(self.jp2file, tfile.name)
|
shutil.copyfile(self.jp2file, tfile.name)
|
||||||
|
|
||||||
jp2 = Jp2k(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')
|
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
|
||||||
data = b'0123456789'
|
data = b'0123456789'
|
||||||
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
|
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
|
||||||
|
|
@ -484,6 +595,7 @@ class TestAppend(unittest.TestCase):
|
||||||
jp2.append(uuidbox)
|
jp2.append(uuidbox)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
class TestWrap(unittest.TestCase):
|
class TestWrap(unittest.TestCase):
|
||||||
"""Tests for wrap method."""
|
"""Tests for wrap method."""
|
||||||
|
|
||||||
|
|
@ -544,7 +656,6 @@ class TestWrap(unittest.TestCase):
|
||||||
self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB)
|
self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB)
|
||||||
self.assertIsNone(jp2.box[2].box[1].icc_profile)
|
self.assertIsNone(jp2.box[2].box[1].icc_profile)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
|
||||||
def test_wrap(self):
|
def test_wrap(self):
|
||||||
"""basic test for rewrapping a j2c file, no specified boxes"""
|
"""basic test for rewrapping a j2c file, no specified boxes"""
|
||||||
j2k = Jp2k(self.j2kfile)
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
@ -552,12 +663,8 @@ class TestWrap(unittest.TestCase):
|
||||||
j2k.wrap(tfile.name)
|
j2k.wrap(tfile.name)
|
||||||
self.verify_wrapped_raw(tfile.name)
|
self.verify_wrapped_raw(tfile.name)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
|
||||||
def test_jpx_to_jp2(self):
|
def test_jpx_to_jp2(self):
|
||||||
"""basic test for rewrapping a jpx file"""
|
"""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]
|
idx = [0, 1, 3, 6]
|
||||||
boxes = [jpx.box[idx] for idx in [0, 1, 3, 6]]
|
boxes = [jpx.box[idx] for idx in [0, 1, 3, 6]]
|
||||||
|
|
@ -576,7 +683,6 @@ class TestWrap(unittest.TestCase):
|
||||||
for j, offset in enumerate(expected_offsets):
|
for j, offset in enumerate(expected_offsets):
|
||||||
self.assertEqual(jp2.box[j].offset, offset)
|
self.assertEqual(jp2.box[j].offset, offset)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
|
||||||
def test_wrap_jp2(self):
|
def test_wrap_jp2(self):
|
||||||
"""basic test for rewrapping a jp2 file, no specified boxes"""
|
"""basic test for rewrapping a jp2 file, no specified boxes"""
|
||||||
j2k = Jp2k(self.jp2file)
|
j2k = Jp2k(self.jp2file)
|
||||||
|
|
@ -585,7 +691,17 @@ class TestWrap(unittest.TestCase):
|
||||||
boxes = [box.box_id for box in jp2.box]
|
boxes = [box.box_id for box in jp2.box]
|
||||||
self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
|
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):
|
def test_default_layout_with_boxes(self):
|
||||||
"""basic test for rewrapping a jp2 file, boxes specified"""
|
"""basic test for rewrapping a jp2 file, boxes specified"""
|
||||||
j2k = Jp2k(self.j2kfile)
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
@ -605,7 +721,6 @@ class TestWrap(unittest.TestCase):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
j2k.wrap(tfile.name, boxes=boxes)
|
||||||
self.verify_wrapped_raw(tfile.name)
|
self.verify_wrapped_raw(tfile.name)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
|
||||||
def test_ihdr_not_first_in_jp2h(self):
|
def test_ihdr_not_first_in_jp2h(self):
|
||||||
"""The specification says that ihdr must be the first box in jp2h."""
|
"""The specification says that ihdr must be the first box in jp2h."""
|
||||||
j2k = Jp2k(self.j2kfile)
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
@ -625,7 +740,6 @@ class TestWrap(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
j2k.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
|
||||||
def test_first_boxes_jp_and_ftyp(self):
|
def test_first_boxes_jp_and_ftyp(self):
|
||||||
"""first two boxes must be jP followed by ftyp"""
|
"""first two boxes must be jP followed by ftyp"""
|
||||||
j2k = Jp2k(self.j2kfile)
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
@ -647,7 +761,33 @@ class TestWrap(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
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):
|
def test_jp2h_not_preceeding_jp2c(self):
|
||||||
"""jp2h must precede jp2c"""
|
"""jp2h must precede jp2c"""
|
||||||
j2k = Jp2k(self.j2kfile)
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
@ -669,7 +809,6 @@ class TestWrap(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
j2k.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
|
||||||
def test_missing_codestream(self):
|
def test_missing_codestream(self):
|
||||||
"""Need a codestream box in order to call wrap method."""
|
"""Need a codestream box in order to call wrap method."""
|
||||||
j2k = Jp2k(self.j2kfile)
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
@ -916,9 +1055,9 @@ class TestRepr(unittest.TestCase):
|
||||||
tree = ET.ElementTree(elt)
|
tree = ET.ElementTree(elt)
|
||||||
box = glymur.jp2box.XMLBox(xml=tree)
|
box = glymur.jp2box.XMLBox(xml=tree)
|
||||||
|
|
||||||
regexp = "glymur.jp2box.XMLBox"
|
regexp = r"""glymur.jp2box.XMLBox"""
|
||||||
regexp += "\(xml=<(xml.etree.ElementTree.){0,1}ElementTree object "
|
regexp += r"""\(xml=<lxml.etree._ElementTree object """
|
||||||
regexp += "at 0x([a-f0-9]*)>\)"
|
regexp += """at 0x([a-f0-9]*)>\)"""
|
||||||
|
|
||||||
if sys.hexversion < 0x03000000:
|
if sys.hexversion < 0x03000000:
|
||||||
self.assertRegexpMatches(repr(box), regexp)
|
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_feature, newbox.vendor_feature)
|
||||||
self.assertEqual(box.vendor_mask, newbox.vendor_mask)
|
self.assertEqual(box.vendor_mask, newbox.vendor_mask)
|
||||||
|
|
||||||
def test_uuid_box(self):
|
def test_uuid_box_generic(self):
|
||||||
"""Verify uuid repr method."""
|
"""Verify uuid repr method."""
|
||||||
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
|
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
|
||||||
data = b'0123456789'
|
data = b'0123456789'
|
||||||
|
|
@ -948,9 +1087,27 @@ class TestRepr(unittest.TestCase):
|
||||||
|
|
||||||
# Since the raw_data parameter is a sequence of bytes which could be
|
# 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()).
|
# quite long, don't bother trying to make it conform to eval(repr()).
|
||||||
regexp = "glymur.jp2box.UUIDBox\("
|
regexp = r"""glymur.jp2box.UUIDBox\("""
|
||||||
regexp += "the_uuid=UUID\('00000000-0000-0000-0000-000000000000'\),\s"
|
regexp += """the_uuid=UUID\('00000000-0000-0000-0000-000000000000'\),\s"""
|
||||||
regexp += "raw_data=<byte\sarray\s10\selements>\)"
|
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:
|
if sys.hexversion < 0x03000000:
|
||||||
self.assertRegexpMatches(repr(box), regexp)
|
self.assertRegexpMatches(repr(box), regexp)
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import lxml.etree as ET
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
from glymur import Jp2k
|
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.")
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
class TestJPXWrap(unittest.TestCase):
|
class TestJPXWrap(unittest.TestCase):
|
||||||
|
|
@ -21,6 +21,7 @@ class TestJPXWrap(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.jp2file = glymur.data.nemo()
|
self.jp2file = glymur.data.nemo()
|
||||||
|
self.j2kfile = glymur.data.goodstuff()
|
||||||
|
|
||||||
raw_xml = b"""<?xml version="1.0"?>
|
raw_xml = b"""<?xml version="1.0"?>
|
||||||
<data>
|
<data>
|
||||||
|
|
@ -40,14 +41,108 @@ class TestJPXWrap(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
os.unlink(self.xmlfile)
|
os.unlink(self.xmlfile)
|
||||||
|
|
||||||
def test_association_box(self):
|
def test_jpx_ftbl_no_codestream(self):
|
||||||
"""Wrap JP2 to JPX with asoc(nlst, xml)"""
|
"""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)
|
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.
|
# The ftyp box must be modified to jpx with jp2 compatibility.
|
||||||
boxes[1].brand = 'jpx '
|
boxes[1].brand = 'jpx '
|
||||||
boxes[1].compatibility_list = ['jp2 ', 'jpx ']
|
boxes[1].compatibility_list = ['jp2 ', 'jpxb']
|
||||||
|
|
||||||
numbers = (0, 1)
|
numbers = (0, 1)
|
||||||
nlst = glymur.jp2box.NumberListBox(numbers)
|
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||||
|
|
@ -59,7 +154,7 @@ class TestJPXWrap(unittest.TestCase):
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
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_id, 'asoc')
|
||||||
self.assertEqual(jpx.box[-1].box[0].box_id, 'nlst')
|
self.assertEqual(jpx.box[-1].box[0].box_id, 'nlst')
|
||||||
self.assertEqual(jpx.box[-1].box[1].box_id, 'xml ')
|
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()),
|
self.assertEqual(ET.tostring(jpx.box[-1].box[1].xml.getroot()),
|
||||||
b'<data>0</data>')
|
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):
|
def test_jp2_to_jpx_sans_jp2_compatibility(self):
|
||||||
"""jp2 wrapped to jpx not including jp2 compatibility is wrong."""
|
"""jp2 wrapped to jpx not including jp2 compatibility is wrong."""
|
||||||
jp2 = Jp2k(self.jp2file)
|
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 ')
|
boxes[1].compatibility_list.append('jp2 ')
|
||||||
|
|
||||||
numbers = [0, 1]
|
numbers = [0, 1]
|
||||||
nlst = glymur.jp2box.NumberListBox(numbers)
|
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||||
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
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 tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||||
with self.assertRaises(RuntimeError):
|
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):
|
def test_jp2_to_jpx_sans_jpx_brand(self):
|
||||||
"""Verify error when jp2 wrapped to jpx does not include jpx brand."""
|
"""Verify error when jp2 wrapped to jpx does not include jpx brand."""
|
||||||
jp2 = Jp2k(self.jp2file)
|
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 '
|
boxes[1].brand = 'jpx '
|
||||||
numbers = [0, 1]
|
numbers = [0, 1]
|
||||||
nlst = glymur.jp2box.NumberListBox(numbers)
|
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||||
|
|
@ -97,82 +318,142 @@ class TestJPXWrap(unittest.TestCase):
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||||
with self.assertRaises(RuntimeError):
|
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.")
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
class TestJPX(unittest.TestCase):
|
class TestJPX(unittest.TestCase):
|
||||||
"""Test suite for other JPX boxes."""
|
"""Test suite for other JPX boxes."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.jp2file = glymur.data.nemo()
|
||||||
self.jpxfile = glymur.data.jpxfile()
|
self.jpxfile = glymur.data.jpxfile()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_rreq_box_strange_mask_length(self):
|
def test_flst_lens_not_the_same(self):
|
||||||
"""The standard says that the mask length should be 1, 2, 4, or 8."""
|
"""A fragment list box items must be the same length."""
|
||||||
with warnings.catch_warnings():
|
offset = [89]
|
||||||
# This file has a rreq mask length that we do not recognize.
|
length = [1132288]
|
||||||
warnings.simplefilter("ignore")
|
reference = [0, 0]
|
||||||
j = Jp2k(self.jpxfile)
|
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||||
self.assertEqual(j.box[2].box_id, 'rreq')
|
with self.assertRaises(IOError):
|
||||||
self.assertEqual(type(j.box[2]),
|
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)
|
glymur.jp2box.ReaderRequirementsBox)
|
||||||
|
self.assertEqual(jpx.box[2].standard_flag,
|
||||||
|
(5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20))
|
||||||
|
|
||||||
|
@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)
|
||||||
|
self.assertEqual(jpx.box[-1].box_id, 'grp ')
|
||||||
|
self.assertEqual(jpx.box[-1].box[0].box_id, 'free')
|
||||||
|
|
||||||
def test_free_box(self):
|
def test_free_box(self):
|
||||||
"""Verify that we can handle a free box."""
|
"""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[0].box_id, 'free')
|
self.assertEqual(j.box[16].box[0].box_id, 'free')
|
||||||
self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox)
|
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):
|
def test_dtbl(self):
|
||||||
"""Verify that we can interpret Data Reference boxes."""
|
"""Verify that we can interpret Data Reference boxes."""
|
||||||
# Copy the existing JPX file, add a data reference box onto the end.
|
# 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 tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
||||||
with open(self.jpxfile, 'rb') as ifile:
|
with open(self.jpxfile, 'rb') as ifile:
|
||||||
tfile.write(ifile.read())
|
tfile.write(ifile.read())
|
||||||
# 8 + 2 + 20 + 36
|
|
||||||
boxlen = 66
|
|
||||||
|
|
||||||
write_buffer = struct.pack('>I4s', boxlen, b'dtbl')
|
deurl1 = glymur.jp2box.DataEntryURLBox(flag, version, url1)
|
||||||
tfile.write(write_buffer)
|
deurl2 = glymur.jp2box.DataEntryURLBox(flag, version, url2)
|
||||||
|
dref = glymur.jp2box.DataReferenceBox([deurl1, deurl2])
|
||||||
# Just two boxes.
|
dref.write(tfile)
|
||||||
write_buffer = struct.pack('>H', 2)
|
|
||||||
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()
|
tfile.flush()
|
||||||
|
|
||||||
with self.assertWarns(UserWarning):
|
|
||||||
jpx = Jp2k(tfile.name)
|
jpx = Jp2k(tfile.name)
|
||||||
|
|
||||||
self.assertEqual(jpx.box[-1].box_id, 'dtbl')
|
self.assertEqual(jpx.box[-1].box_id, 'dtbl')
|
||||||
self.assertEqual(len(jpx.box[-1].DR), 2)
|
self.assertEqual(len(jpx.box[-1].DR), 2)
|
||||||
self.assertEqual(jpx.box[-1].DR[0].url, url1)
|
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):
|
def test_ftbl(self):
|
||||||
"""Verify that we can interpret Fragment Table boxes."""
|
"""Verify that we can interpret Fragment Table boxes."""
|
||||||
|
|
@ -193,7 +474,6 @@ class TestJPX(unittest.TestCase):
|
||||||
|
|
||||||
tfile.flush()
|
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_id, 'ftbl')
|
||||||
|
|
@ -204,9 +484,6 @@ class TestJPX(unittest.TestCase):
|
||||||
|
|
||||||
def test_nlst(self):
|
def test_nlst(self):
|
||||||
"""Verify that we can handle a free box."""
|
"""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(j.box[16].box[1].box[0].box_id, 'nlst')
|
||||||
self.assertEqual(type(j.box[16].box[1].box[0]),
|
self.assertEqual(type(j.box[16].box[1].box[0]),
|
||||||
|
|
|
||||||
194
glymur/test/test_jp2box_uuid.py
Normal file
194
glymur/test/test_jp2box_uuid.py
Normal 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()
|
||||||
|
|
@ -20,7 +20,6 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
|
|
||||||
if sys.hexversion < 0x03000000:
|
if sys.hexversion < 0x03000000:
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
@ -32,6 +31,8 @@ if sys.hexversion <= 0x03030000:
|
||||||
else:
|
else:
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import lxml.etree as ET
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
from glymur import Jp2k
|
from glymur import Jp2k
|
||||||
from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox
|
from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,11 @@ import pkg_resources
|
||||||
import glymur
|
import glymur
|
||||||
from glymur import Jp2k
|
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
|
from .fixtures import OPJ_DATA_ROOT, opj_data_file
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,7 +107,7 @@ class TestJp2k(unittest.TestCase):
|
||||||
jp2k = Jp2k(self.jp2file)
|
jp2k = Jp2k(self.jp2file)
|
||||||
|
|
||||||
# top-level boxes
|
# 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].box_id, 'jP ')
|
||||||
self.assertEqual(jp2k.box[0].offset, 0)
|
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].box_id, 'uuid')
|
||||||
self.assertEqual(jp2k.box[3].offset, 77)
|
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].box_id, 'jp2c')
|
||||||
self.assertEqual(jp2k.box[4].offset, 715)
|
self.assertEqual(jp2k.box[4].offset, 3223)
|
||||||
self.assertEqual(jp2k.box[4].length, 2412)
|
self.assertEqual(jp2k.box[4].length, 1132296)
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
|
|
||||||
self.assertEqual(jp2k.box[5].offset, 3127)
|
|
||||||
self.assertEqual(jp2k.box[5].length, 1132296)
|
|
||||||
|
|
||||||
# jp2h super box
|
# jp2h super box
|
||||||
self.assertEqual(len(jp2k.box[2].box), 2)
|
self.assertEqual(len(jp2k.box[2].box), 2)
|
||||||
|
|
@ -172,7 +172,7 @@ class TestJp2k(unittest.TestCase):
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
||||||
with open(self.jp2file, 'rb') as ifile:
|
with open(self.jp2file, 'rb') as ifile:
|
||||||
# Everything up until the jp2c box.
|
# Everything up until the jp2c box.
|
||||||
write_buffer = ifile.read(3127)
|
write_buffer = ifile.read(3223)
|
||||||
tfile.write(write_buffer)
|
tfile.write(write_buffer)
|
||||||
|
|
||||||
# The L field must be 1 in order to signal the presence of the
|
# 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)
|
jp2k = Jp2k(tfile.name)
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
|
self.assertEqual(jp2k.box[4].box_id, 'jp2c')
|
||||||
self.assertEqual(jp2k.box[5].offset, 3127)
|
self.assertEqual(jp2k.box[4].offset, 3223)
|
||||||
self.assertEqual(jp2k.box[5].length, 1133427 + 8)
|
self.assertEqual(jp2k.box[4].length, 1133427 + 8)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
|
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
|
||||||
def test_length_field_is_zero(self):
|
def test_length_field_is_zero(self):
|
||||||
|
|
@ -357,22 +357,28 @@ class TestJp2k(unittest.TestCase):
|
||||||
self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()),
|
self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()),
|
||||||
b'<test>this is a test</test>')
|
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):
|
def test_xmp_attribute(self):
|
||||||
"""Verify the XMP packet in the shipping example file can be read."""
|
"""Verify the XMP packet in the shipping example file can be read."""
|
||||||
j = Jp2k(self.jp2file)
|
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#}'
|
ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
|
||||||
ns1 = '{http://ns.adobe.com/xap/1.0/}'
|
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)
|
elt = xmp.find(name)
|
||||||
attr_value = elt.attrib['{0}CreatorTool'.format(ns1)]
|
self.assertEqual(elt.text, 'Google')
|
||||||
self.assertEqual(attr_value, 'glymur')
|
|
||||||
|
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):
|
def test_jpx_mult_codestreams_jp2_brand(self):
|
||||||
"""Read JPX codestream when jp2-compatible."""
|
"""Read JPX codestream when jp2-compatible."""
|
||||||
# The file in question has multiple codestreams.
|
# 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()
|
data = jpx.read()
|
||||||
if re.match(r"""1\.[0123]""", glymur.version.openjpeg_version):
|
if re.match(r"""1\.[0123]""", glymur.version.openjpeg_version):
|
||||||
|
|
@ -381,38 +387,6 @@ class TestJp2k(unittest.TestCase):
|
||||||
else:
|
else:
|
||||||
self.assertEqual(data.shape, (1024, 1024, 3))
|
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),
|
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
|
||||||
"Requires at least version 1.5")
|
"Requires at least version 1.5")
|
||||||
|
|
@ -749,19 +723,22 @@ class TestJp2k_2_1(unittest.TestCase):
|
||||||
with open(self.jp2file, 'rb') as fptr:
|
with open(self.jp2file, 'rb') as fptr:
|
||||||
data = fptr.read()
|
data = fptr.read()
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
||||||
# Codestream starts at byte 3127. SIZ marker at 3137.
|
# Codestream starts at byte 3323. SIZ marker at 3233.
|
||||||
# COD marker at 3186. Subsampling at 3180.
|
# COD marker at 3282. Subsampling at 3276.
|
||||||
tfile.write(data[0:3179])
|
offset = 3223
|
||||||
|
tfile.write(data[0:offset+52])
|
||||||
|
|
||||||
# Make the DY bytes of the SIZ segment zero. That means that
|
# Make the DY bytes of the SIZ segment zero. That means that
|
||||||
# a subsampling factor is zero, which is illegal.
|
# a subsampling factor is zero, which is illegal.
|
||||||
tfile.write(b'\x00')
|
tfile.write(b'\x00')
|
||||||
tfile.write(data[3180:3182])
|
tfile.write(data[offset+53:offset+55])
|
||||||
tfile.write(b'\x00')
|
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(b'\x00')
|
||||||
|
|
||||||
tfile.write(data[3186:])
|
tfile.write(data[offset+59:])
|
||||||
|
#tfile.write(data[3186:])
|
||||||
tfile.flush()
|
tfile.flush()
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,19 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
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 read_image, NO_READ_BACKEND, NO_READ_BACKEND_MSG
|
||||||
from .fixtures import OPJ_DATA_ROOT, opj_data_file
|
from .fixtures import OPJ_DATA_ROOT, opj_data_file
|
||||||
|
|
||||||
from glymur import Jp2k
|
from glymur import Jp2k
|
||||||
import glymur
|
import glymur
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "no write support on windows, period")
|
@unittest.skipIf(os.name == "nt", "no write support on windows, period")
|
||||||
@unittest.skipIf(re.match(r"""1\.[01234]\.\d""",
|
@unittest.skipIf(re.match(r"""1\.[01234]\.\d""",
|
||||||
glymur.version.openjpeg_version) is not None,
|
glymur.version.openjpeg_version) is not None,
|
||||||
|
|
@ -38,6 +44,200 @@ class TestSuiteWrite(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
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):
|
def test_NR_ENC_Bretagne1_ppm_1_encode(self):
|
||||||
"""NR-ENC-Bretagne1.ppm-1-encode"""
|
"""NR-ENC-Bretagne1.ppm-1-encode"""
|
||||||
infile = opj_data_file('input/nonregression/Bretagne1.ppm')
|
infile = opj_data_file('input/nonregression/Bretagne1.ppm')
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,10 @@
|
||||||
# pylint: disable=R0904
|
# pylint: disable=R0904
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from xml.etree import cElementTree as ET
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
if sys.hexversion < 0x03000000:
|
if sys.hexversion < 0x03000000:
|
||||||
|
|
@ -29,105 +27,127 @@ if sys.hexversion <= 0x03030000:
|
||||||
else:
|
else:
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import lxml.etree as ET
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
from glymur import Jp2k
|
from glymur import Jp2k
|
||||||
|
from . import fixtures
|
||||||
from .fixtures import OPJ_DATA_ROOT, opj_data_file
|
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(os.name == "nt", "Temporary file issue on window.")
|
||||||
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
|
class TestPrinting(unittest.TestCase):
|
||||||
"Need at least 1.5 in order to write jp2 files.")
|
"""Tests for verifying how printing works."""
|
||||||
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)
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.jpxfile = glymur.data.jpxfile()
|
||||||
self.jp2file = glymur.data.nemo()
|
self.jp2file = glymur.data.nemo()
|
||||||
self.j2kfile = glymur.data.goodstuff()
|
self.j2kfile = glymur.data.goodstuff()
|
||||||
|
|
||||||
# Save the output of dumping nemo.jp2 for more than one test.
|
# Reset printoptions for every test.
|
||||||
lines = ['JPEG 2000 Signature Box (jP ) @ (0, 12)',
|
glymur.set_printoptions(short=False, xml=True, codestream=True)
|
||||||
' 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)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
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):
|
def test_asoc_label_box(self):
|
||||||
"""verify printing of asoc, label boxes"""
|
"""verify printing of asoc, label boxes"""
|
||||||
# Construct a fake file with an asoc and a label box, as
|
# Construct a fake file with an asoc and a label box, as
|
||||||
|
|
@ -184,18 +204,18 @@ class TestPrintingNeedsLib(unittest.TestCase):
|
||||||
def test_jp2dump(self):
|
def test_jp2dump(self):
|
||||||
"""basic jp2dump test"""
|
"""basic jp2dump test"""
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
glymur.jp2dump(self._plain_nemo_file)
|
glymur.jp2dump(self.jp2file)
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
# Get rid of the filename line, as it is not set in stone.
|
# Get rid of the filename line, as it is not set in stone.
|
||||||
lst = actual.split('\n')
|
lst = actual.split('\n')
|
||||||
lst = lst[1:]
|
lst = lst[1:]
|
||||||
actual = '\n'.join(lst)
|
actual = '\n'.join(lst)
|
||||||
self.assertEqual(actual, self.expected_plain)
|
self.assertEqual(actual, fixtures.nemo_dump_full)
|
||||||
|
|
||||||
def test_entire_file(self):
|
def test_entire_file(self):
|
||||||
"""verify output from printing entire file"""
|
"""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:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j)
|
print(j)
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
@ -205,18 +225,7 @@ class TestPrintingNeedsLib(unittest.TestCase):
|
||||||
lst = lst[1:]
|
lst = lst[1:]
|
||||||
actual = '\n'.join(lst)
|
actual = '\n'.join(lst)
|
||||||
|
|
||||||
self.assertEqual(actual, self.expected_plain)
|
self.assertEqual(actual, fixtures.nemo_dump_full)
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def test_coc_segment(self):
|
def test_coc_segment(self):
|
||||||
"""verify printing of COC segment"""
|
"""verify printing of COC segment"""
|
||||||
|
|
@ -226,7 +235,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[6])
|
print(codestream.segment[6])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['COC marker segment @ (3260, 9)',
|
lines = ['COC marker segment @ (3356, 9)',
|
||||||
' Associated component: 1',
|
' Associated component: 1',
|
||||||
' Coding style for this component: '
|
' Coding style for this component: '
|
||||||
+ 'Entropy coder, PARTITION = 0',
|
+ 'Entropy coder, PARTITION = 0',
|
||||||
|
|
@ -254,7 +263,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[2])
|
print(codestream.segment[2])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['COD marker segment @ (3186, 12)',
|
lines = ['COD marker segment @ (3282, 12)',
|
||||||
' Coding style:',
|
' Coding style:',
|
||||||
' Entropy coder, without partitions',
|
' Entropy coder, without partitions',
|
||||||
' SOP marker segments: False',
|
' SOP marker segments: False',
|
||||||
|
|
@ -280,75 +289,6 @@ class TestPrinting(unittest.TestCase):
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
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,
|
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
||||||
"OPJ_DATA_ROOT environment variable not set")
|
"OPJ_DATA_ROOT environment variable not set")
|
||||||
def test_crg(self):
|
def test_crg(self):
|
||||||
|
|
@ -420,7 +360,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[-1])
|
print(codestream.segment[-1])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['EOC marker segment @ (1135421, 0)']
|
lines = ['EOC marker segment @ (1135517, 0)']
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -517,7 +457,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[7])
|
print(codestream.segment[7])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['QCC marker segment @ (3271, 8)',
|
lines = ['QCC marker segment @ (3367, 8)',
|
||||||
' Associated Component: 1',
|
' Associated Component: 1',
|
||||||
' Quantization style: no quantization, 2 guard bits',
|
' Quantization style: no quantization, 2 guard bits',
|
||||||
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
||||||
|
|
@ -533,7 +473,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[3])
|
print(codestream.segment[3])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['QCD marker segment @ (3200, 7)',
|
lines = ['QCD marker segment @ (3296, 7)',
|
||||||
' Quantization style: no quantization, 2 guard bits',
|
' Quantization style: no quantization, 2 guard bits',
|
||||||
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
||||||
|
|
||||||
|
|
@ -548,7 +488,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[1])
|
print(codestream.segment[1])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SIZ marker segment @ (3137, 47)',
|
lines = ['SIZ marker segment @ (3233, 47)',
|
||||||
' Profile: 2',
|
' Profile: 2',
|
||||||
' Reference Grid Height, Width: (1456 x 2592)',
|
' Reference Grid Height, Width: (1456 x 2592)',
|
||||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||||
|
|
@ -570,7 +510,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[0])
|
print(codestream.segment[0])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SOC marker segment @ (3135, 0)']
|
lines = ['SOC marker segment @ (3231, 0)']
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -582,7 +522,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[10])
|
print(codestream.segment[10])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SOD marker segment @ (3302, 0)']
|
lines = ['SOD marker segment @ (3398, 0)']
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -594,7 +534,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[5])
|
print(codestream.segment[5])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SOT marker segment @ (3248, 10)',
|
lines = ['SOT marker segment @ (3344, 10)',
|
||||||
' Tile part index: 0',
|
' Tile part index: 0',
|
||||||
' Tile part length: 1132173',
|
' Tile part length: 1132173',
|
||||||
' Tile part instance: 0',
|
' Tile part instance: 0',
|
||||||
|
|
@ -626,22 +566,10 @@ class TestPrinting(unittest.TestCase):
|
||||||
"""Verify the printing of a UUID/XMP box."""
|
"""Verify the printing of a UUID/XMP box."""
|
||||||
j = glymur.Jp2k(self.jp2file)
|
j = glymur.Jp2k(self.jp2file)
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j.box[4])
|
print(j.box[3])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lst = ['UUID Box (uuid) @ (715, 2412)',
|
expected = fixtures.nemo_xmp_box
|
||||||
' 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)
|
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
def test_codestream(self):
|
def test_codestream(self):
|
||||||
|
|
@ -651,8 +579,8 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(j.get_codestream())
|
print(j.get_codestream())
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lst = ['Codestream:',
|
lst = ['Codestream:',
|
||||||
' SOC marker segment @ (3135, 0)',
|
' SOC marker segment @ (3231, 0)',
|
||||||
' SIZ marker segment @ (3137, 47)',
|
' SIZ marker segment @ (3233, 47)',
|
||||||
' Profile: 2',
|
' Profile: 2',
|
||||||
' Reference Grid Height, Width: (1456 x 2592)',
|
' Reference Grid Height, Width: (1456 x 2592)',
|
||||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||||
|
|
@ -662,7 +590,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
' Signed: (False, False, False)',
|
' Signed: (False, False, False)',
|
||||||
' Vertical, Horizontal Subsampling: '
|
' Vertical, Horizontal Subsampling: '
|
||||||
+ '((1, 1), (1, 1), (1, 1))',
|
+ '((1, 1), (1, 1), (1, 1))',
|
||||||
' COD marker segment @ (3186, 12)',
|
' COD marker segment @ (3282, 12)',
|
||||||
' Coding style:',
|
' Coding style:',
|
||||||
' Entropy coder, without partitions',
|
' Entropy coder, without partitions',
|
||||||
' SOP marker segments: False',
|
' SOP marker segments: False',
|
||||||
|
|
@ -684,11 +612,11 @@ class TestPrinting(unittest.TestCase):
|
||||||
' Vertically stripe causal context: False',
|
' Vertically stripe causal context: False',
|
||||||
' Predictable termination: False',
|
' Predictable termination: False',
|
||||||
' Segmentation symbols: False',
|
' Segmentation symbols: False',
|
||||||
' QCD marker segment @ (3200, 7)',
|
' QCD marker segment @ (3296, 7)',
|
||||||
' Quantization style: no quantization, '
|
' Quantization style: no quantization, '
|
||||||
+ '2 guard bits',
|
+ '2 guard bits',
|
||||||
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]',
|
' 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"']
|
' "Created by OpenJPEG version 2.0.0"']
|
||||||
expected = '\n'.join(lst)
|
expected = '\n'.join(lst)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
@ -702,27 +630,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j.box[2])
|
print(j.box[2])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
self.assertEqual(actual, fixtures.file1_xml)
|
||||||
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)
|
|
||||||
|
|
||||||
@unittest.skipIf(sys.hexversion < 0x03000000,
|
@unittest.skipIf(sys.hexversion < 0x03000000,
|
||||||
"Only trusting python3 for printing non-ascii chars")
|
"Only trusting python3 for printing non-ascii chars")
|
||||||
|
|
@ -733,8 +641,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
#
|
#
|
||||||
# 2.7.5 (fedora 19) prints xml entities.
|
# 2.7.5 (fedora 19) prints xml entities.
|
||||||
# 2.7.3 seems to want to print hex escapes.
|
# 2.7.3 seems to want to print hex escapes.
|
||||||
text = u"""<?xml version="1.0" encoding="utf-8"?>
|
text = u"""<flow>Strömung</flow>"""
|
||||||
<flow>Strömung</flow>"""
|
|
||||||
if sys.hexversion < 0x03000000:
|
if sys.hexversion < 0x03000000:
|
||||||
xml = ET.parse(StringIO(text.encode('utf-8')))
|
xml = ET.parse(StringIO(text.encode('utf-8')))
|
||||||
else:
|
else:
|
||||||
|
|
@ -762,8 +669,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
#
|
#
|
||||||
# 2.7.5 (fedora 19) prints xml entities.
|
# 2.7.5 (fedora 19) prints xml entities.
|
||||||
# 2.7.3 seems to want to print hex escapes.
|
# 2.7.3 seems to want to print hex escapes.
|
||||||
text = u"""<?xml version="1.0" encoding="utf-8"?>
|
text = u"""<country>Россия</country>"""
|
||||||
<country>Россия</country>"""
|
|
||||||
if sys.hexversion < 0x03000000:
|
if sys.hexversion < 0x03000000:
|
||||||
xml = ET.parse(StringIO(text.encode('utf-8')))
|
xml = ET.parse(StringIO(text.encode('utf-8')))
|
||||||
else:
|
else:
|
||||||
|
|
@ -838,19 +744,8 @@ class TestPrinting(unittest.TestCase):
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j.box[2])
|
print(j.box[2])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lines = ['Reader Requirements Box (rreq) @ (44, 24)',
|
self.maxDiff = None
|
||||||
' Standard Features:',
|
expected = fixtures.file7_rreq
|
||||||
' 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.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
||||||
|
|
@ -967,12 +862,10 @@ class TestPrinting(unittest.TestCase):
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
@unittest.skipIf(sys.hexversion < 0x03000000,
|
|
||||||
"Ordered dicts not printing well in 2.7")
|
|
||||||
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
||||||
"OPJ_DATA_ROOT environment variable not set")
|
"OPJ_DATA_ROOT environment variable not set")
|
||||||
def test_jpx_approx_icc_profile(self):
|
def test_icc_profile(self):
|
||||||
"""verify jpx with approx field equal to zero"""
|
"""verify icc profile printing with a jpx"""
|
||||||
# ICC profiles may be used in JP2, but the approximation field should
|
# ICC profiles may be used in JP2, but the approximation field should
|
||||||
# be zero unless we have jpx. This file does both.
|
# be zero unless we have jpx. This file does both.
|
||||||
filename = opj_data_file('input/nonregression/text_GBR.jp2')
|
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:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(jp2.box[3].box[1])
|
print(jp2.box[3].box[1])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lines = ["Colour Specification Box (colr) @ (179, 1339)",
|
if sys.hexversion < 0x03000000:
|
||||||
" Method: any ICC profile",
|
expected = text_gbr_27
|
||||||
" Precedence: 2",
|
elif sys.hexversion < 0x03040000:
|
||||||
" Approximation: accurately represents "
|
expected = text_gbr_33
|
||||||
+ "correct colorspace definition",
|
else:
|
||||||
" ICC Profile:",
|
expected = text_gbr_34
|
||||||
" {'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'}"]
|
|
||||||
|
|
||||||
expected = '\n'.join(lines)
|
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
@unittest.skipIf(OPJ_DATA_ROOT is None,
|
||||||
|
|
@ -1028,7 +900,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(jp2.box[4])
|
print(jp2.box[4])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lines = ['UUID Box (uuid) @ (1544, 25)',
|
lines = ['UUID Box (uuid) @ (1544, 25)',
|
||||||
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71',
|
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)',
|
||||||
' UUID Data: 1 bytes']
|
' UUID Data: 1 bytes']
|
||||||
|
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
|
|
@ -1038,60 +910,37 @@ class TestPrinting(unittest.TestCase):
|
||||||
"Ordered dicts not printing well in 2.7")
|
"Ordered dicts not printing well in 2.7")
|
||||||
def test_exif_uuid(self):
|
def test_exif_uuid(self):
|
||||||
"""Verify printing of exif information"""
|
"""Verify printing of exif information"""
|
||||||
j = glymur.Jp2k(self.jp2file)
|
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', 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:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j.box[3])
|
print(j.box[5])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ["UUID Box (uuid) @ (77, 638)",
|
lines = ["UUID Box (uuid) @ (1135519, 76)",
|
||||||
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)",
|
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (EXIF)",
|
||||||
" UUID Data: ",
|
" UUID Data: OrderedDict([('ImageWidth', 256), ('ImageLength', 512), ('Make', 'HTC')])"]
|
||||||
"{'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}"]
|
|
||||||
|
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
# This file is part of glymur, a Python interface for accessing JPEG 2000.
|
"""
|
||||||
#
|
This file is part of glymur, a Python interface for accessing JPEG 2000.
|
||||||
# http://glymur.readthedocs.org
|
|
||||||
#
|
http://glymur.readthedocs.org
|
||||||
# Copyright 2013 John Evans
|
|
||||||
#
|
Copyright 2013 John Evans
|
||||||
# License: MIT
|
|
||||||
|
License: MIT
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
|
||||||
4
setup.py
4
setup.py
|
|
@ -16,7 +16,7 @@ kwargs = {'name': 'Glymur',
|
||||||
'license': 'MIT',
|
'license': 'MIT',
|
||||||
'test_suite': 'glymur.test'}
|
'test_suite': 'glymur.test'}
|
||||||
|
|
||||||
instllrqrs = ['numpy>=1.4.1']
|
instllrqrs = ['numpy>=1.4.1', 'lxml>=2.3.2']
|
||||||
if sys.hexversion < 0x03030000:
|
if sys.hexversion < 0x03030000:
|
||||||
instllrqrs.append('contextlib2>=0.4')
|
instllrqrs.append('contextlib2>=0.4')
|
||||||
instllrqrs.append('mock>=1.0.1')
|
instllrqrs.append('mock>=1.0.1')
|
||||||
|
|
@ -38,7 +38,7 @@ kwargs['classifiers'] = clssfrs
|
||||||
|
|
||||||
# Get the version string. Cannot do this by importing glymur!
|
# Get the version string. Cannot do this by importing glymur!
|
||||||
version_file = os.path.join('glymur', 'version.py')
|
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()
|
contents = fptr.read()
|
||||||
match = re.search('version\s*=\s*"(?P<version>\d*.\d*.\d*.*)"\n', contents)
|
match = re.search('version\s*=\s*"(?P<version>\d*.\d*.\d*.*)"\n', contents)
|
||||||
kwargs['version'] = match.group('version')
|
kwargs['version'] = match.group('version')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue