Merge branch 'release-0.4.0'
This commit is contained in:
commit
2b5dd29a48
13 changed files with 195 additions and 19 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
Aug 18, 2013 - v0.4.0 Added append method.
|
||||||
|
|
||||||
Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test
|
Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test
|
||||||
failures.
|
failures.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,9 @@ copyright = u'2013, John Evans'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.3'
|
version = '0.4'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.3.2'
|
release = '0.4.0'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ 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 (look in the
|
||||||
COD segment section), but you can take a shortcut by supplying -1 as the
|
COD segment section), but you can take a shortcut by supplying -1 as the
|
||||||
|
|
@ -14,7 +14,7 @@ resolution level. ::
|
||||||
>>> j = glymur.Jp2k(file)
|
>>> j = glymur.Jp2k(file)
|
||||||
>>> thumbnail = j.read(rlevel=-1)
|
>>> thumbnail = j.read(rlevel=-1)
|
||||||
|
|
||||||
Display metadata?
|
display metadata?
|
||||||
=================
|
=================
|
||||||
There are two ways. From the unix command line, the script *jp2dump* is
|
There are two ways. From the unix command line, the script *jp2dump* is
|
||||||
available. ::
|
available. ::
|
||||||
|
|
@ -34,8 +34,41 @@ codestream box, only the main header is printed. It is possible to print
|
||||||
|
|
||||||
>>> print(j.get_codestream())
|
>>> print(j.get_codestream())
|
||||||
|
|
||||||
Add XML Metadata?
|
add XML metadata?
|
||||||
=================
|
=================
|
||||||
|
You can append any number of XML boxes to a JP2 file (not to a raw codestream).
|
||||||
|
Consider the following XML file `data.xml` : ::
|
||||||
|
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<info>
|
||||||
|
<locality>
|
||||||
|
<city>Boston</city>
|
||||||
|
<snowfall>24.9 inches</snowfall>
|
||||||
|
</locality>
|
||||||
|
<locality>
|
||||||
|
<city>Portland</city>
|
||||||
|
<snowfall>31.9 inches</snowfall>
|
||||||
|
</locality>
|
||||||
|
<locality>
|
||||||
|
<city>New York City</city>
|
||||||
|
<snowfall>11.4 inches</snowfall>
|
||||||
|
</locality>
|
||||||
|
</info>
|
||||||
|
|
||||||
|
The **append** method can add an XML box (only XML boxes are currently
|
||||||
|
allowed)::
|
||||||
|
|
||||||
|
>>> import shutil
|
||||||
|
>>> import glymur
|
||||||
|
>>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2')
|
||||||
|
>>> from xml.etree import cElementTree as ET
|
||||||
|
>>> jp2 = glymur.Jp2k('myfile.jp2')
|
||||||
|
>>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml')
|
||||||
|
>>> jp2.append(xmlbox)
|
||||||
|
>>> print(jp2)
|
||||||
|
|
||||||
|
add metadata in a more general fashion?
|
||||||
|
=======================================
|
||||||
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),
|
||||||
|
|
@ -87,7 +120,7 @@ though. Take the following example content in an XML file `favorites.xml` : ::
|
||||||
and add it after the JP2 header box, but before the codestream box ::
|
and add it after the JP2 header box, but before the codestream box ::
|
||||||
|
|
||||||
>>> boxes = jp2.box # The box attribute is the list of JP2 boxes
|
>>> boxes = jp2.box # The box attribute is the list of JP2 boxes
|
||||||
>>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml')
|
>>> xmlbox = glymur.jp2box.XMLBox(filename='favorites.xml')
|
||||||
>>> boxes.insert(3, xmlbox)
|
>>> boxes.insert(3, xmlbox)
|
||||||
>>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
|
>>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
|
||||||
>>> print(jp2_xml)
|
>>> print(jp2_xml)
|
||||||
|
|
@ -119,7 +152,12 @@ and add it after the JP2 header box, but before the codestream box ::
|
||||||
. (truncated)
|
. (truncated)
|
||||||
.
|
.
|
||||||
|
|
||||||
Create an image with an alpha layer?
|
As to the question of which method you should use, **append** or **wrap**,
|
||||||
|
to add metadata, you should keep in mind that **wrap** produces a new JP2 file,
|
||||||
|
while **append** modifies an existing file and is currently limited to XML
|
||||||
|
boxes.
|
||||||
|
|
||||||
|
create an image with an alpha layer?
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
OpenJPEG can create JP2 files with more than 3 components (requires
|
OpenJPEG can create JP2 files with more than 3 components (requires
|
||||||
|
|
@ -181,7 +219,7 @@ 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?
|
||||||
====================
|
====================
|
||||||
The example JP2 file shipped with glymur has an XMP UUID. ::
|
The example JP2 file shipped with glymur has an XMP UUID. ::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1308,7 +1308,7 @@ class QCCsegment(Segment):
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
|
|
||||||
self.mantissa, self.exponent = parse_quantization(self.spqcc,
|
self.mantissa, self.exponent = parse_quantization(self.spqcc,
|
||||||
self.sqcc)
|
self.sqcc)
|
||||||
self.guard_bits = (self.sqcc & 0xe0) >> 5
|
self.guard_bits = (self.sqcc & 0xe0) >> 5
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,42 @@ class Jp2k(Jp2kBox):
|
||||||
# Refresh the metadata.
|
# Refresh the metadata.
|
||||||
self.parse()
|
self.parse()
|
||||||
|
|
||||||
|
def append(self, box):
|
||||||
|
"""Append a JP2 box to the file in-place.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
box : Jp2Box
|
||||||
|
Instance of a JP2 box. Currently only XML boxes are allowed.
|
||||||
|
"""
|
||||||
|
if self._codec_format == opj2.CODEC_J2K:
|
||||||
|
msg = "Only JP2 files can currently have boxes appended to them."
|
||||||
|
raise IOError(msg)
|
||||||
|
|
||||||
|
if box.box_id != 'xml ':
|
||||||
|
raise IOError("Only XML boxes can currently be appended.")
|
||||||
|
|
||||||
|
# Check the last box. If the length field is zero, then rewrite
|
||||||
|
# the length field to reflect the true length of the box.
|
||||||
|
with open(self.filename, 'rb') as ifile:
|
||||||
|
offset = self.box[-1].offset
|
||||||
|
ifile.seek(offset)
|
||||||
|
read_buffer = ifile.read(4)
|
||||||
|
box_length, = struct.unpack('>I', read_buffer)
|
||||||
|
if box_length == 0:
|
||||||
|
# Reopen the file in write mode and rewrite the length field.
|
||||||
|
true_box_length = os.path.getsize(ifile.name) - offset
|
||||||
|
with open(self.filename, 'r+b') as ofile:
|
||||||
|
ofile.seek(offset)
|
||||||
|
write_buffer = struct.pack('>I', true_box_length)
|
||||||
|
ofile.write(write_buffer)
|
||||||
|
|
||||||
|
# Can now safely append the box.
|
||||||
|
with open(self.filename, 'ab') as ofile:
|
||||||
|
box.write(ofile)
|
||||||
|
|
||||||
|
self.parse()
|
||||||
|
|
||||||
def wrap(self, filename, boxes=None):
|
def wrap(self, filename, boxes=None):
|
||||||
"""Write the codestream back out to file, wrapped in new JP2 jacket.
|
"""Write the codestream back out to file, wrapped in new JP2 jacket.
|
||||||
|
|
||||||
|
|
@ -1133,6 +1169,7 @@ def _unpack_colorspace(colorspace, img_array, cparams):
|
||||||
|
|
||||||
return colorspace
|
return colorspace
|
||||||
|
|
||||||
|
|
||||||
def _populate_comptparms(img_array, cparams):
|
def _populate_comptparms(img_array, cparams):
|
||||||
"""Instantiate and populate comptparms structure.
|
"""Instantiate and populate comptparms structure.
|
||||||
|
|
||||||
|
|
@ -1171,6 +1208,7 @@ def _populate_comptparms(img_array, cparams):
|
||||||
|
|
||||||
return comptparms
|
return comptparms
|
||||||
|
|
||||||
|
|
||||||
def _populate_image_struct(cparams, image, imgdata):
|
def _populate_image_struct(cparams, image, imgdata):
|
||||||
"""Populates image struct needed for compression.
|
"""Populates image struct needed for compression.
|
||||||
|
|
||||||
|
|
@ -1203,6 +1241,7 @@ def _populate_image_struct(cparams, image, imgdata):
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def _validate_compression_params(img_array, cparams):
|
def _validate_compression_params(img_array, cparams):
|
||||||
"""Check that the compression parameters are valid.
|
"""Check that the compression parameters are valid.
|
||||||
|
|
||||||
|
|
@ -1313,4 +1352,3 @@ class LibraryNotFoundError(IOError):
|
||||||
"""
|
"""
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
IOError.__init__(self, msg)
|
IOError.__init__(self, msg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ NO_READ_BACKEND_MSG += "order to run the tests in this suite."
|
||||||
try:
|
try:
|
||||||
from matplotlib.pyplot import imread
|
from matplotlib.pyplot import imread
|
||||||
|
|
||||||
# The whole point of trying to import PIL is to determine if it's there
|
# The whole point of trying to import PIL is to determine if it's there
|
||||||
# or not. We won't use it directly.
|
# or not. We won't use it directly.
|
||||||
# pylint: disable=F0401,W0611
|
# pylint: disable=F0401,W0611
|
||||||
import PIL
|
import PIL
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ class TestCallbacks15(unittest.TestCase):
|
||||||
|
|
||||||
def test_info_callbacks_on_read(self):
|
def test_info_callbacks_on_read(self):
|
||||||
"""Verify stdout when reading.
|
"""Verify stdout when reading.
|
||||||
|
|
||||||
Verify that we get the expected stdio output when our internal info
|
Verify that we get the expected stdio output when our internal info
|
||||||
callback handler is enabled.
|
callback handler is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Test suite for codestream parsing.
|
||||||
# unittest doesn't work well with R0904.
|
# unittest doesn't work well with R0904.
|
||||||
# pylint: disable=R0904
|
# pylint: disable=R0904
|
||||||
|
|
||||||
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
|
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
|
||||||
# pylint: disable=E1101
|
# pylint: disable=E1101
|
||||||
|
|
||||||
# unittest2 is python2.6 only (pylint/python-2.7)
|
# unittest2 is python2.6 only (pylint/python-2.7)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ OPENJP2 may be present in some form or other.
|
||||||
# unittest doesn't work well with R0904.
|
# unittest doesn't work well with R0904.
|
||||||
# pylint: disable=R0904
|
# pylint: disable=R0904
|
||||||
|
|
||||||
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
|
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
|
||||||
# pylint: disable=E1101
|
# pylint: disable=E1101
|
||||||
|
|
||||||
# unittest.mock only in Python 3.3 (python2.7/pylint import issue)
|
# unittest.mock only in Python 3.3 (python2.7/pylint import issue)
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ Test suite specifically targeting JP2 box layout.
|
||||||
|
|
||||||
import doctest
|
import doctest
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import uuid
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
|
|
||||||
if sys.hexversion < 0x02070000:
|
if sys.hexversion < 0x02070000:
|
||||||
|
|
@ -502,6 +505,100 @@ class TestColourSpecificationBox(unittest.TestCase):
|
||||||
approximation=approx)
|
approximation=approx)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
|
||||||
|
"Missing openjp2 library.")
|
||||||
|
class TestAppend(unittest.TestCase):
|
||||||
|
"""Tests for append method."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.j2kfile = glymur.data.goodstuff()
|
||||||
|
self.jp2file = glymur.data.nemo()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_append_xml(self):
|
||||||
|
"""Should be able to append an XML box."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||||
|
shutil.copyfile(self.jp2file, tfile.name)
|
||||||
|
|
||||||
|
jp2 = Jp2k(tfile.name)
|
||||||
|
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||||
|
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
|
||||||
|
jp2.append(xmlbox)
|
||||||
|
|
||||||
|
# The sequence of box IDs should be the same as before, but with an
|
||||||
|
# xml box at the end.
|
||||||
|
box_ids = [box.box_id for box in jp2.box]
|
||||||
|
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
|
||||||
|
self.assertEqual(box_ids, expected)
|
||||||
|
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
||||||
|
b'<data>0</data>')
|
||||||
|
|
||||||
|
def test_only_jp2_allowed_to_append(self):
|
||||||
|
"""Only JP2 files are allowed to be appended."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
|
||||||
|
shutil.copyfile(self.j2kfile, tfile.name)
|
||||||
|
|
||||||
|
jp2 = Jp2k(tfile.name)
|
||||||
|
|
||||||
|
# Make a UUID box.
|
||||||
|
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
|
||||||
|
data = b'0123456789'
|
||||||
|
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
jp2.append(uuidbox)
|
||||||
|
|
||||||
|
def test_length_field_is_zero(self):
|
||||||
|
"""L=0 (length field in box header) is handled.
|
||||||
|
|
||||||
|
L=0 implies that the containing box is the last box. If this is not
|
||||||
|
handled properly, the appended box is never seen.
|
||||||
|
"""
|
||||||
|
baseline_jp2 = Jp2k(self.jp2file)
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
||||||
|
with open(self.jp2file, 'rb') as ifile:
|
||||||
|
# Everything up until the jp2c box.
|
||||||
|
offset = baseline_jp2.box[-1].offset
|
||||||
|
tfile.write(ifile.read(offset))
|
||||||
|
|
||||||
|
# Write the L, T fields of the jp2c box such that L == 0
|
||||||
|
write_buffer = struct.pack('>I4s', int(0), b'jp2c')
|
||||||
|
tfile.write(write_buffer)
|
||||||
|
|
||||||
|
# Write out the rest of the codestream.
|
||||||
|
ifile.seek(offset+8)
|
||||||
|
tfile.write(ifile.read())
|
||||||
|
tfile.flush()
|
||||||
|
|
||||||
|
jp2 = Jp2k(tfile.name)
|
||||||
|
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||||
|
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
|
||||||
|
jp2.append(xmlbox)
|
||||||
|
|
||||||
|
# The sequence of box IDs should be the same as before, but with an
|
||||||
|
# xml box at the end.
|
||||||
|
box_ids = [box.box_id for box in jp2.box]
|
||||||
|
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
|
||||||
|
self.assertEqual(box_ids, expected)
|
||||||
|
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
||||||
|
b'<data>0</data>')
|
||||||
|
|
||||||
|
def test_only_xml_allowed_to_append(self):
|
||||||
|
"""Only XML boxes are allowed to be appended."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||||
|
shutil.copyfile(self.jp2file, tfile.name)
|
||||||
|
|
||||||
|
jp2 = Jp2k(tfile.name)
|
||||||
|
|
||||||
|
# Make a UUID box.
|
||||||
|
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
|
||||||
|
data = b'0123456789'
|
||||||
|
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
jp2.append(uuidbox)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
|
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
|
||||||
"Missing openjp2 library.")
|
"Missing openjp2 library.")
|
||||||
class TestWrap(unittest.TestCase):
|
class TestWrap(unittest.TestCase):
|
||||||
|
|
@ -703,7 +800,7 @@ class TestJp2Boxes(unittest.TestCase):
|
||||||
def test_default_ihdr(self):
|
def test_default_ihdr(self):
|
||||||
"""Should be able to instantiate an image header box."""
|
"""Should be able to instantiate an image header box."""
|
||||||
ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256,
|
ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256,
|
||||||
num_components=3)
|
num_components=3)
|
||||||
self.assertEqual(ihdr.height, 512)
|
self.assertEqual(ihdr.height, 512)
|
||||||
self.assertEqual(ihdr.width, 256)
|
self.assertEqual(ihdr.width, 256)
|
||||||
self.assertEqual(ihdr.num_components, 3)
|
self.assertEqual(ihdr.num_components, 3)
|
||||||
|
|
@ -715,7 +812,7 @@ class TestJp2Boxes(unittest.TestCase):
|
||||||
"""Should be able to set jp2h boxes."""
|
"""Should be able to set jp2h boxes."""
|
||||||
box = JP2HeaderBox()
|
box = JP2HeaderBox()
|
||||||
box.box = [ImageHeaderBox(height=512, width=256),
|
box.box = [ImageHeaderBox(height=512, width=256),
|
||||||
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
|
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
|
||||||
self.assertTrue(True)
|
self.assertTrue(True)
|
||||||
|
|
||||||
def test_default_ccodestreambox(self):
|
def test_default_ccodestreambox(self):
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ suite.
|
||||||
# unittest fools pylint with "too many public methods"
|
# unittest fools pylint with "too many public methods"
|
||||||
# pylint: disable=R0904
|
# pylint: disable=R0904
|
||||||
|
|
||||||
# Some tests use numpy test infrastructure, which means the tests never
|
# Some tests use numpy test infrastructure, which means the tests never
|
||||||
# reference "self", so pylint claims it should be a function. No, no, no.
|
# reference "self", so pylint claims it should be a function. No, no, no.
|
||||||
# pylint: disable=R0201
|
# pylint: disable=R0201
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,8 @@ class TestSuiteWrite(unittest.TestCase):
|
||||||
self.assertEqual(len(codestream.segment[2].spcod), 9)
|
self.assertEqual(len(codestream.segment[2].spcod), 9)
|
||||||
|
|
||||||
# 18 SOP segments.
|
# 18 SOP segments.
|
||||||
nsops = [x.nsop for x in codestream.segment if x.marker_id == 'SOP']
|
nsops = [x.nsop for x in codestream.segment
|
||||||
|
if x.marker_id == 'SOP']
|
||||||
self.assertEqual(nsops, list(range(18)))
|
self.assertEqual(nsops, list(range(18)))
|
||||||
|
|
||||||
def test_NR_ENC_Bretagne2_ppm_7_encode(self):
|
def test_NR_ENC_Bretagne2_ppm_7_encode(self):
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
kwargs = {'name': 'Glymur',
|
kwargs = {'name': 'Glymur',
|
||||||
'version': '0.3.2',
|
'version': '0.4.0',
|
||||||
'description': 'Tools for accessing JPEG2000 files',
|
'description': 'Tools for accessing JPEG2000 files',
|
||||||
'long_description': open('README.md').read(),
|
'long_description': open('README.md').read(),
|
||||||
'author': 'John Evans',
|
'author': 'John Evans',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue