[Git][debian-gis-team/glymur][upstream] New upstream version 0.9.4

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sat Sep 4 18:04:56 BST 2021



Antonio Valentino pushed to branch upstream at Debian GIS Project / glymur


Commits:
ab1774db by Antonio Valentino at 2021-09-04T11:08:53+00:00
New upstream version 0.9.4
- - - - -


30 changed files:

- .travis.yml
- CHANGES.txt
- README.md
- appveyor.yml
- − ci/travis-36.yaml
- ci/travis-36-no-opj.yaml → ci/travis-37-no-opj.yaml
- ci/travis-39-opj2p4.yaml
- docs/source/conf.py
- docs/source/how_do_i.rst
- docs/source/introduction.rst
- docs/source/whatsnew/0.9.rst
- glymur/data/__init__.py
- glymur/jp2box.py
- glymur/jp2k.py
- glymur/lib/openjp2.py
- glymur/version.py
- setup.py
- tests/test_callbacks.py
- tests/test_codestream.py
- tests/test_colour_specification_box.py
- tests/test_jp2box.py
- tests/test_jp2box_jpx.py
- tests/test_jp2box_uuid.py
- tests/test_jp2box_xml.py
- tests/test_jp2k.py
- tests/test_openjp2.py
- tests/test_printing.py
- + tests/test_set_decoded_components.py
- tests/test_warnings.py
- + tests/test_writing_tiles.py


Changes:

=====================================
.travis.yml
=====================================
@@ -2,10 +2,10 @@ language: python
 matrix:
     fast_finish: true
     include:
-      - python: 3.9-dev
+      - python: 3.9
         env:
           - JOB="3.9" ENV_FILE="ci/travis-39.yaml"
-      - python: 3.9-dev
+      - python: 3.9
         env:
           - JOB="3.9 opj 2.4" ENV_FILE="ci/travis-39-opj2p4.yaml"
       - python: 3.8
@@ -17,12 +17,9 @@ matrix:
       - python: 3.7
         env:
           - JOB="3.7" ENV_FILE="ci/travis-37-no-gdal.yaml"
-      - python: 3.6
-        env:
-          - JOB="3.6 No OPENJPEG" ENV_FILE="ci/travis-36-no-opj.yaml"
-      - python: 3.6
+      - python: 3.7
         env:
-          - JOB="3.6" ENV_FILE="ci/travis-36.yaml"
+          - JOB="3.7 No OPENJPEG" ENV_FILE="ci/travis-37-no-opj.yaml"
 before_install:
   - echo "before_install"
   - sudo apt-get update


=====================================
CHANGES.txt
=====================================
@@ -1,3 +1,16 @@
+September 01, 2021 - v0.9.4
+    Add support for writing images tile-by-tile.
+    Add support for opj_set_decoded_components.
+    Remove support for Python 3.6.
+
+December 31, 2020 - v0.9.3
+    Qualify support on Python 3.9, OpenJPEG 2.4
+    Add support for multithreaded writes
+
+June 30, 2020 - v0.9.2
+    Update setup.py to include tests.
+    Update gdal imports to stop DeprecationWarning.
+
 June 30, 2020 - v0.9.2
     Update setup.py to include tests.
     Update gdal imports to stop DeprecationWarning.


=====================================
README.md
=====================================
@@ -3,6 +3,6 @@ glymur: a Python interface for JPEG 2000
 
 **glymur** contains a Python interface to the OpenJPEG library which
 allows one to read and write JPEG 2000 files.  **glymur** works on
-Python 3.6 and 3.7.
+Python 3.7, 3.8, and 3.9.
 
 Please read the docs, https://glymur.readthedocs.org/en/latest/


=====================================
appveyor.yml
=====================================
@@ -12,23 +12,23 @@ environment:
 
   matrix:
 
-    - CONDA_ROOT: "C:\\Miniconda3_64"
-      PYTHON_VERSION: "3.6"
-      PYTHON_ARCH: "64"
-      CONDA_PY: "36"
-      CONDA_NPY: "117"
-
     - CONDA_ROOT: "C:\\Miniconda3_64"
       PYTHON_VERSION: "3.7"
       PYTHON_ARCH: "64"
       CONDA_PY: "37"
-      CONDA_NPY: "117"
+      CONDA_NPY: "21"
 
     - CONDA_ROOT: "C:\\Miniconda3_64"
       PYTHON_VERSION: "3.8"
       PYTHON_ARCH: "64"
       CONDA_PY: "38"
-      CONDA_NPY: "117"
+      CONDA_NPY: "21"
+
+    - CONDA_ROOT: "C:\\Miniconda3_64"
+      PYTHON_VERSION: "3.9"
+      PYTHON_ARCH: "64"
+      CONDA_PY: "39"
+      CONDA_NPY: "21"
 
 # We always use a 64-bit machine, but can build x86 distributions
 # with the PYTHON_ARCH variable (which is used by CMD_IN_ENV).


=====================================
ci/travis-36.yaml deleted
=====================================
@@ -1,14 +0,0 @@
-name: glymur
-channels:
-    - defaults
-dependencies:
-    - python=3.6.*
-    - gdal
-    - lxml
-    - numpy
-    - openjpeg
-    - pip
-    - scikit-image
-    - pip:
-      - importlib_resources
-


=====================================
ci/travis-36-no-opj.yaml → ci/travis-37-no-opj.yaml
=====================================
@@ -2,13 +2,9 @@ name: glymur
 channels:
     - defaults
 dependencies:
-    - python=3.6.*
+    - python=3.7.*
     - gdal
     - lxml
     - numpy
     - pip
     - scikit-image
-    - pip:
-      - importlib_resources
-
-


=====================================
ci/travis-39-opj2p4.yaml
=====================================
@@ -6,3 +6,4 @@ dependencies:
     - lxml
     - numpy
     - openjpeg >= 2.4.0
+    - scikit-image


=====================================
docs/source/conf.py
=====================================
@@ -78,7 +78,7 @@ copyright = '2013-2020, John Evans'
 # The short X.Y version.
 version = '0.9'
 # The full version, including alpha/beta/rc tags.
-release = '0.9.3'
+release = '0.9.4'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.


=====================================
docs/source/how_do_i.rst
=====================================
@@ -41,6 +41,39 @@ you can make use of OpenJPEG's thread support to speed-up read operations.  ::
     >>> t1 - t0
     0.4060473537445068
 
+**************************************************
+... efficiently read just one band of a big image?
+**************************************************
+For really large images, before v0.9.4 you had to read in all bands of an
+image, even if you were only interested in just one of those bands.  With
+v0.9.4 or higher, you can make use of the :py:meth:`decoded_components`
+property, which will inform the openjpeg library to just decode the
+specified component(s), which can significantly speed up read operations
+on large images.  Be aware, however, that the openjpeg library will not
+employ the MCT when decoding these components.
+
+You can set the property to None to restore the behavior of decoding all
+bands.
+
+    >>> import glymur
+    >>> jp2file = glymur.data.nemo()
+    >>> jp2 = glymur.Jp2k(jp2file)
+    >>> data = jp2[:]
+    >>> data.shape
+    (1456, 2592, 3)
+    >>> jp2.decoded_components = 1
+    >>> data = jp2[:]
+    >>> data.shape
+    (1456, 2592)
+    >>> jp2.decoded_components = [0, 2]
+    >>> data = jp2[:]
+    >>> data.shape
+    (1456, 2592, 2)
+    >>> jp2.decoded_components = None
+    >>> data = jp2[:]
+    >>> data.shape
+    (1456, 2592, 3)
+
 *****************
 ... write images?
 *****************
@@ -60,7 +93,7 @@ or::
 **********************************************
 ... write images using multithreaded encoding?
 **********************************************
-If you have glymur 0.9.2 or higher
+If you have glymur 0.9.3 or higher
 and OpenJPEG 2.4.0 or higher,
 you can make use of OpenJPEG's thread support to speed-up read operations.
 With a puny 2015 macbook, just two cores, and a 5824x10368x3 image, we get::
@@ -81,6 +114,30 @@ With a puny 2015 macbook, just two cores, and a 5824x10368x3 image, we get::
     7.24 seconds
 
 
+*********************************************
+... write images that cannot fit into memory?
+*********************************************
+If you have glymur 0.9.4 or higher, you can write out an image tile-by-tile.
+In this example, we take a 512x512x3 image and tile it into a 20x20 grid, 
+resulting in a 10240x10240x3 image.
+
+    >>> import skimage.data
+    >>> from glymur import Jp2k
+    >>> img = skimage.data.astronaut()
+    >>> print(img.shape)
+    (512, 512, 3)
+    >>> shape = img.shape[0] * 20, img.shape[1] * 20, 3
+    >>> tilesize = (img.shape[0], img.shape[1])
+    >>> j = Jp2k('400astronauts.jp2', shape=shape, tilesize=tilesize, verbose=True)
+    >>> for tw in j.get_tilewriters():
+            tw[:] = img
+    >>> j = Jp2k('400astronauts.jp2')
+    >>> print(j.shape)
+    (10240, 10240, 3)
+
+Note that the tiles are written out left-to-right, tile-row-by-tile-row.  You must
+have image data ready to feed each tile writer, you cannot skip a tile.
+
 ************************************************************************
 ... write images with different compression ratios for different layers?
 ************************************************************************


=====================================
docs/source/introduction.rst
=====================================
@@ -15,7 +15,7 @@ Glymur will look to use **lxml** when processing boxes with XML content, but can
 fall back upon the standard library's **ElementTree** if **lxml** is not
 available.
 
-The current version of glymur works on Python versions 3.6, 3.7, 3.8, and 3.9.
+The current version of glymur works on Python versions 3.7, 3.8, and 3.9..
 
 For more information about OpenJPEG, please consult http://www.openjpeg.org.
 


=====================================
docs/source/whatsnew/0.9.rst
=====================================
@@ -2,6 +2,14 @@
 Changes in glymur 0.9
 #####################
 
+****************
+Changes in 0.9.4
+****************
+
+    * Remove support for Python 3.6.
+    * Add support for writing images tile-by-tile.
+    * Add support for opj_set_decoded_components.
+
 ****************
 Changes in 0.9.3
 ****************


=====================================
glymur/data/__init__.py
=====================================
@@ -6,7 +6,7 @@ These include:
     goodstuff.j2k:  my favorite bevorage.
 
 """
-import pkg_resources
+import importlib.resources as ir
 
 
 def nemo():
@@ -17,8 +17,8 @@ def nemo():
     file : str
         Platform-independent path to nemo.jp2.
     """
-    filename = pkg_resources.resource_filename(__name__, "nemo.jp2")
-    return filename
+    with ir.path('glymur.data', 'nemo.jp2') as filename:
+        return str(filename)
 
 
 def goodstuff():
@@ -29,8 +29,8 @@ def goodstuff():
     file : str
         Platform-independent path to goodstuff.j2k.
     """
-    filename = pkg_resources.resource_filename(__name__, "goodstuff.j2k")
-    return filename
+    with ir.path('glymur.data', 'goodstuff.j2k') as filename:
+        return str(filename)
 
 
 def jpxfile():
@@ -41,5 +41,5 @@ def jpxfile():
     file : str
         Platform-independent path to 12-v6.4.jpx
     """
-    filename = pkg_resources.resource_filename(__name__, "heliov.jpx")
-    return filename
+    with ir.path('glymur.data', 'heliov.jpx') as filename:
+        return str(filename)


=====================================
glymur/jp2box.py
=====================================
@@ -191,9 +191,12 @@ class Jp2kBox(object):
                 f'The original error message was "{str(err)}".'
             )
             warnings.warn(msg, UserWarning)
-            box = UnknownBox(box_id.decode('utf-8'),
-                             length=num_bytes,
-                             offset=start, longname='Unknown')
+            box = UnknownBox(
+                box_id.decode('utf-8'),
+                length=num_bytes,
+                offset=start,
+                longname='Unknown'
+            )
 
         return box
 
@@ -1003,8 +1006,9 @@ class ContiguousCodestreamBox(Jp2kBox):
             if self._filename is not None:
                 with open(self._filename, 'rb') as fptr:
                     fptr.seek(self.main_header_offset)
-                    codestream = Codestream(fptr, self._length,
-                                            header_only=header_only)
+                    codestream = Codestream(
+                        fptr, self._length, header_only=header_only
+                    )
                     self._codestream = codestream
         return self._codestream
 
@@ -1052,8 +1056,12 @@ class ContiguousCodestreamBox(Jp2kBox):
             codestream = Codestream(fptr, length, header_only=False)
         else:
             codestream = None
-        box = cls(codestream, main_header_offset=main_header_offset,
-                  length=length, offset=offset)
+        box = cls(
+            codestream,
+            main_header_offset=main_header_offset,
+            length=length,
+            offset=offset
+        )
         box._filename = fptr.name
         box._length = length
         return box
@@ -1333,9 +1341,13 @@ class FileTypeBox(Jp2kBox):
 
             compatibility_list.append(entry)
 
-        return cls(brand=brand, minor_version=minor_version,
-                   compatibility_list=compatibility_list,
-                   length=length, offset=offset)
+        return cls(
+            brand=brand,
+            minor_version=minor_version,
+            compatibility_list=compatibility_list,
+            length=length,
+            offset=offset
+        )
 
 
 class FragmentListBox(Jp2kBox):
@@ -1355,8 +1367,10 @@ class FragmentListBox(Jp2kBox):
     box_id = 'flst'
     longname = 'Fragment List'
 
-    def __init__(self, fragment_offset, fragment_length, data_reference,
-                 length=0, offset=-1):
+    def __init__(
+        self, fragment_offset, fragment_length, data_reference, length=0,
+        offset=-1
+    ):
         super().__init__()
         self.fragment_offset = fragment_offset
         self.fragment_length = fragment_length
@@ -1618,9 +1632,11 @@ class ImageHeaderBox(Jp2kBox):
     box_id = 'ihdr'
     longname = 'Image Header'
 
-    def __init__(self, height, width, num_components=1, signed=False,
-                 bits_per_component=8, compression=7, colorspace_unknown=False,
-                 ip_provided=False, length=0, offset=-1):
+    def __init__(
+        self, height, width, num_components=1, signed=False,
+        bits_per_component=8, compression=7, colorspace_unknown=False,
+        ip_provided=False, length=0, offset=-1
+    ):
         """
         Examples
         --------
@@ -2041,8 +2057,10 @@ class PaletteBox(Jp2kBox):
         """Verify that the box obeys the specifications."""
         if (((len(self.bits_per_component) != len(self.signed))
              or (len(self.signed) != self.palette.shape[1]))):
-            msg = ("The length of the 'bits_per_component' and the 'signed' "
-                   "members must equal the number of columns of the palette.")
+            msg = (
+                "The length of the 'bits_per_component' and the 'signed' "
+                "members must equal the number of columns of the palette."
+            )
             self._dispatch_validation_error(msg, writing=writing)
         bps = self.bits_per_component
         if writing and not all(b == bps[0] for b in bps):
@@ -2127,8 +2145,10 @@ class PaletteBox(Jp2kBox):
         # Are any components signed or differently sized?  We don't handle
         # that.
         if any(signed) or len(set(bps)) != 1:
-            msg = ("Palettes with signed components or differently sized "
-                   "components are not supported.")
+            msg = (
+                "Palettes with signed components or differently sized "
+                "components are not supported."
+            )
             raise InvalidJp2kError(msg)
 
         # The palette is unsigned and all components have the same width..
@@ -2861,8 +2881,10 @@ class XMLBox(Jp2kBox):
         """
         super().__init__()
         if filename is not None and xml is not None:
-            msg = ("Only one of either a filename or an ElementTree instance "
-                   "should be provided.")
+            msg = (
+                "Only one of either a filename or an ElementTree instance "
+                "should be provided."
+            )
             raise RuntimeError(msg)
         if filename is not None:
             self.xml = ET.parse(str(filename))
@@ -3436,32 +3458,46 @@ class UUIDBox(Jp2kBox):
 
         # report corners
         uleft = self.GDALInfoReportCorner(gtif, hTransform, "Upper Left", 0, 0)
-        lleft = self.GDALInfoReportCorner(gtif, hTransform, "Lower Left",
-                                          0, gtif.RasterYSize)
-        uright = self.GDALInfoReportCorner(gtif, hTransform, "Upper Right",
-                                           gtif.RasterXSize, 0)
-        lright = self.GDALInfoReportCorner(gtif, hTransform, "Lower Right",
-                                           gtif.RasterXSize, gtif.RasterYSize)
-        center = self.GDALInfoReportCorner(gtif, hTransform, "Center",
-                                           gtif.RasterXSize / 2.0,
-                                           gtif.RasterYSize / 2.0)
+        lleft = self.GDALInfoReportCorner(
+            gtif, hTransform, "Lower Left", 0, gtif.RasterYSize
+        )
+        uright = self.GDALInfoReportCorner(
+            gtif, hTransform, "Upper Right", gtif.RasterXSize, 0
+        )
+        lright = self.GDALInfoReportCorner(
+            gtif,
+            hTransform,
+            "Lower Right",
+            gtif.RasterXSize, gtif.RasterYSize
+        )
+        center = self.GDALInfoReportCorner(
+            gtif,
+            hTransform,
+            "Center",
+            gtif.RasterXSize / 2.0, gtif.RasterYSize / 2.0
+        )
 
         gdal.Unlink(in_mem_name)
 
-        fmt = ("Coordinate System =\n"
-               "{coordinate_system}\n"
-               "{geotransform}\n"
-               "Corner Coordinates:\n"
-               "{upper_left}\n"
-               "{lower_left}\n"
-               "{upper_right}\n"
-               "{lower_right}\n"
-               "{center}")
+        fmt = (
+            "Coordinate System =\n"
+            "{coordinate_system}\n"
+            "{geotransform}\n"
+            "Corner Coordinates:\n"
+            "{upper_left}\n"
+            "{lower_left}\n"
+            "{upper_right}\n"
+            "{lower_right}\n"
+            "{center}"
+        )
         coordinate_system = textwrap.indent(psz_pretty_wkt, ' ' * 4)
-        msg = fmt.format(coordinate_system=coordinate_system,
-                         geotransform=geotransform_str,
-                         upper_left=uleft, upper_right=uright,
-                         lower_left=lleft, lower_right=lright, center=center)
+        msg = fmt.format(
+            coordinate_system=coordinate_system,
+            geotransform=geotransform_str,
+            upper_left=uleft, upper_right=uright,
+            lower_left=lleft, lower_right=lright,
+            center=center
+        )
         return msg
 
     def GDALInfoReportCorner(self, hDataset, hTransform, corner_name, x, y):
@@ -3481,8 +3517,10 @@ class UUIDBox(Jp2kBox):
         if hTransform is not None:
             point = hTransform.TransformPoint(dfGeoX, dfGeoY, 0)
             if point is not None:
-                line += '({},{})'.format(gdal.DecToDMS(point[0], 'Long', 2),
-                                         gdal.DecToDMS(point[1], 'Lat', 2))
+                line += '({},{})'.format(
+                    gdal.DecToDMS(point[0], 'Long', 2),
+                    gdal.DecToDMS(point[1], 'Lat', 2)
+                )
 
         return line
 


=====================================
glymur/jp2k.py
=====================================
@@ -21,6 +21,7 @@ import warnings
 import numpy as np
 
 # Local imports...
+import glymur
 from .codestream import Codestream
 from . import core, version, get_option
 from .jp2box import (
@@ -49,6 +50,8 @@ class Jp2k(Jp2kBox):
     ----------
     codestream : glymur.codestream.Codestream
         JP2 or J2K codestream object.
+    decoded_components : sequence or None
+        If set, decode only these components.  The MCT will not be used.
     ignore_pclr_cmap_cdef : bool
         Whether or not to ignore the pclr, cmap, or cdef boxes during any
         color transformation, defaults to False.
@@ -91,7 +94,13 @@ class Jp2k(Jp2kBox):
     0.4060473537445068
     """
 
-    def __init__(self, filename, data=None, shape=None, **kwargs):
+    def __init__(
+        self, filename, data=None, shape=None, tilesize=None, verbose=False,
+        cbsize=None, cinema2k=None, cinema4k=None, colorspace=None,
+        cratios=None, eph=None, grid_offset=None, irreversible=None, mct=None,
+        modesw=None, numres=None, prog=None, psizes=None, psnr=None, sop=None,
+        subsam=None,
+    ):
         """
         Parameters
         ----------
@@ -120,8 +129,8 @@ class Jp2k(Jp2kBox):
         irreversible : bool, optional
             If true, use the irreversible DWT 9-7 transform.
         mct : bool, optional
-            Usage of the multi component transform.  If not specified, defaults
-            to True if the color space is RGB.
+            Usage of the multi component transform to write an image.  If not
+            specified, defaults to True if the color space is RGB.
         modesw : int, optional
             mode switch
                 1 = BYPASS(LAZY)
@@ -156,21 +165,51 @@ class Jp2k(Jp2kBox):
 
         self.box = []
         self._codec_format = None
-        self._colorspace = None
         self._layer = 0
         self._codestream = None
+        self._decoded_components = None
+
+        self._cbsize = cbsize
+        self._cinema2k = cinema2k
+        self._cinema4k = cinema4k
+        self._colorspace = colorspace
+        self._cratios = cratios
+        self._eph = eph
+        self._grid_offset = grid_offset
+        self._irreversible = irreversible
+        self._mct = mct
+        self._modesw = modesw
+        self._numres = numres
+        self._prog = prog
+        self._psizes = psizes
+        self._psnr = psnr
+        self._sop = sop
+        self._subsam = subsam
+        self._tilesize = tilesize
 
         self._ignore_pclr_cmap_cdef = False
-        self._verbose = False
+        self._verbose = verbose
+
+        if self.filename[-4:].endswith(('.jp2', '.JP2')):
+            self._codec_format = opj2.CODEC_JP2
+        else:
+            self._codec_format = opj2.CODEC_J2K
+
+        if self._codec_format == opj2.CODEC_J2K and colorspace is not None:
+            msg = 'Do not specify a colorspace when writing a raw codestream.'
+            raise InvalidJp2kError(msg)
 
         if data is not None:
-            # We are writing a JP2/J2K/JPX file.
+            # We are writing a JP2/J2K/JPX file where the image is
+            # contained in memory.
             self._shape = data.shape
-            self._write(data, **kwargs)
-        elif shape is not None:
-            # Only if J2X?
+            self._write(data)
+        elif data is None and shape is not None:
+            # We are writing an entire image via the slice protocol, or we are
+            # writing an image tile-by-tile.  A future course of action will
+            # determine that.
             self._shape = shape
-        if data is None and shape is None:
+        elif data is None and shape is None:
             # We must be just reading a JP2/J2K/JPX file.  Parse its
             # contents, then determine "shape".
             self.parse()
@@ -219,6 +258,32 @@ class Jp2k(Jp2kBox):
     def ignore_pclr_cmap_cdef(self, ignore_pclr_cmap_cdef):
         self._ignore_pclr_cmap_cdef = ignore_pclr_cmap_cdef
 
+    @property
+    def decoded_components(self):
+        return self._decoded_components
+
+    @decoded_components.setter
+    def decoded_components(self, components):
+
+        if components is None:
+            # this is a special case where we are restoring the original
+            # behavior of reading all bands
+            self._decoded_components = components
+            return
+
+        if np.isscalar(components):
+            components = [components]
+
+        if any(x > len(self.codestream.segment[1].xrsiz) for x in components):
+            msg = (
+                f"{components} has at least one invalid component, "
+                f"cannot be greater than "
+                f"{len(self.codestream.segment[1].xrsiz)}."
+            )
+            raise ValueError(msg)
+
+        self._decoded_components = components
+
     @property
     def layer(self):
         return self._layer
@@ -242,6 +307,10 @@ class Jp2k(Jp2kBox):
             self._codestream = self.get_codestream(header_only=True)
         return self._codestream
 
+    @property
+    def tilesize(self):
+        return self._tilesize
+
     @property
     def verbose(self):
         return self._verbose
@@ -280,6 +349,13 @@ class Jp2k(Jp2kBox):
             metadata.append(str(self.codestream))
         return '\n'.join(metadata)
 
+    def get_tilewriters(self):
+        """
+        Return an object that facilitates writing tile by tile.
+        """
+
+        return _TileWriter(self)
+
     def parse(self):
         """Parses the JPEG 2000 file.
 
@@ -421,11 +497,7 @@ class Jp2k(Jp2kBox):
             # cinema4k
             self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K
 
-    def _populate_cparams(self, img_array, mct=None, cratios=None, psnr=None,
-                          cinema2k=None, cinema4k=None, irreversible=None,
-                          cbsize=None, eph=None, grid_offset=None, modesw=None,
-                          numres=None, prog=None, psizes=None, sop=None,
-                          subsam=None, tilesize=None, colorspace=None):
+    def _populate_cparams(self, img_array):
         """Directs processing of write method arguments.
 
         Parameters
@@ -435,33 +507,40 @@ class Jp2k(Jp2kBox):
         kwargs : dictionary
             Non-image keyword inputs provided to write method.
         """
-        other_args = (mct, cratios, psnr, irreversible, cbsize, eph,
-                      grid_offset, modesw, numres, prog, psizes, sop, subsam)
+        other_args = (
+            self._mct, self._cratios, self._psnr, self._irreversible,
+            self._cbsize, self._eph, self._grid_offset, self._modesw,
+            self._numres, self._prog, self._psizes, self._sop, self._subsam
+        )
         if (
-            (cinema2k is not None or cinema4k is not None)
+            (
+                self._cinema2k is not None or self._cinema4k is not None
+            )
             and (not all([arg is None for arg in other_args]))
         ):
             msg = ("Cannot specify cinema2k/cinema4k along with any other "
                    "options.")
             raise InvalidJp2kError(msg)
 
-        if psnr is not None:
-            if cratios is not None:
+        if self._psnr is not None:
+            if self._cratios is not None:
                 msg = "Cannot specify cratios and psnr options together."
                 raise InvalidJp2kError(msg)
 
-            if 0 in psnr and psnr[-1] != 0:
+            if 0 in self._psnr and self._psnr[-1] != 0:
                 msg = ("If a zero value is supplied in the PSNR keyword "
                        "argument, it must be in the final position.")
                 raise InvalidJp2kError(msg)
 
             if (
-                (0 in psnr and np.any(np.diff(psnr[:-1]) < 0))
-                or (0 not in psnr and np.any(np.diff(psnr) < 0))
+                (0 in self._psnr and np.any(np.diff(self._psnr[:-1]) < 0))
+                or (0 not in self._psnr and np.any(np.diff(self._psnr) < 0))
             ):
-                msg = ("PSNR values must be increasing, with one exception - "
-                       "zero may be in the final position to indicate a "
-                       "lossless layer.")
+                msg = (
+                    "PSNR values must be increasing, with one exception - "
+                    "zero may be in the final position to indicate a lossless "
+                    "layer."
+                )
                 raise InvalidJp2kError(msg)
 
         cparams = opj2.set_default_encoder_parameters()
@@ -471,82 +550,81 @@ class Jp2k(Jp2kBox):
         outfile += b'0' * num_pad_bytes
         cparams.outfile = outfile
 
-        if self.filename[-4:].endswith(('.jp2', '.JP2')):
-            cparams.codec_fmt = opj2.CODEC_JP2
-        else:
-            cparams.codec_fmt = opj2.CODEC_J2K
+        cparams.codec_fmt = self._codec_format
 
-        cparams.irreversible = 1 if irreversible else 0
+        cparams.irreversible = 1 if self._irreversible else 0
 
-        if cinema2k is not None:
+        if self._cinema2k is not None:
             self._cparams = cparams
-            self._set_cinema_params('cinema2k', cinema2k)
+            self._set_cinema_params('cinema2k', self._cinema2k)
 
-        if cinema4k is not None:
+        if self._cinema4k is not None:
             self._cparams = cparams
-            self._set_cinema_params('cinema4k', cinema4k)
+            self._set_cinema_params('cinema4k', self._cinema4k)
 
-        if cbsize is not None:
-            cparams.cblockw_init = cbsize[1]
-            cparams.cblockh_init = cbsize[0]
+        if self._cbsize is not None:
+            cparams.cblockw_init = self._cbsize[1]
+            cparams.cblockh_init = self._cbsize[0]
 
-        if cratios is not None:
-            cparams.tcp_numlayers = len(cratios)
-            for j, cratio in enumerate(cratios):
+        if self._cratios is not None:
+            cparams.tcp_numlayers = len(self._cratios)
+            for j, cratio in enumerate(self._cratios):
                 cparams.tcp_rates[j] = cratio
             cparams.cp_disto_alloc = 1
 
-        cparams.csty |= 0x02 if sop else 0
-        cparams.csty |= 0x04 if eph else 0
+        cparams.csty |= 0x02 if self._sop else 0
+        cparams.csty |= 0x04 if self._eph else 0
 
-        if grid_offset is not None:
-            cparams.image_offset_x0 = grid_offset[1]
-            cparams.image_offset_y0 = grid_offset[0]
+        if self._grid_offset is not None:
+            cparams.image_offset_x0 = self._grid_offset[1]
+            cparams.image_offset_y0 = self._grid_offset[0]
 
-        if modesw is not None:
+        if self._modesw is not None:
             for shift in range(6):
                 power_of_two = 1 << shift
-                if modesw & power_of_two:
+                if self._modesw & power_of_two:
                     cparams.mode |= power_of_two
 
-        if numres is not None:
-            cparams.numresolution = numres
+        if self._numres is not None:
+            cparams.numresolution = self._numres
 
-        if prog is not None:
-            cparams.prog_order = core.PROGRESSION_ORDER[prog.upper()]
+        if self._prog is not None:
+            cparams.prog_order = core.PROGRESSION_ORDER[self._prog.upper()]
 
-        if psnr is not None:
-            cparams.tcp_numlayers = len(psnr)
-            for j, snr_layer in enumerate(psnr):
+        if self._psnr is not None:
+            cparams.tcp_numlayers = len(self._psnr)
+            for j, snr_layer in enumerate(self._psnr):
                 cparams.tcp_distoratio[j] = snr_layer
             cparams.cp_fixed_quality = 1
 
-        if psizes is not None:
-            for j, (prch, prcw) in enumerate(psizes):
+        if self._psizes is not None:
+            for j, (prch, prcw) in enumerate(self._psizes):
                 cparams.prcw_init[j] = prcw
                 cparams.prch_init[j] = prch
             cparams.csty |= 0x01
-            cparams.res_spec = len(psizes)
+            cparams.res_spec = len(self._psizes)
 
-        if subsam is not None:
-            cparams.subsampling_dy = subsam[0]
-            cparams.subsampling_dx = subsam[1]
+        if self._subsam is not None:
+            cparams.subsampling_dy = self._subsam[0]
+            cparams.subsampling_dx = self._subsam[1]
 
-        if tilesize is not None:
-            cparams.cp_tdx = tilesize[1]
-            cparams.cp_tdy = tilesize[0]
+        if self._tilesize is not None:
+            cparams.cp_tdx = self._tilesize[1]
+            cparams.cp_tdy = self._tilesize[0]
             cparams.tile_size_on = opj2.TRUE
 
-        if mct is None:
+        if self._mct is None:
             # If the multi component transform was not specified, we infer
             # that it should be used if the color space is RGB.
             cparams.tcp_mct = 1 if self._colorspace == opj2.CLRSPC_SRGB else 0
         else:
             if self._colorspace == opj2.CLRSPC_GRAY:
-                msg = ("Cannot specify usage of the multi component transform "
-                       "if the colorspace is gray.")
+                msg = (
+                    "Cannot specify usage of the multi component transform "
+                    "if the colorspace is gray."
+                )
                 raise InvalidJp2kError(msg)
-            cparams.tcp_mct = 1 if mct else 0
+            cparams.tcp_mct = 1 if self._mct else 0
 
         # Set defaults to lossless to begin.
         if cparams.tcp_numlayers == 0:
@@ -554,11 +632,11 @@ class Jp2k(Jp2kBox):
             cparams.tcp_numlayers += 1
             cparams.cp_disto_alloc = 1
 
-        self._validate_compression_params(img_array, cparams, colorspace)
+        self._validate_compression_params(img_array, cparams)
 
         self._cparams = cparams
 
-    def _write(self, img_array, verbose=False, **kwargs):
+    def _write(self, img_array):
         """Write image data to a JP2/JPX/J2k file.  Intended usage of the
         various parameters follows that of OpenJPEG's opj_compress utility.
 
@@ -570,18 +648,18 @@ class Jp2k(Jp2kBox):
                    "in order to write images.")
             raise RuntimeError(msg)
 
-        self._determine_colorspace(**kwargs)
-        self._populate_cparams(img_array, **kwargs)
+        self._determine_colorspace()
+        self._populate_cparams(img_array)
 
-        self._write_openjp2(img_array, verbose=verbose)
+        if img_array.ndim == 2:
+            # Force the image to be 3D.  This makes it easier to copy the
+            # image data later on.
+            numrows, numcols = img_array.shape
+            img_array = img_array.reshape(numrows, numcols, 1)
 
-    def _validate_j2k_colorspace(self, cparams, colorspace):
-        """
-        Cannot specify a colorspace with J2K.
-        """
-        if cparams.codec_fmt == opj2.CODEC_J2K and colorspace is not None:
-            msg = 'Do not specify a colorspace when writing a raw codestream.'
-            raise InvalidJp2kError(msg)
+        self._populate_comptparms(img_array)
+
+        self._write_openjp2(img_array)
 
     def _validate_codeblock_size(self, cparams):
         """
@@ -660,11 +738,13 @@ class Jp2k(Jp2kBox):
         Only uint8 and uint16 images are currently supported.
         """
         if img_array.dtype != np.uint8 and img_array.dtype != np.uint16:
-            msg = ("Only uint8 and uint16 datatypes are currently supported "
-                   "when writing.")
+            msg = (
+                "Only uint8 and uint16 datatypes are currently supported when "
+                "writing."
+            )
             raise InvalidJp2kError(msg)
 
-    def _validate_compression_params(self, img_array, cparams, colorspace):
+    def _validate_compression_params(self, img_array, cparams):
         """Check that the compression parameters are valid.
 
         Parameters
@@ -674,21 +754,15 @@ class Jp2k(Jp2kBox):
         cparams : CompressionParametersType(ctypes.Structure)
             Corresponds to cparameters_t type in openjp2 headers.
         """
-        self._validate_j2k_colorspace(cparams, colorspace)
         self._validate_codeblock_size(cparams)
         self._validate_precinct_size(cparams)
         self._validate_image_rank(img_array)
         self._validate_image_datatype(img_array)
 
-    def _determine_colorspace(self, colorspace=None, **kwargs):
+    def _determine_colorspace(self):
         """Determine the colorspace from the supplied inputs.
-
-        Parameters
-        ----------
-        colorspace : str, optional
-            Either 'rgb' or 'gray'.
         """
-        if colorspace is None:
+        if self._colorspace is None:
             # Must infer the colorspace from the image dimensions.
             if len(self.shape) < 3:
                 # A single channel image is grayscale.
@@ -701,33 +775,28 @@ class Jp2k(Jp2kBox):
                 # Anything else must be RGB, right?
                 self._colorspace = opj2.CLRSPC_SRGB
         else:
-            if colorspace.lower() not in ('rgb', 'grey', 'gray'):
-                msg = f'Invalid colorspace "{colorspace}".'
+            if self._colorspace.lower() not in ('rgb', 'grey', 'gray'):
+                msg = f'Invalid colorspace "{self._colorspace}".'
                 raise InvalidJp2kError(msg)
-            elif colorspace.lower() == 'rgb' and self.shape[2] < 3:
+            elif self._colorspace.lower() == 'rgb' and self.shape[2] < 3:
                 msg = 'RGB colorspace requires at least 3 components.'
                 raise InvalidJp2kError(msg)
 
             # Turn the colorspace from a string to the enumerated value that
             # the library expects.
-            COLORSPACE_MAP = {'rgb': opj2.CLRSPC_SRGB,
-                              'gray': opj2.CLRSPC_GRAY,
-                              'grey': opj2.CLRSPC_GRAY,
-                              'ycc': opj2.CLRSPC_YCC}
+            COLORSPACE_MAP = {
+                'rgb': opj2.CLRSPC_SRGB,
+                'gray': opj2.CLRSPC_GRAY,
+                'grey': opj2.CLRSPC_GRAY,
+                'ycc': opj2.CLRSPC_YCC
+            }
 
-            self._colorspace = COLORSPACE_MAP[colorspace.lower()]
+            self._colorspace = COLORSPACE_MAP[self._colorspace.lower()]
 
-    def _write_openjp2(self, img_array, verbose=False):
+    def _write_openjp2(self, img_array):
         """
         Write JPEG 2000 file using OpenJPEG 2.x interface.
         """
-        if img_array.ndim == 2:
-            # Force the image to be 3D.  Just makes things easier later on.
-            numrows, numcols = img_array.shape
-            img_array = img_array.reshape(numrows, numcols, 1)
-
-        self._populate_comptparms(img_array)
-
         with ExitStack() as stack:
             image = opj2.image_create(self._comptparms, self._colorspace)
             stack.callback(opj2.image_destroy, image)
@@ -737,7 +806,7 @@ class Jp2k(Jp2kBox):
             codec = opj2.create_compress(self._cparams.codec_fmt)
             stack.callback(opj2.destroy_codec, codec)
 
-            if self._verbose or verbose:
+            if self._verbose:
                 info_handler = _INFO_CALLBACK
             else:
                 info_handler = None
@@ -748,8 +817,8 @@ class Jp2k(Jp2kBox):
 
             opj2.setup_encoder(codec, self._cparams, image)
 
-            strm = opj2.stream_create_default_file_stream(self.filename,
-                                                          False)
+            strm = opj2.stream_create_default_file_stream(self.filename, False)
+
             num_threads = get_option('lib.num_threads')
             if version.openjpeg_version >= '2.4.0':
                 opj2.codec_set_threads(codec, num_threads)
@@ -789,8 +858,9 @@ class Jp2k(Jp2kBox):
             and box.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
         )
         if not (box_is_xml or box_is_xmp):
-            msg = ("Only XML boxes and XMP UUID boxes can currently be "
-                   "appended.")
+            msg = (
+                "Only XML boxes and XMP UUID boxes can currently be appended."
+            )
             raise RuntimeError(msg)
 
         # Check the last box.  If the length field is zero, then rewrite
@@ -1081,13 +1151,14 @@ class Jp2k(Jp2kBox):
         if np.abs(np.log2(step) - np.round(np.log2(step))) > 1e-6:
             msg = "Row and column strides must be powers of 2."
             raise ValueError(msg)
-        rlevel = np.int(np.round(np.log2(step)))
+        rlevel = int(np.round(np.log2(step)))
 
-        area = (0 if rows.start is None else rows.start,
-                0 if cols.start is None else cols.start,
-                numrows if rows.stop is None else rows.stop,
-                numcols if cols.stop is None else cols.stop
-                )
+        area = (
+            0 if rows.start is None else rows.start,
+            0 if cols.start is None else cols.start,
+            numrows if rows.stop is None else rows.stop,
+            numcols if cols.stop is None else cols.stop
+        )
         data = self._read(area=area, rlevel=rlevel)
         if len(pargs) == 2:
             return data
@@ -1155,8 +1226,19 @@ class Jp2k(Jp2kBox):
     def _subsampling_sanity_check(self):
         """Check for differing subsample factors.
         """
-        dxs = np.array(self.codestream.segment[1].xrsiz)
-        dys = np.array(self.codestream.segment[1].yrsiz)
+        if self._decoded_components is None:
+            dxs = np.array(self.codestream.segment[1].xrsiz)
+            dys = np.array(self.codestream.segment[1].yrsiz)
+        else:
+            dxs = np.array([
+                self.codestream.segment[1].xrsiz[i]
+                for i in self._decoded_components
+            ])
+            dys = np.array([
+                self.codestream.segment[1].yrsiz[i]
+                for i in self._decoded_components
+            ])
+
         if np.any(dxs - dxs[0]) or np.any(dys - dys[0]):
             msg = (
                 f"The read_bands method should be used when the subsampling "
@@ -1232,13 +1314,18 @@ class Jp2k(Jp2kBox):
             raw_image = opj2.read_header(stream, codec)
             stack.callback(opj2.image_destroy, raw_image)
 
+            if self._decoded_components is not None:
+                opj2.set_decoded_components(codec, self._decoded_components)
+
             if self._dparams.nb_tile_to_decode:
                 opj2.get_decoded_tile(codec, stream, raw_image,
                                       self._dparams.tile_index)
             else:
-                opj2.set_decode_area(codec, raw_image,
-                                     self._dparams.DA_x0, self._dparams.DA_y0,
-                                     self._dparams.DA_x1, self._dparams.DA_y1)
+                opj2.set_decode_area(
+                    codec, raw_image,
+                    self._dparams.DA_x0, self._dparams.DA_y0,
+                    self._dparams.DA_x1, self._dparams.DA_y1
+                )
                 opj2.decode(codec, stream, raw_image)
 
             opj2.end_decompress(codec, stream)
@@ -1350,9 +1437,11 @@ class Jp2k(Jp2kBox):
         >>> components_lst = jp.read_bands(rlevel=1)
         """
         if version.openjpeg_version < '2.3.0':
-            msg = ("You must have at least version 2.3.0 of OpenJPEG "
-                   "installed before using this method.  Your version of "
-                   "OpenJPEG is {version.openjpeg_version}.")
+            msg = (
+                f"You must have at least version 2.3.0 of OpenJPEG installed "
+                f"before using this method.  Your version of OpenJPEG is "
+                f"{version.openjpeg_version}."
+            )
             raise RuntimeError(msg)
 
         self.ignore_pclr_cmap_cdef = ignore_pclr_cmap_cdef
@@ -1387,8 +1476,10 @@ class Jp2k(Jp2kBox):
             dtypes.append(self._component2dtype(component))
             nrows.append(component.h)
             ncols.append(component.w)
-        is_cube = all(r == nrows[0] and c == ncols[0] and d == dtypes[0]
-                      for r, c, d in zip(nrows, ncols, dtypes))
+        is_cube = all(
+            r == nrows[0] and c == ncols[0] and d == dtypes[0]
+            for r, c, d in zip(nrows, ncols, dtypes)
+        )
 
         if is_cube:
             image = np.zeros((nrows[0], ncols[0], ncomps), dtypes[0])
@@ -1503,18 +1594,27 @@ class Jp2k(Jp2kBox):
 
             return codestream
 
-    def _populate_image_struct(self, image, imgdata):
+    def _populate_image_struct(
+        self, image, imgdata, tile_x_factor=1, tile_y_factor=1
+    ):
         """Populates image struct needed for compression.
 
         Parameters
         ----------
         image : ImageType(ctypes.Structure)
             Corresponds to image_t type in openjp2 headers.
-        img_array : ndarray
+        imgdata : ndarray
             Image data to be written to file.
+        tile_x_factor, tile_y_factor: int
+            Used only when writing tile-by-tile.  In this case, the image data
+            that we have is only the size of a single tile.
         """
 
-        numrows, numcols, num_comps = imgdata.shape
+        if len(self.shape) < 3:
+            (numrows, numcols), num_comps = self.shape, 1
+        else:
+            numrows, numcols, num_comps = self.shape
+
         for k in range(num_comps):
             self._validate_nonzero_image_size(numrows, numcols, k)
 
@@ -1523,15 +1623,19 @@ class Jp2k(Jp2kBox):
         image.contents.y0 = self._cparams.image_offset_y0
         image.contents.x1 = (
             image.contents.x0
-            + (numcols - 1) * self._cparams.subsampling_dx
+            + (numcols - 1) * self._cparams.subsampling_dx * tile_x_factor
             + 1
         )
         image.contents.y1 = (
             image.contents.y0
-            + (numrows - 1) * self._cparams.subsampling_dy
+            + (numrows - 1) * self._cparams.subsampling_dy * tile_y_factor
             + 1
         )
 
+        if tile_x_factor != 1 or tile_y_factor != 1:
+            # don't stage the data if writing tiles
+            return image
+
         # Stage the image data to the openjpeg data structure.
         for k in range(0, num_comps):
             if self._cparams.rsiz in (core.OPJ_PROFILE_CINEMA_2K,
@@ -1562,7 +1666,11 @@ class Jp2k(Jp2kBox):
         else:
             comp_prec = 16
 
-        numrows, numcols, num_comps = img_array.shape
+        if len(self.shape) < 3:
+            (numrows, numcols), num_comps = self.shape, 1
+        else:
+            numrows, numcols, num_comps = self.shape
+
         comptparms = (opj2.ImageComptParmType * num_comps)()
         for j in range(num_comps):
             comptparms[j].dx = self._cparams.subsampling_dx
@@ -1593,9 +1701,11 @@ class Jp2k(Jp2kBox):
 
         This is non-exhaustive.
         """
-        JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h',
-                   'jP  ', 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst',
-                   'uinf', 'url ', 'uuid']
+        JP2_IDS = [
+            'colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP  ',
+            'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
+            'uuid'
+        ]
 
         self._validate_signature_compatibility(boxes)
         self._validate_jp2h(boxes)
@@ -1623,8 +1733,10 @@ class Jp2k(Jp2kBox):
         jp2h = lst[0]
         for colr in [box for box in jp2h.box if box.box_id == 'colr']:
             if colr.approximation != 0:
-                msg = ("A JP2 colr box cannot have a non-zero approximation "
-                       "field.")
+                msg = (
+                    "A JP2 colr box cannot have a non-zero approximation "
+                    "field."
+                )
                 raise InvalidJp2kError(msg)
 
     def _validate_jpx_box_sequence(self, boxes):
@@ -1639,8 +1751,10 @@ class Jp2k(Jp2kBox):
         # Check for a bad sequence of boxes.
         # 1st two boxes must be 'jP  ' and 'ftyp'
         if boxes[0].box_id != 'jP  ' or boxes[1].box_id != 'ftyp':
-            msg = ("The first box must be the signature box and the second "
-                   "must be the file type box.")
+            msg = (
+                "The first box must be the signature box and the second must "
+                "be the file type box."
+            )
             raise InvalidJp2kError(msg)
 
         # The compatibility list must contain at a minimum 'jp2 '.
@@ -1657,8 +1771,10 @@ class Jp2k(Jp2kBox):
         jp2c_lst = [idx for (idx, box) in enumerate(boxes)
                     if box.box_id == 'jp2c']
         if len(jp2c_lst) == 0:
-            msg = ("A codestream box must be defined in the outermost "
-                   "list of boxes.")
+            msg = (
+                "A codestream box must be defined in the outermost list of "
+                "boxes."
+            )
             raise InvalidJp2kError(msg)
 
         jp2c_idx = jp2c_lst[0]
@@ -1680,13 +1796,16 @@ class Jp2k(Jp2kBox):
 
         # 1st jp2 header box must be ihdr
         if jp2h.box[0].box_id != 'ihdr':
-            msg = ("The first box in the jp2 header box must be the image "
-                   "header box.")
+            msg = (
+                "The first box in the jp2 header box must be the image header "
+                "box."
+            )
             raise InvalidJp2kError(msg)
 
         # colr must be present in jp2 header box.
-        colr_lst = [j for (j, box) in enumerate(jp2h.box)
-                    if box.box_id == 'colr']
+        colr_lst = [
+            j for (j, box) in enumerate(jp2h.box) if box.box_id == 'colr'
+        ]
         if len(colr_lst) == 0:
             msg = "The jp2 header box must contain a color definition box."
             raise InvalidJp2kError(msg)
@@ -1800,8 +1919,10 @@ class Jp2k(Jp2kBox):
         for box in boxes:
             if box.box_id in JPX_IDS:
                 if len(set(['jpx ', 'jpxb']).intersection(jpx_cl)) == 0:
-                    msg = ("A JPX box requires that either 'jpx ' or 'jpxb' "
-                           "be present in the ftype compatibility list.")
+                    msg = (
+                        "A JPX box requires that either 'jpx ' or 'jpxb' be "
+                        "present in the ftype compatibility list."
+                    )
                     raise InvalidJp2kError(msg)
             if hasattr(box, 'box') != 0:
                 # Same set of checks on any child boxes.
@@ -1817,13 +1938,162 @@ class Jp2k(Jp2kBox):
                 if hasattr(box, 'box'):
                     for boxi in box.box:
                         if boxi.box_id == 'lbl ':
-                            msg = (f"A label box cannot be nested inside a "
-                                   f"{box.box_id} box.")
+                            msg = (
+                                f"A label box cannot be nested inside a "
+                                f"{box.box_id} box."
+                            )
                             raise InvalidJp2kError(msg)
                     # Same set of checks on any child boxes.
                     self._validate_label(box.box)
 
 
+class _TileWriter(object):
+    """
+    Writes tiles to file, one by one.
+
+    Attributes
+    ----------
+    jp2k : glymur.Jp2k
+        Object wrapping the JPEG2000 file.
+    num_tile_rows, num_tile_cols : int
+        Dimensions of the image in terms of tiles.
+    number_of_tiles : int
+        This many tiles in the image.
+    tile_index : int
+        Each time the iteration protocol fires, this index will increase by
+        one, as the openjpeg library requires the tiles to be processed
+        sequentially.
+    """
+
+    def __init__(self, jp2k):
+        self.jp2k = jp2k
+
+        self.num_tile_rows = int(self.jp2k.shape[0] / self.jp2k.tilesize[0])
+        self.num_tile_cols = int(self.jp2k.shape[1] / self.jp2k.tilesize[1])
+        self.number_of_tiles = self.num_tile_rows * self.num_tile_cols
+
+    def __iter__(self):
+        self.tile_index = -1
+        return self
+
+    def __next__(self):
+        if self.tile_index < self.number_of_tiles - 1:
+            self.tile_index += 1
+            return self
+        else:
+            # We've gone thru all the tiles by this point.
+            raise StopIteration
+
+    def __setitem__(self, index, img_array):
+        """Write image data to a JP2/JPX/J2k file.  Intended usage of the
+        various parameters follows that of OpenJPEG's opj_compress utility.
+        """
+        if version.openjpeg_version < '2.3.0':
+            msg = ("You must have at least version 2.3.0 of OpenJPEG "
+                   "in order to write images.")
+            raise RuntimeError(msg)
+
+        if not isinstance(index, slice):
+            msg = (
+                "When writing tiles, the tile slice arguments must be just"
+                "a single slice(None, None, None), i.e. [:]."
+            )
+            raise RuntimeError(msg)
+
+        if self.tile_index == 0:
+            self.setup_first_tile(img_array)
+
+        try:
+            opj2.write_tile(
+                self.codec,
+                self.tile_index,
+                _set_planar_pixel_order(img_array),
+                self.stream
+            )
+        except glymur.lib.openjp2.OpenJPEGLibraryError as e:
+            # properly dispose of these resources
+            opj2.end_compress(self.codec, self.stream)
+            opj2.stream_destroy(self.stream)
+            opj2.image_destroy(self.image)
+            opj2.destroy_codec(self.codec)
+            raise(e)
+
+        if self.tile_index == self.number_of_tiles - 1:
+            # properly dispose of these resources
+            opj2.end_compress(self.codec, self.stream)
+            opj2.stream_destroy(self.stream)
+            opj2.image_destroy(self.image)
+            opj2.destroy_codec(self.codec)
+
+            # ... and reparse the newly created file to get all the metadata
+            self.jp2k.parse()
+
+    def setup_first_tile(self, img_array):
+        """
+        Only do these things for the first tile.
+        """
+        self.jp2k._determine_colorspace()
+        self.jp2k._populate_cparams(img_array)
+        self.jp2k._populate_comptparms(img_array)
+
+        self.codec = opj2.create_compress(self.jp2k._cparams.codec_fmt)
+
+        if self.jp2k.verbose:
+            info_handler = _INFO_CALLBACK
+        else:
+            info_handler = None
+
+        opj2.set_info_handler(self.codec, info_handler)
+        opj2.set_warning_handler(self.codec, _WARNING_CALLBACK)
+        opj2.set_error_handler(self.codec, _ERROR_CALLBACK)
+
+        self.image = opj2.image_tile_create(
+            self.jp2k._comptparms, self.jp2k._colorspace
+        )
+
+        self.jp2k._populate_image_struct(
+            self.image, img_array,
+            tile_x_factor=self.num_tile_cols,
+            tile_y_factor=self.num_tile_rows
+        )
+        self.image.contents.x1 = self.jp2k.shape[1]
+        self.image.contents.y1 = self.jp2k.shape[0]
+
+        opj2.setup_encoder(self.codec, self.jp2k._cparams, self.image)
+
+        self.stream = opj2.stream_create_default_file_stream(
+            self.jp2k.filename, False
+        )
+
+        num_threads = get_option('lib.num_threads')
+        if version.openjpeg_version >= '2.4.0':
+            opj2.codec_set_threads(self.codec, num_threads)
+        elif num_threads > 1:
+            msg = (
+                f'Threaded encoding is not supported in library versions '
+                f'prior to 2.4.0.  Your version is '
+                f'{version.openjpeg_version}.'
+            )
+            warnings.warn(msg, UserWarning)
+
+        opj2.start_compress(self.codec, self.image, self.stream)
+
+
+def _set_planar_pixel_order(img):
+    """
+    Reorder the image pixels so that plane-0 comes first, then plane-1, etc.
+    This is a requirement for using opj_write_tile.
+    """
+    if img.ndim == 3:
+        # C-order increments along the y-axis slowest (0), then x-axis (1),
+        # then z-axis (2).  We want it to go along the z-axis slowest, then
+        # y-axis, then x-axis.
+        img = np.swapaxes(img, 1, 2)
+        img = np.swapaxes(img, 0, 1)
+
+    return img.copy()
+
+
 # Setup the default callback handlers.  See the callback functions subsection
 # in the ctypes section of the Python documentation for a solid explanation of
 # what's going on here.


=====================================
glymur/lib/openjp2.py
=====================================
@@ -7,6 +7,9 @@ import ctypes
 import queue
 import textwrap
 
+# 3rd party library imports
+import numpy as np
+
 # Local imports
 from ..config import glymur_config
 
@@ -661,8 +664,11 @@ def decode(codec, stream, image):
     RuntimeError
         If the OpenJPEG library routine opj_decode fails.
     """
-    OPENJP2.opj_decode.argtypes = [CODEC_TYPE, STREAM_TYPE_P,
-                                   ctypes.POINTER(ImageType)]
+    OPENJP2.opj_decode.argtypes = [
+        CODEC_TYPE,
+        STREAM_TYPE_P,
+        ctypes.POINTER(ImageType)
+    ]
     OPENJP2.opj_decode.restype = check_error
 
     OPENJP2.opj_decode(codec, stream, image)
@@ -691,19 +697,23 @@ def decode_tile_data(codec, tidx, data, data_size, stream):
     RuntimeError
         If the OpenJPEG library routine opj_decode fails.
     """
-    OPENJP2.opj_decode_tile_data.argtypes = [CODEC_TYPE,
-                                             ctypes.c_uint32,
-                                             ctypes.POINTER(ctypes.c_uint8),
-                                             ctypes.c_uint32,
-                                             STREAM_TYPE_P]
+    OPENJP2.opj_decode_tile_data.argtypes = [
+        CODEC_TYPE,
+        ctypes.c_uint32,
+        ctypes.POINTER(ctypes.c_uint8),
+        ctypes.c_uint32,
+        STREAM_TYPE_P
+    ]
     OPENJP2.opj_decode_tile_data.restype = check_error
 
     datap = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))
-    OPENJP2.opj_decode_tile_data(codec,
-                                 ctypes.c_uint32(tidx),
-                                 datap,
-                                 ctypes.c_uint32(data_size),
-                                 stream)
+    OPENJP2.opj_decode_tile_data(
+        codec,
+        ctypes.c_uint32(tidx),
+        datap,
+        ctypes.c_uint32(data_size),
+        stream
+    )
 
 
 def create_decompress(codec_format):
@@ -786,10 +796,12 @@ def get_decoded_tile(codec, stream, imagep, tile_index):
     RuntimeError
         If the OpenJPEG library routine opj_get_decoded_tile fails.
     """
-    OPENJP2.opj_get_decoded_tile.argtypes = [CODEC_TYPE,
-                                             STREAM_TYPE_P,
-                                             ctypes.POINTER(ImageType),
-                                             ctypes.c_uint32]
+    OPENJP2.opj_get_decoded_tile.argtypes = [
+        CODEC_TYPE,
+        STREAM_TYPE_P,
+        ctypes.POINTER(ImageType),
+        ctypes.c_uint32
+    ]
     OPENJP2.opj_get_decoded_tile.restype = check_error
 
     OPENJP2.opj_get_decoded_tile(codec, stream, imagep, tile_index)
@@ -899,14 +911,18 @@ def image_create(comptparms, clrspc):
     image : ImageType
         Reference to ImageType instance.
     """
-    OPENJP2.opj_image_create.argtypes = [ctypes.c_uint32,
-                                         ctypes.POINTER(ImageComptParmType),
-                                         COLOR_SPACE_TYPE]
+    OPENJP2.opj_image_create.argtypes = [
+        ctypes.c_uint32,
+        ctypes.POINTER(ImageComptParmType),
+        COLOR_SPACE_TYPE
+    ]
     OPENJP2.opj_image_create.restype = ctypes.POINTER(ImageType)
 
-    image = OPENJP2.opj_image_create(len(comptparms),
-                                     comptparms,
-                                     clrspc)
+    image = OPENJP2.opj_image_create(
+        len(comptparms),
+        comptparms,
+        clrspc
+    )
     return image
 
 
@@ -927,15 +943,19 @@ def image_tile_create(comptparms, clrspc):
     image : ImageType
         Reference to ImageType instance.
     """
-    ARGTYPES = [ctypes.c_uint32,
-                ctypes.POINTER(ImageComptParmType),
-                COLOR_SPACE_TYPE]
+    ARGTYPES = [
+        ctypes.c_uint32,
+        ctypes.POINTER(ImageComptParmType),
+        COLOR_SPACE_TYPE
+    ]
     OPENJP2.opj_image_tile_create.argtypes = ARGTYPES
     OPENJP2.opj_image_tile_create.restype = ctypes.POINTER(ImageType)
 
-    image = OPENJP2.opj_image_tile_create(len(comptparms),
-                                          comptparms,
-                                          clrspc)
+    image = OPENJP2.opj_image_tile_create(
+        len(comptparms),
+        comptparms,
+        clrspc
+    )
     return image
 
 
@@ -961,8 +981,11 @@ def read_header(stream, codec):
     RuntimeError
         If the OpenJPEG library routine opj_read_header fails.
     """
-    ARGTYPES = [STREAM_TYPE_P, CODEC_TYPE,
-                ctypes.POINTER(ctypes.POINTER(ImageType))]
+    ARGTYPES = [
+        STREAM_TYPE_P,
+        CODEC_TYPE,
+        ctypes.POINTER(ctypes.POINTER(ImageType))
+    ]
     OPENJP2.opj_read_header.argtypes = ARGTYPES
     OPENJP2.opj_read_header.restype = check_error
 
@@ -1003,16 +1026,18 @@ def read_tile_header(codec, stream):
     RuntimeError
         If the OpenJPEG library routine opj_read_tile_header fails.
     """
-    ARGTYPES = [CODEC_TYPE,
-                STREAM_TYPE_P,
-                ctypes.POINTER(ctypes.c_uint32),
-                ctypes.POINTER(ctypes.c_uint32),
-                ctypes.POINTER(ctypes.c_int32),
-                ctypes.POINTER(ctypes.c_int32),
-                ctypes.POINTER(ctypes.c_int32),
-                ctypes.POINTER(ctypes.c_int32),
-                ctypes.POINTER(ctypes.c_uint32),
-                ctypes.POINTER(BOOL_TYPE)]
+    ARGTYPES = [
+        CODEC_TYPE,
+        STREAM_TYPE_P,
+        ctypes.POINTER(ctypes.c_uint32),
+        ctypes.POINTER(ctypes.c_uint32),
+        ctypes.POINTER(ctypes.c_int32),
+        ctypes.POINTER(ctypes.c_int32),
+        ctypes.POINTER(ctypes.c_int32),
+        ctypes.POINTER(ctypes.c_int32),
+        ctypes.POINTER(ctypes.c_uint32),
+        ctypes.POINTER(BOOL_TYPE)
+    ]
     OPENJP2.opj_read_tile_header.argtypes = ARGTYPES
     OPENJP2.opj_read_tile_header.restype = check_error
 
@@ -1024,25 +1049,29 @@ def read_tile_header(codec, stream):
     row1 = ctypes.c_int32()
     ncomps = ctypes.c_uint32()
     go_on = BOOL_TYPE()
-    OPENJP2.opj_read_tile_header(codec,
-                                 stream,
-                                 ctypes.byref(tile_index),
-                                 ctypes.byref(data_size),
-                                 ctypes.byref(col0),
-                                 ctypes.byref(row0),
-                                 ctypes.byref(col1),
-                                 ctypes.byref(row1),
-                                 ctypes.byref(ncomps),
-                                 ctypes.byref(go_on))
+    OPENJP2.opj_read_tile_header(
+        codec,
+        stream,
+        ctypes.byref(tile_index),
+        ctypes.byref(data_size),
+        ctypes.byref(col0),
+        ctypes.byref(row0),
+        ctypes.byref(col1),
+        ctypes.byref(row1),
+        ctypes.byref(ncomps),
+        ctypes.byref(go_on)
+    )
     go_on = bool(go_on.value)
-    return (tile_index.value,
-            data_size.value,
-            col0.value,
-            row0.value,
-            col1.value,
-            row1.value,
-            ncomps.value,
-            go_on)
+    return (
+        tile_index.value,
+        data_size.value,
+        col0.value,
+        row0.value,
+        col1.value,
+        row1.value,
+        ncomps.value,
+        go_on
+    )
 
 
 def set_decode_area(codec, image, start_x=0, start_y=0, end_x=0, end_y=0):
@@ -1067,19 +1096,62 @@ def set_decode_area(codec, image, start_x=0, start_y=0, end_x=0, end_y=0):
     RuntimeError
         If the OpenJPEG library routine opj_set_decode_area fails.
     """
-    OPENJP2.opj_set_decode_area.argtypes = [CODEC_TYPE,
-                                            ctypes.POINTER(ImageType),
-                                            ctypes.c_int32,
-                                            ctypes.c_int32,
-                                            ctypes.c_int32,
-                                            ctypes.c_int32]
+    OPENJP2.opj_set_decode_area.argtypes = [
+        CODEC_TYPE,
+        ctypes.POINTER(ImageType),
+        ctypes.c_int32,
+        ctypes.c_int32,
+        ctypes.c_int32,
+        ctypes.c_int32
+    ]
     OPENJP2.opj_set_decode_area.restype = check_error
 
-    OPENJP2.opj_set_decode_area(codec, image,
-                                ctypes.c_int32(start_x),
-                                ctypes.c_int32(start_y),
-                                ctypes.c_int32(end_x),
-                                ctypes.c_int32(end_y))
+    OPENJP2.opj_set_decode_area(
+        codec, image,
+        ctypes.c_int32(start_x),
+        ctypes.c_int32(start_y),
+        ctypes.c_int32(end_x),
+        ctypes.c_int32(end_y)
+    )
+
+
+def set_decoded_components(codec, comp_indices):
+    """Wraps openjp2 library function opj_set_decoded_components.
+
+    Restrict the number of components to decode.  This function should be
+    called right after read_header.
+
+    Parameters
+    ----------
+    codec : CODEC_TYPE
+        Codec initialized by create_decompress function.
+    comp_indices : list-like
+        The indices of the components to decode (relative to the codestream,
+        starting at 0).
+
+    Raises
+    ------
+    RuntimeError
+        If the OpenJPEG library routine opj_set_decoded_components fails..
+    """
+    comp_indices = np.uint32(comp_indices)
+    OPENJP2.opj_set_decoded_components.argtypes = [
+        CODEC_TYPE,
+        ctypes.c_uint32,
+        ctypes.POINTER(ctypes.c_uint32),
+        ctypes.c_int32
+    ]
+    OPENJP2.opj_set_decoded_components.restype = check_error
+
+    ncomps = len(comp_indices)
+    indices_p = comp_indices.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
+
+    # This is always False (0) for now.
+    apply_color_xforms = ctypes.c_int32(0)
+
+    OPENJP2.opj_set_decoded_components(
+        codec, ncomps, indices_p, apply_color_xforms
+    )
 
 
 def set_default_decoder_parameters():
@@ -1159,9 +1231,11 @@ def set_error_handler(codec, handler, data=None):
     RuntimeError
         If the OpenJPEG library routine opj_set_error_handler fails.
     """
-    OPENJP2.opj_set_error_handler.argtypes = [CODEC_TYPE,
-                                              ctypes.c_void_p,
-                                              ctypes.c_void_p]
+    OPENJP2.opj_set_error_handler.argtypes = [
+        CODEC_TYPE,
+        ctypes.c_void_p,
+        ctypes.c_void_p
+    ]
     OPENJP2.opj_set_error_handler.restype = check_error
     OPENJP2.opj_set_error_handler(codec, handler, data)
 
@@ -1185,9 +1259,9 @@ def set_info_handler(codec, handler, data=None):
     RuntimeError
         If the OpenJPEG library routine opj_set_info_handler fails.
     """
-    OPENJP2.opj_set_info_handler.argtypes = [CODEC_TYPE,
-                                             ctypes.c_void_p,
-                                             ctypes.c_void_p]
+    OPENJP2.opj_set_info_handler.argtypes = [
+        CODEC_TYPE, ctypes.c_void_p, ctypes.c_void_p
+    ]
     OPENJP2.opj_set_info_handler.restype = check_error
     OPENJP2.opj_set_info_handler(codec, handler, data)
 
@@ -1211,9 +1285,9 @@ def set_warning_handler(codec, handler, data=None):
     RuntimeError
         If the OpenJPEG library routine opj_set_warning_handler fails.
     """
-    OPENJP2.opj_set_warning_handler.argtypes = [CODEC_TYPE,
-                                                ctypes.c_void_p,
-                                                ctypes.c_void_p]
+    OPENJP2.opj_set_warning_handler.argtypes = [
+        CODEC_TYPE, ctypes.c_void_p, ctypes.c_void_p
+    ]
     OPENJP2.opj_set_warning_handler.restype = check_error
 
     OPENJP2.opj_set_warning_handler(codec, handler, data)
@@ -1263,9 +1337,11 @@ def setup_encoder(codec, cparams, image):
     RuntimeError
         If the OpenJPEG library routine opj_setup_encoder fails.
     """
-    ARGTYPES = [CODEC_TYPE,
-                ctypes.POINTER(CompressionParametersType),
-                ctypes.POINTER(ImageType)]
+    ARGTYPES = [
+        CODEC_TYPE,
+        ctypes.POINTER(CompressionParametersType),
+        ctypes.POINTER(ImageType)
+    ]
     OPENJP2.opj_setup_encoder.argtypes = ARGTYPES
     OPENJP2.opj_setup_encoder.restype = check_error
     OPENJP2.opj_setup_encoder(codec, ctypes.byref(cparams), image)
@@ -1290,9 +1366,9 @@ def start_compress(codec, image, stream):
     RuntimeError
         If the OpenJPEG library routine opj_start_compress fails.
     """
-    OPENJP2.opj_start_compress.argtypes = [CODEC_TYPE,
-                                           ctypes.POINTER(ImageType),
-                                           STREAM_TYPE_P]
+    OPENJP2.opj_start_compress.argtypes = [
+        CODEC_TYPE, ctypes.POINTER(ImageType), STREAM_TYPE_P
+    ]
     OPENJP2.opj_start_compress.restype = check_error
 
     OPENJP2.opj_start_compress(codec, image, stream)
@@ -1353,7 +1429,7 @@ def write_tile(codec, tile_index, data, *pargs):
     tile_index : int
         The index of the tile to write, zero-indexing assumed
     data : array
-        Image data arranged in usual C-order
+        Image data.  The memory layout is planar, not the usual C-order.
     data_size : int, optional
         Size of a tile in bytes.  If not provided, it will be inferred.
     stream : STREAM_TYPE_P
@@ -1372,19 +1448,23 @@ def write_tile(codec, tile_index, data, *pargs):
         data_size = data.nbytes
         stream = pargs[0]
 
-    OPENJP2.opj_write_tile.argtypes = [CODEC_TYPE,
-                                       ctypes.c_uint32,
-                                       ctypes.POINTER(ctypes.c_uint8),
-                                       ctypes.c_uint32,
-                                       STREAM_TYPE_P]
+    OPENJP2.opj_write_tile.argtypes = [
+        CODEC_TYPE,
+        ctypes.c_uint32,
+        ctypes.POINTER(ctypes.c_uint8),
+        ctypes.c_uint32,
+        STREAM_TYPE_P
+    ]
     OPENJP2.opj_write_tile.restype = check_error
 
     datap = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))
-    OPENJP2.opj_write_tile(codec,
-                           ctypes.c_uint32(int(tile_index)),
-                           datap,
-                           ctypes.c_uint32(int(data_size)),
-                           stream)
+    OPENJP2.opj_write_tile(
+        codec,
+        ctypes.c_uint32(int(tile_index)),
+        datap,
+        ctypes.c_uint32(int(data_size)),
+        stream
+    )
 
 
 def set_error_message(msg):


=====================================
glymur/version.py
=====================================
@@ -20,7 +20,7 @@ from .lib import openjp2 as opj2
 
 # Do not change the format of this next line!  Doing so risks breaking
 # setup.py
-version = "0.9.3"
+version = "0.9.4"
 _sv = LooseVersion(version)
 version_tuple = _sv.version
 


=====================================
setup.py
=====================================
@@ -24,7 +24,6 @@ kwargs = {
 
 kwargs['classifiers'] = [
     "Programming Language :: Python",
-    "Programming Language :: Python :: 3.6",
     "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",


=====================================
tests/test_callbacks.py
=====================================
@@ -7,6 +7,9 @@ import warnings
 import unittest
 from unittest.mock import patch
 
+# 3rd party library imports
+import skimage.data
+
 # Local imports ...
 import glymur
 from . import fixtures
@@ -66,3 +69,26 @@ class TestSuite(fixtures.TestCommon):
             actual = fake_out.getvalue().strip()
 
         self.assertIn('[INFO]', actual)
+
+    def test_info_callbacks_on_writing_tiles(self):
+        """
+        SCENARIO:  the verbose attribute is set to True
+
+        EXPECTED RESULT:  The info callback handler should be enabled.  There
+        should be [INFO] output present in sys.stdout.
+        """
+        jp2_data = skimage.data.moon()
+
+        shape = jp2_data.shape[0] * 3, jp2_data.shape[1] * 2
+        tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+        j = glymur.Jp2k(
+            self.temp_jp2_filename, shape=shape, tilesize=tilesize,
+            verbose=True
+        )
+        with patch('sys.stdout', new=StringIO()) as fake_out:
+            for tw in j.get_tilewriters():
+                tw[:] = jp2_data
+            actual = fake_out.getvalue().strip()
+
+        self.assertIn('[INFO] tile number', actual)


=====================================
tests/test_codestream.py
=====================================
@@ -4,11 +4,7 @@ Test suite for codestream oddities
 """
 
 # Standard library imports ...
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma: no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO
 import struct
 import unittest


=====================================
tests/test_colour_specification_box.py
=====================================
@@ -5,11 +5,7 @@ Test suite specifically targeting ICC profiles
 
 # Standard library imports ...
 from datetime import datetime
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 import struct
 import tempfile
 import unittest


=====================================
tests/test_jp2box.py
=====================================
@@ -2,11 +2,7 @@
 """
 # Standard library imports ...
 import doctest
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO
 import os
 import pathlib


=====================================
tests/test_jp2box_jpx.py
=====================================
@@ -4,11 +4,7 @@ Test suite specifically targeting JPX box layout.
 """
 # Standard library imports ...
 import ctypes
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO
 import shutil
 import struct


=====================================
tests/test_jp2box_uuid.py
=====================================
@@ -2,11 +2,7 @@
 """Test suite for printing.
 """
 # Standard library imports
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 import io
 import shutil
 import struct


=====================================
tests/test_jp2box_xml.py
=====================================
@@ -3,11 +3,7 @@
 Test suite specifically targeting the JP2 XML box layout.
 """
 # Standard library imports
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO
 import pathlib
 import struct


=====================================
tests/test_jp2k.py
=====================================
@@ -5,11 +5,7 @@ Tests for general glymur functionality.
 import collections
 import datetime
 import doctest
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO
 import os
 import pathlib
@@ -95,7 +91,7 @@ class TestSliceProtocolBaseWrite(SliceProtocolBase):
 
     def test_basic_write(self):
         """
-        SCENARIO:  write image by specifying image data in constructor
+        SCENARIO:  write image by specifying image data with slice protocol
 
         EXPECTED RESULT:  image is validated
         """


=====================================
tests/test_openjp2.py
=====================================
@@ -2,16 +2,19 @@
 Tests for libopenjp2 wrapping functions.
 """
 # Standard library imports ...
+from contextlib import ExitStack
 from io import StringIO
 import unittest
 from unittest.mock import patch
 
 # Third party library imports ...
 import numpy as np
+import skimage.data
 
 # Local imports ...
 import glymur
 from glymur.lib import openjp2
+from glymur.jp2k import _INFO_CALLBACK, _WARNING_CALLBACK, _ERROR_CALLBACK
 from . import fixtures
 from .fixtures import OPENJPEG_NOT_AVAILABLE, OPENJPEG_NOT_AVAILABLE_MSG
 
@@ -181,6 +184,207 @@ class TestOpenJP2(fixtures.TestCommon):
         xtx5_setup(filename, short_sig=True)
         self.assertTrue(True)
 
+    def test_tile_write_moon(self):
+        """
+        Test writing tiles for a 2D image.
+        """
+        img = skimage.data.moon()
+
+        num_comps = 1
+        image_height, image_width = img.shape
+
+        tile_height, tile_width = 256, 256
+
+        comp_prec = 8
+        irreversible = True
+
+        cblockh_init, cblockw_init = 64, 64
+
+        numresolution = 6
+
+        cparams = openjp2.set_default_encoder_parameters()
+
+        cparams.tile_size_on = openjp2.TRUE
+        cparams.cp_tdx = tile_width
+        cparams.cp_tdy = tile_height
+
+        cparams.cblockw_init, cparams.cblockh_init = cblockw_init, cblockh_init
+
+        cparams.irreversible = 1 if irreversible else 0
+
+        cparams.numresolution = numresolution
+        cparams.prog_order = glymur.core.PROGRESSION_ORDER['LRCP']
+
+        # greyscale so no mct
+        cparams.tcp_mct = 0
+
+        # comptparms == l_params
+        comptparms = (openjp2.ImageComptParmType * num_comps)()
+        for j in range(num_comps):
+            comptparms[j].dx = 1
+            comptparms[j].dy = 1
+            comptparms[j].w = tile_width
+            comptparms[j].h = tile_height
+            comptparms[j].x0 = 0
+            comptparms[j].y0 = 0
+            comptparms[j].prec = comp_prec
+            comptparms[j].bpp = comp_prec
+            comptparms[j].sgnd = 0
+
+        with ExitStack() as stack:
+
+            codec = openjp2.create_compress(openjp2.CODEC_J2K)
+            stack.callback(openjp2.destroy_codec, codec)
+
+            info_handler = _INFO_CALLBACK
+
+            openjp2.set_info_handler(codec, info_handler)
+            openjp2.set_warning_handler(codec, _WARNING_CALLBACK)
+            openjp2.set_error_handler(codec, _ERROR_CALLBACK)
+
+            # l_params == comptparms
+            # l_image == tile
+            image = openjp2.image_tile_create(comptparms, openjp2.CLRSPC_GRAY)
+            stack.callback(openjp2.image_destroy, image)
+
+            image.contents.x0, image.contents.y0 = 0, 0
+            image.contents.x1, image.contents.y1 = image_width, image_height
+            image.contents.color_space = openjp2.CLRSPC_GRAY
+
+            openjp2.setup_encoder(codec, cparams, image)
+
+            filename = str(self.temp_j2k_filename)
+            strm = openjp2.stream_create_default_file_stream(filename, False)
+            stack.callback(openjp2.stream_destroy, strm)
+
+            openjp2.start_compress(codec, image, strm)
+
+            openjp2.write_tile(codec, 0, img[0:256, 0:256].copy(), strm)
+            openjp2.write_tile(codec, 1, img[0:256, 256:512].copy(), strm)
+            openjp2.write_tile(codec, 2, img[256:512, 0:256].copy(), strm)
+            openjp2.write_tile(codec, 3, img[256:512, 256:512].copy(), strm)
+
+            openjp2.end_compress(codec, strm)
+
+    def test_write_tiles_3D(self):
+        """
+        Test writing tiles for an RGB image.
+        """
+
+        img = skimage.data.astronaut()
+
+        image_height, image_width, num_comps = img.shape
+
+        tile_height, tile_width = 256, 256
+
+        comp_prec = 8
+        irreversible = False
+
+        cblockh_init, cblockw_init = 64, 64
+
+        numresolution = 6
+
+        cparams = openjp2.set_default_encoder_parameters()
+
+        outfile = str(self.temp_j2k_filename).encode()
+        num_pad_bytes = openjp2.PATH_LEN - len(outfile)
+        outfile += b'0' * num_pad_bytes
+        cparams.outfile = outfile
+
+        # not from openjpeg test file
+        cparams.cp_disto_alloc = 1
+
+        cparams.tile_size_on = openjp2.TRUE
+        cparams.cp_tdx = tile_width
+        cparams.cp_tdy = tile_height
+
+        cparams.cblockw_init, cparams.cblockh_init = cblockw_init, cblockh_init
+
+        # not from openjpeg test file
+        cparams.mode = 0
+
+        cparams.irreversible = 1 if irreversible else 0
+
+        cparams.numresolution = numresolution
+        cparams.prog_order = glymur.core.PROGRESSION_ORDER['LRCP']
+
+        cparams.tcp_mct = 1
+
+        cparams.tcp_numlayers = 1
+        cparams.tcp_rates[0] = 0
+        cparams.tcp_distoratio[0] = 0
+
+        # comptparms == l_params
+        comptparms = (openjp2.ImageComptParmType * num_comps)()
+        for j in range(num_comps):
+            comptparms[j].dx = 1
+            comptparms[j].dy = 1
+            comptparms[j].w = image_width
+            comptparms[j].h = image_height
+            comptparms[j].x0 = 0
+            comptparms[j].y0 = 0
+            comptparms[j].prec = comp_prec
+            comptparms[j].bpp = comp_prec
+            comptparms[j].sgnd = 0
+
+        with ExitStack() as stack:
+            codec = openjp2.create_compress(openjp2.CODEC_J2K)
+            stack.callback(openjp2.destroy_codec, codec)
+
+            info_handler = _INFO_CALLBACK
+
+            openjp2.set_info_handler(codec, info_handler)
+            openjp2.set_warning_handler(codec, _WARNING_CALLBACK)
+            openjp2.set_error_handler(codec, _ERROR_CALLBACK)
+
+            image = openjp2.image_tile_create(comptparms, openjp2.CLRSPC_SRGB)
+            stack.callback(openjp2.image_destroy, image)
+
+            image.contents.x0, image.contents.y0 = 0, 0
+            image.contents.x1, image.contents.y1 = image_width, image_height
+            image.contents.color_space = openjp2.CLRSPC_SRGB
+
+            openjp2.setup_encoder(codec, cparams, image)
+
+            filename = str(self.temp_j2k_filename)
+            strm = openjp2.stream_create_default_file_stream(filename, False)
+            stack.callback(openjp2.stream_destroy, strm)
+
+            openjp2.start_compress(codec, image, strm)
+
+            # have to change the memory layout of 3D images in order to use
+            # opj_write_tile
+            openjp2.write_tile(
+                codec, 0, _set_planar_pixel_order(img[0:256, 0:256, :]), strm
+            )
+            openjp2.write_tile(
+                codec, 1, _set_planar_pixel_order(img[0:256, 256:512, :]), strm
+            )
+            openjp2.write_tile(
+                codec, 2, _set_planar_pixel_order(img[256:512, 0:256, :]), strm
+            )
+            openjp2.write_tile(
+                codec, 3, _set_planar_pixel_order(img[256:512, 256:512, :]),
+                strm
+            )
+
+            openjp2.end_compress(codec, strm)
+
+
+def _set_planar_pixel_order(img):
+    """
+    Reorder the image pixels so that plane-0 comes first, then plane-1, etc.
+    This is a requirement for using opj_write_tile.
+    """
+    if img.ndim == 3:
+        # C-order increments along the y-axis slowest (0), then x-axis (1),
+        # then z-axis (2).  We want it to go along the z-axis slowest, then
+        # y-axis, then x-axis.
+        img = np.swapaxes(img, 1, 2)
+        img = np.swapaxes(img, 0, 1)
+
+    return img.copy()
+
 
 def tile_encoder(**kwargs):
     """Fixture used by many tests."""


=====================================
tests/test_printing.py
=====================================
@@ -3,11 +3,7 @@
 Test suite for printing.
 """
 # Standard library imports ...
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO, StringIO
 import struct
 import sys


=====================================
tests/test_set_decoded_components.py
=====================================
@@ -0,0 +1,196 @@
+# standard library imports
+import importlib.resources as ir
+import pathlib
+import shutil
+import tempfile
+import unittest
+
+# 3rd party library imports
+import numpy as np
+
+# local imports
+import glymur
+from glymur import Jp2k
+
+
+class TestSuite(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(self):
+        """
+        We need a test image without the MCT and with at least 2 quality
+        layers.
+        """
+        self.testdir = tempfile.mkdtemp()
+
+        # windows won't like it if we try using tempfile.NamedTemporaryFile
+        # here
+        self.j2kfile = pathlib.Path(self.testdir) / 'tmp.j2k'
+
+        data = Jp2k(glymur.data.goodstuff())[:]
+        Jp2k(self.j2kfile.name, data=data, mct=False, cratios=[200, 100, 50])
+
+    @classmethod
+    def tearDownClass(self):
+        shutil.rmtree(self.testdir)
+
+    def test_one_component(self):
+        """
+        SCENARIO:  Decode the 1st component of an RGB image.  Then restore
+        the original configuration of reading all bands.
+
+        EXPECTED RESULT:  the data matches what we get from the regular way.
+        """
+        j2k = Jp2k(self.j2kfile.name)
+        expected = j2k[:, :, 0]
+
+        j2k.decoded_components = 0
+        actual = j2k[:]
+
+        np.testing.assert_array_equal(actual, expected)
+
+        # restore the original configuration
+        j2k.decoded_components = None
+        actual = j2k[:]
+        self.assertEqual(actual.shape, (800, 480, 3))
+
+    def test_second_component(self):
+        """
+        SCENARIO:  Decode the 2nd component of a non-MCT image.
+
+        EXPECTED RESULT:  Match the 2nd component read in the regular way.
+        """
+        j2k = Jp2k(self.j2kfile.name)
+        expected = j2k[:, :, 1]
+
+        j2k.decoded_components = 1
+        actual = j2k[:]
+
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_three_components_without_MCT(self):
+        """
+        SCENARIO:  Decode three components without using the MCT.
+
+        EXPECTED RESULT:  Match the results the regular way.
+        """
+
+        j2k = Jp2k(self.j2kfile.name)
+
+        expected = j2k[:]
+
+        j2k.decoded_components = [0, 1, 2]
+        actual = j2k[:]
+
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_partial_component_decoding_with_area(self):
+        """
+        SCENARIO:  Decode one component with a specific area.
+
+        EXPECTED RESULT:  Match the results the regular way.
+        """
+        j2k = Jp2k(self.j2kfile.name)
+
+        expected = j2k[20:40, 10:30, 0]
+
+        j2k.decoded_components = 0
+        actual = j2k[20:40, 10:30]
+
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_layer(self):
+        """
+        SCENARIO:  Decode one component with a particular layer
+
+        EXPECTED RESULT:  Match the results the regular way.
+        """
+
+        j2k = Jp2k(self.j2kfile.name)
+        j2k.layer = 1
+
+        expected = j2k[:, :, 0]
+
+        j2k.decoded_components = 0
+        actual = j2k[:]
+
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_reduced_resolution(self):
+        """
+        SCENARIO:  Decode one component with reduced resolution.
+
+        EXPECTED RESULT:  Match the results the regular way.
+        """
+
+        j2k = Jp2k(self.j2kfile.name)
+
+        expected = j2k[::2, ::2, 0]
+
+        j2k.decoded_components = [0]
+        actual = j2k[::2, ::2]
+
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_negative_component(self):
+        """
+        SCENARIO:  Provide a negative component.
+
+        EXPECTED RESULT:  exception
+        """
+
+        j2k = Jp2k(self.j2kfile.name)
+
+        with self.assertRaises(glymur.lib.openjp2.OpenJPEGLibraryError):
+            j2k.decoded_components = -1
+            j2k[:]
+
+    def test_same_component_several_times(self):
+        """
+        SCENARIO:  Decode one component multiple times.
+
+        EXPECTED RESULT:  exception
+        """
+
+        j2k = Jp2k(self.j2kfile.name)
+
+        with self.assertRaises(glymur.lib.openjp2.OpenJPEGLibraryError):
+            j2k.decoded_components = [0, 0]
+            j2k[:]
+
+    def test_invalid_component(self):
+        """
+        SCENARIO:  Decode an invalid component.
+
+        EXPECTED RESULT:  exception
+        """
+        j2k = Jp2k(self.j2kfile.name)
+
+        with self.assertRaises(ValueError):
+            j2k.decoded_components = 10
+
+    def test_differing_subsamples(self):
+        """
+        SCENARIO:  Decode a component where other components have different
+        subsamples.
+
+        EXPECTED RESULT:  success, trying to read that component without
+        setting decoded_components would require us to use the read_bands
+        method.
+        """
+        with ir.path('tests.data', 'p0_06.j2k') as path:
+            j2k = Jp2k(path)
+
+            expected = j2k.read_bands()[0]
+
+            j2k.decoded_components = 0
+            actual = j2k[:]
+
+            np.testing.assert_array_equal(actual, expected)
+
+        # verify that without using decoded components, we cannot read the
+        # image using the slice protocol
+        with ir.path('tests.data', 'p0_06.j2k') as path:
+            j2k = Jp2k(path)
+            with self.assertRaises(RuntimeError):
+                j2k[:, :, 0]


=====================================
tests/test_warnings.py
=====================================
@@ -3,11 +3,7 @@ Test suite for warnings issued by glymur.
 """
 # Standard library imports
 import codecs
-try:
-    import importlib.resources as ir
-except ImportError:  # pragma:  no cover
-    # before 3.7
-    import importlib_resources as ir
+import importlib.resources as ir
 from io import BytesIO
 import struct
 import unittest


=====================================
tests/test_writing_tiles.py
=====================================
@@ -0,0 +1,154 @@
+# 3rd party library imports
+import skimage.io
+import numpy as np
+
+# local imports
+import glymur
+from glymur import Jp2k
+from . import fixtures
+
+
+class TestSuite(fixtures.TestCommon):
+    """
+    Test suite for writing with tiles.
+    """
+    def test_astronaut(self):
+        """
+        SCENARIO:  construct a j2k file by tiling an image in a 2x2 grid..
+
+        EXPECTED RESULT:  the written image validates
+        """
+        j2k_data = skimage.data.astronaut()
+        data = [
+            j2k_data[:256, :256, :],
+            j2k_data[:256, 256:512, :],
+            j2k_data[256:512, :256, :],
+            j2k_data[256:512, 256:512, :],
+        ]
+
+        shape = j2k_data.shape
+        tilesize = 256, 256
+
+        j = Jp2k(self.temp_j2k_filename, shape=shape, tilesize=tilesize)
+        for idx, tw in enumerate(j.get_tilewriters()):
+            tw[:] = data[idx]
+
+        new_j = Jp2k(self.temp_j2k_filename)
+        actual = new_j[:]
+        expected = j2k_data
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_smoke(self):
+        """
+        SCENARIO:  construct a j2k file by repeating a 3D image in a 2x2 grid.
+
+        EXPECTED RESULT:  the written image matches the 2x2 grid
+        """
+        j2k_data = skimage.data.astronaut()
+
+        shape = (
+            j2k_data.shape[0] * 2, j2k_data.shape[1] * 2, j2k_data.shape[2]
+        )
+        tilesize = (j2k_data.shape[0], j2k_data.shape[1])
+
+        j = Jp2k(self.temp_j2k_filename, shape=shape, tilesize=tilesize)
+        for tw in j.get_tilewriters():
+            tw[:] = j2k_data
+
+        new_j = Jp2k(self.temp_j2k_filename)
+        actual = new_j[:]
+        expected = np.tile(j2k_data, (2, 2, 1))
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_moon(self):
+        """
+        SCENARIO:  construct a jp2 file by repeating a 2D image in a 3x2 grid.
+
+        EXPECTED RESULT:  the written image matches the 3x2 grid
+        """
+        jp2_data = skimage.data.moon()
+
+        shape = jp2_data.shape[0] * 3, jp2_data.shape[1] * 2
+        tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+        j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+        for tw in j.get_tilewriters():
+            tw[:] = jp2_data
+
+        new_j = Jp2k(self.temp_jp2_filename)
+        actual = new_j[:]
+        expected = np.tile(jp2_data, (3, 2))
+        np.testing.assert_array_equal(actual, expected)
+
+    def test_tile_slice_has_non_none_elements(self):
+        """
+        SCENARIO:  construct a jp2 file by repeating a 2D image in a 2x2 grid,
+        but the tile writer does not receive a degenerate slice object.
+
+        EXPECTED RESULT:  RuntimeError
+        """
+        jp2_data = skimage.data.moon()
+
+        shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+        tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+        j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+        with self.assertRaises(RuntimeError):
+            for tw in j.get_tilewriters():
+                tw[:256, :256] = jp2_data
+
+    def test_tile_slice_is_ellipsis(self):
+        """
+        SCENARIO:  construct a jp2 file by repeating a 2D image in a 2x2 grid,
+        but the tile writer does not receive a degenerate slice object.
+
+        EXPECTED RESULT:  RuntimeError
+        """
+        jp2_data = skimage.data.moon()
+
+        shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+        tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+        j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+        with self.assertRaises(RuntimeError):
+            for tw in j.get_tilewriters():
+                tw[...] = jp2_data
+
+    def test_too_much_data_for_slice(self):
+        """
+        SCENARIO:  construct a jp2 file by repeating a 2D image in a 2x2 grid,
+        but the tile writer does not receive a degenerate slice object.
+
+        EXPECTED RESULT:  RuntimeError
+        """
+        jp2_data = skimage.data.moon()
+
+        shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+        tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+        j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+        with self.assertRaises(glymur.lib.openjp2.OpenJPEGLibraryError):
+            for tw in j.get_tilewriters():
+                tw[:] = np.tile(jp2_data, (2, 2))
+
+    def test_write_with_different_compression_ratios(self):
+        """
+        SCENARIO:  construct a jp2 file by repeating a 2D image in a 2x2 grid.
+
+        EXPECTED RESULT:  There are three layers.
+        """
+        jp2_data = skimage.data.moon()
+
+        shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+        tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+        j = Jp2k(
+            self.temp_jp2_filename, shape=shape, tilesize=tilesize,
+            cratios=[20, 5, 1]
+        )
+
+        for tw in j.get_tilewriters():
+            tw[:] = jp2_data
+
+        codestream = j.get_codestream()
+        self.assertEqual(codestream.segment[2].layers, 3)  # layers = 3



View it on GitLab: https://salsa.debian.org/debian-gis-team/glymur/-/commit/ab1774db59e4ae7ed02768e77a09d8fd9d259e0e

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/glymur/-/commit/ab1774db59e4ae7ed02768e77a09d8fd9d259e0e
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210904/dd27ebd6/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list