[rasterio] 02/07: Imported Upstream version 0.25.0

Johan Van de Wauw johanvdw-guest at moszumanska.debian.org
Sun Aug 16 19:47:06 UTC 2015


This is an automated email from the git hooks/post-receive script.

johanvdw-guest pushed a commit to branch master
in repository rasterio.

commit 3424e4f333c4a2b315ad9066e6d4ba606350d7eb
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date:   Wed Jul 22 21:25:25 2015 +0200

    Imported Upstream version 0.25.0
---
 AUTHORS.txt                |  13 +-
 CHANGES.txt                |  29 ++-
 README.rst                 | 121 +++++++------
 docs/cli.rst               | 131 +++++++++++++-
 rasterio/__init__.py       |   8 +-
 rasterio/_base.pxd         |   1 +
 rasterio/_base.pyx         |  40 +++-
 rasterio/_features.pxd     |  20 +-
 rasterio/_gdal.pxd         |   9 +
 rasterio/_io.pyx           |  70 +++++--
 rasterio/_warp.pyx         |  13 +-
 rasterio/crs.py            |  40 +++-
 rasterio/enums.py          |  15 +-
 rasterio/errors.py         |   9 +
 rasterio/rio/__init__.py   |   4 +-
 rasterio/rio/bands.py      |  16 +-
 rasterio/rio/calc.py       |  28 +--
 rasterio/rio/cli.py        | 255 --------------------------
 rasterio/rio/convert.py    | 105 +++++++++++
 rasterio/rio/features.py   | 175 +++++++++++++++---
 rasterio/rio/helpers.py    |  79 ++++++++
 rasterio/rio/info.py       | 305 ++++++++++++++++++++++++-------
 rasterio/rio/main.py       |  62 ++++---
 rasterio/rio/merge.py      |  27 ++-
 rasterio/rio/options.py    | 156 ++++++++++++++++
 rasterio/rio/overview.py   | 101 +++++++++++
 rasterio/rio/rio.py        | 203 ---------------------
 rasterio/rio/sample.py     |   4 +-
 rasterio/rio/warp.py       | 208 +++++++++++++++++++++
 rasterio/sample.py         |  10 +
 rasterio/tool.py           |  17 +-
 rasterio/warp.py           |  24 ++-
 requirements-dev.txt       |   2 +-
 setup.py                   |  24 ++-
 tests/data/RGB.byte.tif    | Bin 1713704 -> 1743350 bytes
 tests/test_cli_main.py     |  21 +++
 tests/test_colormap.py     |  26 ++-
 tests/test_crs.py          |  39 +++-
 tests/test_indexing.py     |  11 +-
 tests/test_meta.py         |  27 ++-
 tests/test_options.py      |  20 ++
 tests/test_overviews.py    |  40 ++++
 tests/test_profile.py      |  10 +
 tests/test_rio_cli.py      |  18 --
 tests/test_rio_convert.py  | 120 ++++++++++++
 tests/test_rio_helpers.py  |  23 +++
 tests/test_rio_info.py     | 443 ++++++++++++++++++++++++++++++++++++++++++++-
 tests/test_rio_merge.py    |  20 ++
 tests/test_rio_overview.py |  67 +++++++
 tests/test_rio_rio.py      | 182 -------------------
 tests/test_rio_warp.py     | 238 ++++++++++++++++++++++++
 tests/test_sampling.py     |   9 +
 tests/test_warp.py         |  24 ++-
 tests/test_write.py        |  20 ++
 54 files changed, 2694 insertions(+), 988 deletions(-)

diff --git a/AUTHORS.txt b/AUTHORS.txt
index cfbfc24..481e0ea 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -3,19 +3,22 @@ Authors
 
 Sean Gillies <sean at mapbox.com>
 Brendan Ward <bcward at consbio.org>
+Amit Kapadia <amit at planet.com>
+Kelsey Jordahl <kjordahl at alum.mit.edu>
+Kevin Wurster <wursterk at gmail.com>
+Maxim Dubinin <sim at gis-lab.info>
 Ryan Grout <rgrout at continuum.io>
 Mike Toews <mwtoews at gmail.com>
 AsgerPetersen <asgerpetersen at gmail.com>
-Alessandro Amici <alexamici at gmail.com>
 Joshua Arnott <josh at snorfalorpagus.net>
-Amit Kapadia <amit at planet.com>
+Alessandro Amici <alexamici at gmail.com>
 Johan Van de Wauw <johan.vandewauw at gmail.com>
-Robin Wilson <robin at rtwilson.com>
 James Seppi <james.seppi at gmail.com>
+Jacques Tardie <hi at jacquestardie.org>
 Etienne B. Racine <etiennebr at gmail.com>
 cgohlke <cgohlke at uci.edu>
-Kevin Wurster <wursterk at gmail.com>
-Aldo Culquicondor <alculquicondor at gmail.com>
 Martijn Visser <mgvisser at gmail.com>
+Aldo Culquicondor <alculquicondor at gmail.com>
+Robin Wilson <robin at rtwilson.com>
 
 See also https://github.com/mapbox/rasterio/graphs/contributors.
diff --git a/CHANGES.txt b/CHANGES.txt
index 3ecfe74..b5c549d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,8 +1,35 @@
 Changes
 =======
 
-0.24.0 (2015-05-27)
+0.25.0 (2015-07-17)
+-------------------
+- New rio-warp command (#264, #404).
+- Add driver-specific creation options (`--co`) to many commands (#379, #403).
+- Add support for arbitrary CRS output to rio-bounds (#385, #392).
+- Add support for getting values from template files in rio-edit-info with a
+  `--like` option (#387, #399).
+- New rio-overview command (#388, #408).
+- Fix rounding error in extracting shapes from decimated data (#391).
+- Remove creation options from meta property and move them to new profile
+  property (#405, #406).
+- Fix for bug in passing affine keyword argument to open() in 'w' mode (#411).
+- New rio-convert command (#414, #417), a replacement for gdal_translate 
+  with more features to come by 1.0.
+- Improved error messages when seeking a driver when none are registered 
+  (#415).
+- Replace read_band() with read() in the rio-insp banner (#418).
+- Fix an indexing error that prevented window() and window_bounds() from 
+  round-tripping properly (#419).
+
+0.24.1 (2015-06-30)
 -------------------
+- Improve safety of the sample() generator (#378).
+- Provide array masking features missing from Numpy<1.9 (#380, #389).
+- Guard against attempts to write RGBA colormap entries to TIFFs, which the
+  format can not support (#394, #395).
+
+0.24.0 (2015-05-27)
+-------------------#408).
 - New rio-edit-info command (#358).
 - Add option to package GDAL data in distributions (#362).
 - Remove check that the path given to `rasterio.open()` in read mode is an
diff --git a/README.rst b/README.rst
index 5951445..a113f1e 100644
--- a/README.rst
+++ b/README.rst
@@ -21,77 +21,63 @@ Example
 
 Here's a simple example of the basic features rasterio provides. Three bands
 are read from an image and summed to produce something like a panchromatic
-band.  This new band is then written to a new single band TIFF. 
+band.  This new band is then written to a new single band TIFF.
 
 .. code-block:: python
 
     import numpy
     import rasterio
     import subprocess
-    
+
     # Register GDAL format drivers and configuration options with a
     # context manager.
-    
-    with rasterio.drivers(CPL_DEBUG=True):
-        
+    with rasterio.drivers():
+
         # Read raster bands directly to Numpy arrays.
         #
         with rasterio.open('tests/data/RGB.byte.tif') as src:
-            b, g, r = src.read()
-        
-        # Combine arrays in place. Expecting that the sum will 
-        # temporarily exceed the 8-bit integer range, initialize it as
-        # 16-bit. Adding other arrays to it in-place converts those
-        # arrays "up" and preserves the type of the total array.
+            r, g, b = src.read()
 
-        total = numpy.zeros(r.shape, dtype=rasterio.uint16)
+        # Combine arrays in place. Expecting that the sum will
+        # temporarily exceed the 8-bit integer range, initialize it as
+        # a 64-bit float (the numpy default) array. Adding other
+        # arrays to it in-place converts those arrays "up" and
+        # preserves the type of the total array.
+        total = numpy.zeros(r.shape)
         for band in r, g, b:
             total += band
         total /= 3
 
         # Write the product as a raster band to a new 8-bit file. For
-        # keyword arguments, we start with the meta attributes of the
-        # source file, but then change the band count to 1, set the
+        # the new file's profile, we start with the meta attributes of
+        # the source file, but then change the band count to 1, set the
         # dtype to uint8, and specify LZW compression.
-
-        kwargs = src.meta
-        kwargs.update(
+        profile = src.profile
+        profile.update(
             dtype=rasterio.uint8,
             count=1,
             compress='lzw')
-        
-        with rasterio.open('example-total.tif', 'w', **kwargs) as dst:
-            dst.write_band(1, total.astype(rasterio.uint8))
+
+        with rasterio.open('example-total.tif', 'w', **profile) as dst:
+            dst.write(total.astype(rasterio.uint8), 1)
 
     # At the end of the ``with rasterio.drivers()`` block, context
     # manager exits and all drivers are de-registered.
 
-    # Dump out gdalinfo's report card and open the image.
-    
-    info = subprocess.check_output(
-        ['gdalinfo', '-stats', 'example-total.tif'])
-    print(info)
-    subprocess.call(['open', 'example-total.tif'])
+The output:
 
 .. image:: http://farm6.staticflickr.com/5501/11393054644_74f54484d9_z_d.jpg
    :width: 640
    :height: 581
 
-The rasterio.drivers() function and context manager are new in 0.5. The example
-above shows the way to use it to register and de-register drivers in
-a deterministic and efficient way. Code written for rasterio 0.4 will continue
-to work: opened raster datasets may manage the global driver registry if no
-other manager is present.
-
 API Overview
 ============
 
 Simple access is provided to properties of a geospatial raster file.
 
 .. code-block:: python
-    
-    with rasterio.drivers():
 
+    with rasterio.drivers():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             print(src.width, src.height)
             print(src.crs)
@@ -107,18 +93,17 @@ Simple access is provided to properties of a geospatial raster file.
     # 3
     # [1, 2, 3]
 
-Rasterio also affords conversion of GeoTIFFs to other formats.
+A dataset also provides methods for getting extended array slices given
+georeferenced coordinates and vice versa.
+
 
 .. code-block:: python
     
     with rasterio.drivers():
-
-        rasterio.copy(
-            'example-total.tif',
-            'example-total.jpg', 
-            driver='JPEG')
-    
-    subprocess.call(['open', 'example-total.jpg'])
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            print src.window(**src.window_bounds(((100, 200), (100, 200))))
+    # Output:
+    # ((100, 200), (100, 200))
 
 Rasterio CLI
 ============
@@ -179,8 +164,11 @@ click), enum34, numpy.
 
 Development also requires (see requirements-dev.txt) Cython and other packages.
 
-Rasterio binaries for OS X
---------------------------
+Installing from binaries
+------------------------
+
+OS X
+----
 
 Binary wheels with the GDAL, GEOS, and PROJ4 libraries included are available
 for OS X versions 10.7+ starting with Rasterio version 0.17. To install, just
@@ -195,6 +183,23 @@ you must build from a source distribution (see below).
 Binary wheels for other operating systems will be available in a future
 release.
 
+Windows
+-------
+
+Binary wheels for rasterio and GDAL are created by Christoph Gohlke and are
+available from his website.
+
+To install rasterio, simply download both binaries for your system (`rasterio
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#rasterio>`__ and `GDAL
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal>`__) and run something like
+this from the downloads folder:
+
+.. code-block:: console
+
+    $ pip install -U pip 
+    $ pip install GDAL-1.11.2-cp27-none-win32.whl
+    $ pip install rasterio-0.24.0-cp27-none-win32.whl
+
 Installing from the source distribution
 ---------------------------------------
 
@@ -233,26 +238,28 @@ For a Homebrew based Python environment, do the following.
 Windows
 -------
 
-Windows binary packages created by Christoph Gohlke are available `here
-<http://www.lfd.uci.edu/~gohlke/pythonlibs/#rasterio>`_.
-
 You can download a binary distribution of GDAL from `here
-<http://www.gisinternals.com/release.php>`_.  You will also need to download
+<http://www.gisinternals.com/release.php>`__.  You will also need to download
 the compiled libraries and headers (include files).
 
 When building from source on Windows, it is important to know that setup.py
-cannot rely on gdal-config, which is only present on UNIX systems, to discover 
-the locations of header files and libraries that rasterio needs to compile its 
-C extensions. On Windows, these paths need to be provided by the user. 
-You will need to find the include files and the library files for gdal and 
-use setup.py as follows.
+cannot rely on gdal-config, which is only present on UNIX systems, to discover
+the locations of header files and libraries that rasterio needs to compile its
+C extensions. On Windows, these paths need to be provided by the user. You
+will need to find the include files and the library files for gdal and use
+setup.py as follows.
 
 .. code-block:: console
 
     $ python setup.py build_ext -I<path to gdal include files> -lgdal_i -L<path to gdal library>
     $ python setup.py install
 
-Note: The GDAL dll (gdal111.dll) and gdal-data directory need to be in your 
+We have had success compiling code using the same version of Microsoft's
+Visual Studio used to compile the targeted version of Python (more info on
+versions used `here
+<https://docs.python.org/devguide/setup.html#windows>`__.).
+
+Note: The GDAL dll (gdal111.dll) and gdal-data directory need to be in your
 Windows PATH otherwise rasterio will fail to work.
 
 Testing
@@ -264,6 +271,12 @@ From the repo directory, run py.test
 
     $ py.test
 
+
+Note: some tests do not succeed on Windows (see
+`#66
+<https://github.com/mapbox/rasterio/issues/66>`__.).
+
+
 Documentation
 -------------
 
diff --git a/docs/cli.rst b/docs/cli.rst
index f32ab95..8db75d5 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -1,11 +1,11 @@
 Command Line Interface
 ======================
 
-Rasterio's new command line interface is a program named "rio".
+Rasterio's command line interface is a program named "rio".
 
 .. code-block:: console
 
-    $ rio
+    $ rio --help
     Usage: rio [OPTIONS] COMMAND [ARGS]...
 
       Rasterio command line interface.
@@ -18,17 +18,23 @@ Rasterio's new command line interface is a program named "rio".
 
     Commands:
       bounds     Write bounding boxes to stdout as GeoJSON.
+      calc       Raster data calculator.
+      convert    Copy and convert raster dataset.
+      edit-info  Edit dataset metadata.
       env        Print information about the rio environment.
       info       Print information about a data file.
       insp       Open a data file and start an interpreter.
       mask       Mask in raster using features.
       merge      Merge a stack of raster datasets.
+      overview   Construct overviews in an existing dataset.
       rasterize  Rasterize features.
       sample     Sample a dataset.
-      shapes     Write the shapes of features.
+      shapes     Write shapes extracted from bands or masks.
       stack      Stack a number of bands into a multiband dataset.
+      transform  Transform coordinates.
+      warp       Warp a raster dataset.
 
-It is developed using `Click <http://click.pocoo.org/3/>`__.
+It is developed using `Click <http://click.pocoo.org/>`__.
 
 Commands are shown below. See ``--help`` of individual commands for more
 details.
@@ -130,6 +136,41 @@ efficiently in Python.
 Please see `calc.rst <calc.rst>`__ for more details.
 
 
+convert
+-------
+
+New in 0.25
+
+Like ``gdal_translate``, rio-convert copies and converts raster datasets to
+other data types and formats. 
+
+Data values may be linearly scaled when copying by using the ``--scale-ratio``
+and ``--scale-offset`` options. Destination raster values are calculated as
+
+.. code-block:: python
+
+    dst = scale_ratio * src + scale_offset
+
+For example, to scale uint16 data with an actual range of 0-4095 to 0-255
+as uint8:
+
+.. code-block:: console
+
+    $ rio convert in16.tif out8.tif --dtype uint8 --scale-ratio 0.0625
+
+Format specific creation options may also be passed using --co. To tile a
+new GeoTIFF output file, add the following.
+
+.. code-block:: console
+
+    --co tiled=true --co blockxsize=256 --co blockysize=256
+
+To compress it using the LZW method, add
+
+.. code-block:: console
+
+    --co compress=LZW
+
 edit-info
 ---------
 
@@ -156,7 +197,7 @@ set its `affine transformation matrix <https://github.com/mapbox/rasterio/blob/m
 
 .. code-block:: console
 
-    $ rio edit-info --transform "[300.0, 0.0, 101985.0, 0.0, -300.0, 2826915.0]"
+    $ rio edit-info --transform "[300.0, 0.0, 101985.0, 0.0, -300.0, 2826915.0]" example.tif
 
 or set its nodata value to, e.g., `0`:
 
@@ -308,6 +349,36 @@ datasets.
 
     $ rio merge rasterio/tests/data/R*.tif merged.tif
 
+overview
+--------
+
+New in 0.25
+
+A pyramid of overviews computed once and stored in the dataset using
+rio-overview can improve performance in some applications.
+
+The decimation levels at which to build overviews can be specified as a
+comma separated list
+
+.. code-block:: console
+
+    $ rio pyramid --build 2,4,8,16
+
+or a base and range of exponents.
+
+.. code-block:: console
+
+    $ rio pyramid --build 2^1..4
+
+Note that overviews can not currently be removed and are not automatically
+updated when the dataset's primary bands are modified.
+
+Information about existing overviews can be printed using the --ls option.
+
+.. code-block:: console
+
+    $ rio pyramid --ls
+
 rasterize
 ---------
 
@@ -323,7 +394,7 @@ raster.
 The resulting file will have an upper left coordinate determined by the bounds
 of the GeoJSON (in EPSG:4326, which is the default), with a
 pixel size of approximately 30 arc seconds.  Pixels whose center is within the
-polygon or that are selected by brezenhams line algorithm will be burned in
+polygon or that are selected by Bresenham's line algorithm will be burned in
 with a default value of 1.
 
 It is possible to rasterize into an existing raster and use an alternative
@@ -446,4 +517,52 @@ a raster dataset, do the following.
     $ echo "[-78.0, 23.0, -76.0, 25.0]" | rio transform - --dst-crs tests/data/RGB.byte.tif --precision 2
     [192457.13, 2546667.68, 399086.97, 2765319.94]
 
+
+warp
+----
+
+New in 0.25
+
+The warp command warps (reprojects) a raster based on parameters that can be
+obtained from a template raster, or input directly.  The output is always
+overwritten.
+
+
+To copy coordinate reference system, transform, and dimensions from a template
+raster, do the following:
+
+.. code-block:: console
+
+    $ rio warp input.tif output.tif --like template.tif
+
+You can specify an output coordinate system using a PROJ.4 or EPSG:nnnn string,
+or a JSON text-encoded PROJ.4 object:
+
+.. code-block:: console
+
+    $ rio warp input.tif output.tif --dst-crs EPSG:4326
+
+    $ rio warp input.tif output.tif --dst-crs '+proj=longlat +ellps=WGS84 +datum=WGS84'
+
+You can also specify dimensions, which will automatically calculate appropriate
+resolution based on the relationship between the bounds in the target crs and
+these dimensions:
+
+.. code-block:: console
+
+    $ rio warp input.tif output.tif --dst-crs EPSG:4326 --dimensions 100 200
+
+Or provide output bounds (in source crs) and resolution:
+
+.. code-block:: console
+
+    $ rio warp input.tif output.tif --dst-crs EPSG:4326 --bounds -78 22 -76 24 --res 0.1
+
+Other options are available, see:
+
+.. code-block:: console
+
+    $ rio warp --help
+
+
 Suggestions for other commands are welcome!
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
index 5b0f009..e03e1bf 100644
--- a/rasterio/__init__.py
+++ b/rasterio/__init__.py
@@ -23,7 +23,7 @@ from rasterio import _err, coords, enums
 
 __all__ = [
     'band', 'open', 'drivers', 'copy', 'pad']
-__version__ = "0.24.0"
+__version__ = "0.25.0"
 
 log = logging.getLogger('rasterio')
 class NullHandler(logging.Handler):
@@ -89,9 +89,13 @@ def open(
         raise TypeError("invalid mode: %r" % mode)
     if driver and not isinstance(driver, string_types):
         raise TypeError("invalid driver: %r" % driver)
+
     if transform:
         transform = guard_transform(transform)
-    
+    elif 'affine' in kwargs:
+        affine = kwargs.pop('affine')
+        transform = guard_transform(affine)
+
     if mode == 'r':
         from rasterio._io import RasterReader
         s = RasterReader(path)
diff --git a/rasterio/_base.pxd b/rasterio/_base.pxd
index ca070b8..a193da9 100644
--- a/rasterio/_base.pxd
+++ b/rasterio/_base.pxd
@@ -23,4 +23,5 @@ cdef class DatasetReader:
 
     cdef void *band(self, int bidx)
 
+
 cdef void *_osr_from_crs(object crs)
diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx
index 0cd52b2..b514845 100644
--- a/rasterio/_base.pyx
+++ b/rasterio/_base.pyx
@@ -385,8 +385,8 @@ cdef class DatasetReader(object):
     def window(self, left, bottom, right, top, boundless=False):
         """Returns the window corresponding to the world bounding box.
         If boundless is False, window is limited to extent of this dataset."""
-
-        window = tuple(zip(self.index(left, top), self.index(right, bottom)))
+        EPS = 1.0e-8
+        window = tuple(zip(self.index(left + EPS, top - EPS), self.index(right + EPS, bottom - EPS)))
         if boundless:
             return window
         else:
@@ -406,6 +406,7 @@ cdef class DatasetReader(object):
 
     @property
     def meta(self):
+        """The basic metadata of this dataset."""
         m = {
             'driver': self.driver,
             'dtype': self.dtypes[0],
@@ -416,12 +417,27 @@ cdef class DatasetReader(object):
             'crs': self.crs,
             'transform': self.affine.to_gdal(),
             'affine': self.affine,
-            'blockxsize': self.block_shapes[0][1],
-            'blockysize': self.block_shapes[0][0],
-            'tiled': self.block_shapes[0][1] != self.width }
+        }
         self._read = True
         return m
 
+
+    property profile:
+        """Basic metadata and creation options of this dataset.
+
+        May be passed as keyword arguments to `rasterio.open()` to
+        create a clone of this dataset.
+        """
+        def __get__(self):
+            m = self.meta
+            m.update(self.tags(ns='rio_creation_kwds'))
+            m.update(
+                blockxsize=self.block_shapes[0][1],
+                blockysize=self.block_shapes[0][0],
+                tiled=self.block_shapes[0][1] != self.width)
+            return m
+
+
     def lnglat(self):
         w, s, e, n = self.bounds
         cx = (w + e)/2.0
@@ -582,6 +598,20 @@ cdef class DatasetReader(object):
     def kwds(self):
         return self.tags(ns='rio_creation_kwds')
 
+
+    # Overviews.
+    def overviews(self, bidx):
+        cdef void *hovband = NULL
+        cdef void *hband = self.band(bidx)
+        num_overviews = _gdal.GDALGetOverviewCount(hband)
+        factors = []
+        for i in range(num_overviews):
+            hovband = _gdal.GDALGetOverview(hband, i)
+            # Compute the overview factor only from the xsize (width).
+            xsize = _gdal.GDALGetRasterBandXSize(hovband)
+            factors.append(int(round(float(self.width)/float(xsize))))
+        return factors
+
 # Window utils
 # A window is a 2D ndarray indexer in the form of a tuple:
 # ((row_start, row_stop), (col_start, col_stop))
diff --git a/rasterio/_features.pxd b/rasterio/_features.pxd
index 9ffcacd..e4898ae 100644
--- a/rasterio/_features.pxd
+++ b/rasterio/_features.pxd
@@ -16,14 +16,14 @@ cdef class GeomBuilder:
 
 
 cdef class OGRGeomBuilder:
-    cdef void * _createOgrGeometry(self, int geom_type)
+    cdef void * _createOgrGeometry(self, int geom_type) except NULL
     cdef _addPointToGeometry(self, void *cogr_geometry, object coordinate)
-    cdef void * _buildPoint(self, object coordinates)
-    cdef void * _buildLineString(self, object coordinates)
-    cdef void * _buildLinearRing(self, object coordinates)
-    cdef void * _buildPolygon(self, object coordinates)
-    cdef void * _buildMultiPoint(self, object coordinates)
-    cdef void * _buildMultiLineString(self, object coordinates)
-    cdef void * _buildMultiPolygon(self, object coordinates)
-    cdef void * _buildGeometryCollection(self, object coordinates)
-    cdef void * build(self, object geom)
+    cdef void * _buildPoint(self, object coordinates) except NULL
+    cdef void * _buildLineString(self, object coordinates) except NULL
+    cdef void * _buildLinearRing(self, object coordinates) except NULL
+    cdef void * _buildPolygon(self, object coordinates) except NULL
+    cdef void * _buildMultiPoint(self, object coordinates) except NULL
+    cdef void * _buildMultiLineString(self, object coordinates) except NULL
+    cdef void * _buildMultiPolygon(self, object coordinates) except NULL
+    cdef void * _buildGeometryCollection(self, object coordinates) except NULL
+    cdef void * build(self, object geom) except NULL
diff --git a/rasterio/_gdal.pxd b/rasterio/_gdal.pxd
index bc42d2e..a4b4154 100644
--- a/rasterio/_gdal.pxd
+++ b/rasterio/_gdal.pxd
@@ -64,7 +64,13 @@ cdef extern from "gdal.h" nogil:
     int GDALGetRasterXSize(void *ds)
     int GDALGetRasterYSize(void *ds)
     int GDALGetRasterCount(void *ds)
+
     void * GDALGetRasterBand(void *ds, int num)
+    void * GDALGetOverview(void *hband, int num)
+
+    int GDALGetRasterBandXSize(void *hband)
+    int GDALGetRasterBandYSize(void *hband)
+
     int GDALSetGeoTransform	(void *ds, double *transform)
     int GDALSetProjection(void *ds, const char *wkt)
 
@@ -125,6 +131,9 @@ cdef extern from "gdal.h" nogil:
     void *GDALGetMaskBand (void *hBand)
     int GDALCreateMaskBand (void *hDS, int flags)
 
+    int GDALGetOverviewCount (void *hBand)
+    int GDALBuildOverviews (void *hDS, const char *resampling, int nOverviews, int *overviews, int nBands, int *bands, void *progress_func, void *progress_data)
+
 cdef extern from "gdalwarper.h":
 
     ctypedef enum GDALResampleAlg:
diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx
index 64ef9ec..b1bb71b 100644
--- a/rasterio/_io.pyx
+++ b/rasterio/_io.pyx
@@ -20,7 +20,8 @@ from rasterio import dtypes
 from rasterio.coords import BoundingBox
 from rasterio.five import text_type, string_types
 from rasterio.transform import Affine
-from rasterio.enums import ColorInterp
+from rasterio.enums import ColorInterp, Resampling
+from rasterio.sample import sample_gen
 
 
 log = logging.getLogger('rasterio')
@@ -38,7 +39,16 @@ log.addHandler(NullHandler())
 cdef bint in_dtype_range(value, dtype):
     """Returns True if value is in the range of dtype, else False."""
     infos = {
-            'c': np.finfo, 'f': np.finfo, 'i': np.iinfo, 'u': np.iinfo}
+        'c': np.finfo,
+        'f': np.finfo,
+        'i': np.iinfo,
+        'u': np.iinfo,
+        # Cython 0.22 returns dtype.kind as an int and will not cast to a char
+        99: np.finfo,
+        102: np.finfo,
+        105: np.iinfo,
+        117: np.iinfo
+    }
     rng = infos[np.dtype(dtype).kind](dtype)
     return rng.min <= value <= rng.max
 
@@ -1173,12 +1183,12 @@ cdef class RasterReader(_base.DatasetReader):
         Iterable, yielding dataset values for the specified `indexes`
         as an ndarray.
         """
-        for x, y in xy:
-            r, c = self.index(x, y)
-            window = ((r, r+1), (c, c+1))
-            data = self.read(
-                    indexes, window=window, masked=False, boundless=True)
-            yield data[:,0,0]
+        # In https://github.com/mapbox/rasterio/issues/378 a user has
+        # found what looks to be a Cython generator bug. Until that can
+        # be confirmed and fixed, the workaround is a pure Python 
+        # generator implemented in sample.py.
+        return sample_gen(self, xy, indexes)
+
 
 cdef class RasterUpdater(RasterReader):
     # Read-write access to raster data and metadata.
@@ -1670,14 +1680,26 @@ cdef class RasterUpdater(RasterReader):
         # GPI_Gray=0,  GPI_RGB=1, GPI_CMYK=2,     GPI_HLS=3
         hTable = _gdal.GDALCreateColorTable(1)
         vals = range(256)
+
         for i, rgba in colormap.items():
+
+            if len(rgba) == 4 and self.driver in ('GTiff'):
+                raise ValueError(
+                    "Format '%s' doesn't support 4 component colormap entries"
+                    % self.driver)
+
+            elif len(rgba) == 3:
+                rgba = tuple(rgba) + (255,)
+
             if i not in vals:
                 log.warn("Invalid colormap key %d", i)
                 continue
+
             color.c1, color.c2, color.c3, color.c4 = rgba
             _gdal.GDALSetColorEntry(hTable, i, &color)
+
         # TODO: other color interpretations?
-        _gdal.GDALSetRasterColorInterpretation(hBand, 2)
+        _gdal.GDALSetRasterColorInterpretation(hBand, 1)
         _gdal.GDALSetRasterColorTable(hBand, hTable)
         _gdal.GDALDestroyColorTable(hTable)
 
@@ -1686,9 +1708,9 @@ cdef class RasterUpdater(RasterReader):
         mask.
 
         The optional `window` argument takes a tuple like:
-        
+
             ((row_start, row_stop), (col_start, col_stop))
-            
+
         specifying a raster subset to write into.
         """
         cdef void *hband
@@ -1727,7 +1749,31 @@ cdef class RasterUpdater(RasterReader):
         else:
             retval = io_ubyte(
                 hmask, 1, xoff, yoff, width, height, mask)
-        
+
+    def build_overviews(self, factors, resampling=Resampling.nearest):
+        """Build overviews at one or more decimation factors for all
+        bands of the dataset."""
+        cdef int *factors_c = NULL
+        cdef const char *resampling_c = NULL
+
+        if self._hds == NULL:
+            raise ValueError("can't write closed raster file")
+
+        # Allocate arrays.
+        if factors:
+            factors_c = <int *>_gdal.CPLMalloc(len(factors)*sizeof(int))
+            for i, factor in enumerate(factors):
+                factors_c[i] = factor
+
+            with cpl_errs:
+                resampling_b = resampling.value.encode('utf-8')
+                resampling_c = resampling_b
+                err = _gdal.GDALBuildOverviews(self._hds, resampling_c,
+                    len(factors), factors_c, 0, NULL, NULL, NULL)
+
+            if factors_c != NULL:
+                _gdal.CPLFree(factors_c)
+
 
 cdef class InMemoryRaster:
     """
diff --git a/rasterio/_warp.pyx b/rasterio/_warp.pyx
index 8e13061..1e267af 100644
--- a/rasterio/_warp.pyx
+++ b/rasterio/_warp.pyx
@@ -8,6 +8,7 @@ cimport numpy as np
 
 from rasterio cimport _base, _gdal, _ogr, _io, _features
 from rasterio import dtypes
+from rasterio.errors import RasterioDriverRegistrationError
 
 
 cdef extern from "gdalwarper.h" nogil:
@@ -259,7 +260,10 @@ def _reproject(
 
         hrdriver = _gdal.GDALGetDriverByName("MEM")
         if hrdriver == NULL:
-            raise ValueError("NULL driver for 'MEM'")
+            raise RasterioDriverRegistrationError(
+                "'MEM' driver not found. Check that this call is contained "
+                "in a `with rasterio.drivers()` or `with rasterio.open()` "
+                "block.")
 
         hdsin = _gdal.GDALCreate(
                     hrdriver, "input", cols, rows, 
@@ -301,9 +305,14 @@ def _reproject(
             destination = destination.reshape(1, *destination.shape)
         if destination.shape[0] != src_count:
             raise ValueError("Destination's shape is invalid")
+
         hrdriver = _gdal.GDALGetDriverByName("MEM")
         if hrdriver == NULL:
-            raise ValueError("NULL driver for 'MEM'")
+            raise RasterioDriverRegistrationError(
+                "'MEM' driver not found. Check that this call is contained "
+                "in a `with rasterio.drivers()` or `with rasterio.open()` "
+                "block.")
+
         _, rows, cols = destination.shape
         hdsout = _gdal.GDALCreate(
                         hrdriver, "output", cols, rows, src_count, 
diff --git a/rasterio/crs.py b/rasterio/crs.py
index c70f03e..775a54f 100644
--- a/rasterio/crs.py
+++ b/rasterio/crs.py
@@ -10,9 +10,15 @@
 #   {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}
 #
 
-from rasterio._base import is_geographic_crs, is_projected_crs
+import json
+from rasterio._base import is_geographic_crs, is_projected_crs, is_same_crs
 from rasterio.five import string_types
 
+
+def is_valid_crs(crs):
+    return is_geographic_crs(crs) or is_projected_crs(crs)
+
+
 def to_string(crs):
     """Turn a parameter mapping into a more conventional PROJ.4 string.
 
@@ -24,22 +30,39 @@ def to_string(crs):
     items = []
     for k, v in sorted(filter(
             lambda x: x[0] in all_proj_keys and x[1] is not False and (
-                isinstance(x[1], (bool, int, float)) or 
+                isinstance(x[1], (bool, int, float)) or
                 isinstance(x[1], string_types)),
-            crs.items() )):
+            crs.items())):
         items.append(
             "+" + "=".join(
                 map(str, filter(
-                    lambda y: (y or y == 0) and y is not True, (k, v)))) )
+                    lambda y: (y or y == 0) and y is not True, (k, v)))))
     return " ".join(items)
 
+
 def from_string(prjs):
     """Turn a PROJ.4 string into a mapping of parameters.
 
     Bare parameters like "+no_defs" are given a value of ``True``. All keys
     are checked against the ``all_proj_keys`` list.
+
+    EPSG:nnnn is allowed.
+
+    JSON text-encoded strings are allowed.
     """
+
+    if '{' in prjs:
+        # may be json, try to decode it
+        try:
+            return json.loads(prjs, strict=False)
+        except ValueError:
+            raise ValueError('crs appears to be JSON but is not valid')
+
+    if 'EPSG:' in prjs.upper():
+        return from_epsg(prjs.split(':')[1])
+
     parts = [o.lstrip('+') for o in prjs.strip().split()]
+
     def parse(v):
         if v in ('True', 'true'):
             return True
@@ -54,10 +77,13 @@ def from_string(prjs):
                 return float(v)
             except ValueError:
                 return v
+
     items = map(
         lambda kv: len(kv) == 2 and (kv[0], parse(kv[1])) or (kv[0], True),
-        (p.split('=') for p in parts) )
-    return dict((k,v) for k, v in items if k in all_proj_keys)
+        (p.split('=') for p in parts))
+
+    return dict((k, v) for k, v in items if k in all_proj_keys)
+
 
 def from_epsg(code):
     """Given an integer code, returns an EPSG-like mapping.
@@ -183,5 +209,5 @@ _param_data = """
 
 _lines = filter(lambda x: len(x) > 1, _param_data.split("\n"))
 all_proj_keys = list(
-    set(line.split()[0].lstrip("+").strip() for line in _lines) 
+    set(line.split()[0].lstrip("+").strip() for line in _lines)
     ) + ['no_mayo']
diff --git a/rasterio/enums.py b/rasterio/enums.py
index 3926680..669538e 100644
--- a/rasterio/enums.py
+++ b/rasterio/enums.py
@@ -1,5 +1,6 @@
 
-from enum import IntEnum
+from enum import Enum, IntEnum
+
 
 class ColorInterp(IntEnum):
     undefined=0
@@ -16,4 +17,14 @@ class ColorInterp(IntEnum):
     cyan=10
     magenta=11
     yellow=12
-    black=13
\ No newline at end of file
+    black=13
+
+
+class Resampling(Enum):
+    nearest='NEAREST'
+    gauss='GAUSS'
+    cubic='CUBIC'
+    average='AVERAGE',
+    mode='MODE'
+    average_magphase='AVERAGE_MAGPHASE'
+    none='NONE'
diff --git a/rasterio/errors.py b/rasterio/errors.py
new file mode 100644
index 0000000..288fb0e
--- /dev/null
+++ b/rasterio/errors.py
@@ -0,0 +1,9 @@
+"""A module of errors."""
+
+
+class RasterioIOError(IOError):
+    """A failure to open a dataset using the presently registered drivers."""
+
+
+class RasterioDriverRegistrationError(ValueError):
+    """To be raised when, eg, _gdal.GDALGetDriverByName("MEM") returns NULL"""
diff --git a/rasterio/rio/__init__.py b/rasterio/rio/__init__.py
index e736ca1..570be93 100644
--- a/rasterio/rio/__init__.py
+++ b/rasterio/rio/__init__.py
@@ -1 +1,3 @@
-# module of CLI commands.
+"""
+Rasterio commandline interface components
+"""
diff --git a/rasterio/rio/bands.py b/rasterio/rio/bands.py
index b167f44..09b9c16 100644
--- a/rasterio/rio/bands.py
+++ b/rasterio/rio/bands.py
@@ -1,13 +1,12 @@
 import logging
-import sys
 
 import click
 from cligj import files_inout_arg, format_opt
 
+from .helpers import resolve_inout
+from . import options
 import rasterio
-
 from rasterio.five import zip_longest
-from rasterio.rio.cli import cli, bidx_mult_opt, output_opt, resolve_inout
 
 
 PHOTOMETRIC_CHOICES = [val.lower() for val in [
@@ -22,16 +21,17 @@ PHOTOMETRIC_CHOICES = [val.lower() for val in [
 
 
 # Stack command.
- at cli.command(short_help="Stack a number of bands into a multiband dataset.")
+ at click.command(short_help="Stack a number of bands into a multiband dataset.")
 @files_inout_arg
- at output_opt
+ at options.output_opt
 @format_opt
- at bidx_mult_opt
+ at options.bidx_mult_opt
 @click.option('--photometric', default=None,
               type=click.Choice(PHOTOMETRIC_CHOICES),
               help="Photometric interpretation")
+ at options.creation_options
 @click.pass_context
-def stack(ctx, files, output, driver, bidx, photometric):
+def stack(ctx, files, output, driver, bidx, photometric, creation_options):
     """Stack a number of bands from one or more input files into a
     multiband dataset.
 
@@ -63,7 +63,6 @@ def stack(ctx, files, output, driver, bidx, photometric):
       rio stack RGB.byte.tif --bidx ..2 RGB.byte.tif --bidx 3.. -o stacked.tif
 
     """
-    import numpy as np
 
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 2
     logger = logging.getLogger('rio')
@@ -97,6 +96,7 @@ def stack(ctx, files, output, driver, bidx, photometric):
 
             with rasterio.open(files[0]) as first:
                 kwargs = first.meta
+                kwargs.update(**creation_options)
                 kwargs['transform'] = kwargs.pop('affine')
 
             kwargs.update(
diff --git a/rasterio/rio/calc.py b/rasterio/rio/calc.py
index 9a8c4bb..7018174 100644
--- a/rasterio/rio/calc.py
+++ b/rasterio/rio/calc.py
@@ -1,18 +1,17 @@
 # Calc command.
 
+from distutils.version import LooseVersion
 import logging
-import sys
-import traceback
 
 import click
 import snuggs
 from cligj import files_inout_arg
 
+from .helpers import resolve_inout
+from . import options
 import rasterio
 from rasterio.fill import fillnodata
 from rasterio.features import sieve
-from rasterio.rio.cli import (
-    cli, dtype_opt, masked_opt, output_opt, resolve_inout)
 
 
 def get_bands(inputs, d, i=None):
@@ -31,18 +30,19 @@ def read_array(ix, subix=None, dtype=None):
     return arr
 
 
- at cli.command(short_help="Raster data calculator.")
+ at click.command(short_help="Raster data calculator.")
 @click.argument('command')
 @files_inout_arg
- at output_opt
+ at options.output_opt
 @click.option('--name', multiple=True,
               help='Specify an input file with a unique short (alphas only) '
                    'name for use in commands like '
                    '"a=tests/data/RGB.byte.tif".')
- at dtype_opt
- at masked_opt
+ at options.dtype_opt
+ at options.masked_opt
+ at options.creation_options
 @click.pass_context
-def calc(ctx, command, files, output, name, dtype, masked):
+def calc(ctx, command, files, output, name, dtype, masked, creation_options):
     """A raster data calculator
 
     Evaluates an expression using input datasets and writes the result
@@ -96,6 +96,7 @@ def calc(ctx, command, files, output, name, dtype, masked):
 
             with rasterio.open(inputs[0][1]) as first:
                 kwargs = first.meta
+                kwargs.update(**creation_options)
                 kwargs['transform'] = kwargs.pop('affine')
                 dtype = dtype or first.meta['dtype']
                 kwargs['dtype'] = dtype
@@ -122,6 +123,10 @@ def calc(ctx, command, files, output, name, dtype, masked):
 
             res = snuggs.eval(command, **ctxkwds)
 
+            if (isinstance(res, np.ma.core.MaskedArray) and
+                    tuple(LooseVersion(np.__version__).version) < (1, 9, 0)):
+                res = res.filled(kwargs['nodata'])
+
             if len(res.shape) == 3:
                 results = np.ndarray.astype(res, dtype, copy=False)
             else:
@@ -139,8 +144,3 @@ def calc(ctx, command, files, output, name, dtype, masked):
         click.echo(' ' +  ' ' * err.offset + "^")
         click.echo(err)
         raise click.Abort()
-    #except Exception as err:
-    #    t, v, tb = sys.exc_info()
-    #    for line in traceback.format_exception_only(t, v):
-    #        click.echo(line, nl=False)
-    #    raise click.Abort()
diff --git a/rasterio/rio/cli.py b/rasterio/rio/cli.py
deleted file mode 100644
index 7bfea62..0000000
--- a/rasterio/rio/cli.py
+++ /dev/null
@@ -1,255 +0,0 @@
-"""Rasterio's command line interface core."""
-
-import json
-import logging
-import os
-import sys
-import traceback
-
-import click
-from cligj import verbose_opt, quiet_opt
-
-import rasterio
-
-
-def configure_logging(verbosity):
-    log_level = max(10, 30 - 10*verbosity)
-    logging.basicConfig(stream=sys.stderr, level=log_level)
-
-
-class BrokenCommand(click.Command):
-    """A dummy command that provides help for broken plugins."""
-
-    def __init__(self, name):
-        click.Command.__init__(self, name)
-        self.help = (
-            "Warning: entry point could not be loaded. Contact "
-            "its author for help.\n\n\b\n"
-            + traceback.format_exc())
-        self.short_help = (
-            "Warning: could not load plugin. See `rio %s --help`." % self.name)
-
-
-class RioGroup(click.Group):
-    """Custom formatting for the commands of broken plugins."""
-
-    def format_commands(self, ctx, formatter):
-        """Extra format methods for multi methods that adds all the commands
-        after the options.
-        """
-        rows = []
-        for subcommand in self.list_commands(ctx):
-            cmd = self.get_command(ctx, subcommand)
-            # What is this, the tool lied about a command.  Ignore it
-            if cmd is None:
-                continue
-
-            help = cmd.short_help or ''
-
-            # Mark broken subcommands with a pile of poop.
-            name = cmd.name
-            if isinstance(cmd, BrokenCommand):
-                if os.environ.get('RIO_HONESTLY'):
-                    name += u'\U0001F4A9'
-                else:
-                    name += u'\u2020'
-
-            rows.append((name, help))
-
-        if rows:
-            with formatter.section('Commands'):
-                formatter.write_dl(rows)
-
-
-# The CLI command group.
- at click.group(help="Rasterio command line interface.", cls=RioGroup)
- at verbose_opt
- at quiet_opt
- at click.version_option(version=rasterio.__version__, message='%(version)s')
- at click.pass_context
-def cli(ctx, verbose, quiet):
-    verbosity = verbose - quiet
-    configure_logging(verbosity)
-    ctx.obj = {}
-    ctx.obj['verbosity'] = verbosity
-
-
-# Common arguments and options
-
-# TODO: move file_in_arg and file_out_arg to cligj
-
-# Singular input file
-file_in_arg = click.argument(
-    'INPUT',
-    type=click.Path(exists=True, resolve_path=True))
-
-# Singular output file
-file_out_arg = click.argument(
-    'OUTPUT',
-    type=click.Path(resolve_path=True))
-
-bidx_opt = click.option(
-    '-b', '--bidx',
-    type=int,
-    default=1,
-    help="Input file band index (default: 1)")
-
-bidx_mult_opt = click.option(
-    '-b', '--bidx',
-    multiple=True,
-    help="Indexes of input file bands.")
-
-# TODO: may be better suited to cligj
-bounds_opt = click.option(
-    '--bounds',
-    nargs=4, type=float, default=None,
-    help='Output bounds: left, bottom, right, top.')
-
-dtype_opt = click.option(
-    '-t', '--dtype',
-    type=click.Choice([
-        'ubyte', 'uint8', 'uint16', 'int16', 'uint32', 'int32',
-        'float32', 'float64']),
-    default=None,
-    help="Output data type (default: float64).")
-
-like_file_opt = click.option(
-    '--like',
-    type=click.Path(exists=True),
-    help='Raster dataset to use as a template for obtaining affine '
-         'transform (bounds and resolution), crs, data type, and driver '
-         'used to create the output.')
-
-masked_opt = click.option(
-    '--masked/--not-masked',
-    default=True,
-    help="Evaluate expressions using masked arrays (the default) or ordinary "
-         "numpy arrays.")
-
-output_opt = click.option(
-    '-o', '--output',
-    default=None,
-    type=click.Path(resolve_path=True),
-    help="Path to output file (optional alternative to a positional arg "
-         "for some commands).")
-
-
-resolution_opt = click.option(
-    '-r', '--res',
-    multiple=True, type=float, default=None,
-    help='Output dataset resolution in units of coordinate '
-         'reference system. Pixels assumed to be square if this option '
-         'is used once, otherwise use: '
-         '--res pixel_width --res pixel_height')
-
-"""
-Registry of command line options (also see cligj options):
--a, --all: Use all pixels touched by features.  In rio-mask, rio-rasterize
---as-mask/--not-as-mask: interpret band as mask or not.  In rio-shapes
---band/--mask: use band or mask.  In rio-shapes
---bbox:
--b, --bidx: band index(es) (singular or multiple value versions).
-    In rio-info, rio-sample, rio-shapes, rio-stack (different usages)
---bounds: bounds in world coordinates.
-    In rio-info, rio-rasterize (different usages)
---count: count of bands.  In rio-info
---crop: Crop raster to extent of features.  In rio-mask
---crs: CRS of input raster.  In rio-info
---default-value: default for rasterized pixels.  In rio-rasterize
---dimensions: Output width, height.  In rio-rasterize
---dst-crs: destination CRS.  In rio-transform
---fill: fill value for pixels not covered by features.  In rio-rasterize
---formats: list available formats.  In rio-info
---height: height of raster.  In rio-info
--i, --invert: Invert mask created from features: In rio-mask
--j, --geojson-mask: GeoJSON for masking raster.  In rio-mask
---lnglat: geograhpic coordinates of center of raster.  In rio-info
---masked/--not-masked: read masked data from source file.
-    In rio-calc, rio-info
--m, --mode: output file mode (r, r+).  In rio-insp
---name: input file name alias.  In rio-calc
---nodata: nodata value.  In rio-info, rio-merge (different usages)
---photometric: photometric interpretation.  In rio-stack
---property: GeoJSON property to use as values for rasterize.  In rio-rasterize
--r, --res: output resolution.
-    In rio-info, rio-rasterize (different usages.  TODO: try to combine
-    usages, prefer rio-rasterize version)
---sampling: Inverse of sampling fraction.  In rio-shapes
---shape: shape (width, height) of band.  In rio-info
---src-crs: source CRS.
-    In rio-insp, rio-rasterize (different usages.  TODO: consolidate usages)
---stats: print raster stats.  In rio-inf
--t, --dtype: data type.  In rio-calc, rio-info (different usages)
---width: width of raster.  In rio-info
---with-nodata/--without-nodata: include nodata regions or not.  In rio-shapes.
--v, --tell-me-more, --verbose
-"""
-
-
-def coords(obj):
-    """Yield all coordinate coordinate tuples from a geometry or feature.
-    From python-geojson package."""
-    if isinstance(obj, (tuple, list)):
-        coordinates = obj
-    elif 'geometry' in obj:
-        coordinates = obj['geometry']['coordinates']
-    else:
-        coordinates = obj.get('coordinates', obj)
-    for e in coordinates:
-        if isinstance(e, (float, int)):
-            yield tuple(coordinates)
-            break
-        else:
-            for f in coords(e):
-                yield f
-
-
-def write_features(
-        fobj, collection, sequence=False, geojson_type='feature', use_rs=False,
-        **dump_kwds):
-    """Read an iterator of (feat, bbox) pairs and write to file using
-    the selected modes."""
-    # Sequence of features expressed as bbox, feature, or collection.
-    if sequence:
-        for feat in collection():
-            xs, ys = zip(*coords(feat))
-            bbox = (min(xs), min(ys), max(xs), max(ys))
-            if use_rs:
-                fobj.write(u'\u001e')
-            if geojson_type == 'feature':
-                fobj.write(json.dumps(feat, **dump_kwds))
-            elif geojson_type == 'bbox':
-                fobj.write(json.dumps(bbox, **dump_kwds))
-            else:
-                fobj.write(
-                    json.dumps({
-                        'type': 'FeatureCollection',
-                        'bbox': bbox,
-                        'features': [feat]}, **dump_kwds))
-            fobj.write('\n')
-    # Aggregate all features into a single object expressed as
-    # bbox or collection.
-    else:
-        features = list(collection())
-        if geojson_type == 'bbox':
-            fobj.write(json.dumps(collection.bbox, **dump_kwds))
-        elif geojson_type == 'feature':
-            fobj.write(json.dumps(features[0], **dump_kwds))
-        else:
-            fobj.write(json.dumps({
-                'bbox': collection.bbox,
-                'type': 'FeatureCollection',
-                'features': features},
-                **dump_kwds))
-        fobj.write('\n')
-
-
-def resolve_inout(input=None, output=None, files=None):
-    """Resolves inputs and outputs from standard args and options.
-
-    Returns `output_filename, [input_filename0, ...]`."""
-    resolved_output = output or (files[-1] if files else None)
-    resolved_inputs = (
-        [input] if input else [] +
-        list(files[:-1 if not output else None]) if files else [])
-    return resolved_output, resolved_inputs
diff --git a/rasterio/rio/convert.py b/rasterio/rio/convert.py
new file mode 100644
index 0000000..37fea24
--- /dev/null
+++ b/rasterio/rio/convert.py
@@ -0,0 +1,105 @@
+"""File translation command"""
+
+import logging
+import warnings
+
+import click
+from cligj import files_inout_arg, format_opt
+import numpy as np
+
+from .helpers import resolve_inout
+from . import options
+import rasterio
+
+
+warnings.simplefilter('default')
+
+
+ at click.command(short_help="Copy and convert raster dataset.")
+ at click.argument(
+    'files',
+    nargs=-1,
+    type=click.Path(resolve_path=True),
+    required=True,
+    metavar="INPUT OUTPUT")
+ at options.output_opt
+ at format_opt
+ at options.dtype_opt
+ at click.option('--scale-ratio', type=float, default=None,
+              help="Source to destination scaling ratio.")
+ at click.option('--scale-offset', type=float, default=None,
+              help="Source to destination scaling offset.")
+ at options.creation_options
+ at click.pass_context
+def convert(
+        ctx, files, output, driver, dtype, scale_ratio, scale_offset,
+        creation_options):
+    """Copy and convert raster datasets to other data types and formats.
+
+    Data values may be linearly scaled when copying by using the
+    --scale-ratio and --scale-offset options. Destination raster values
+    are calculated as
+
+      dst = scale_ratio * src + scale_offset
+
+    For example, to scale uint16 data with an actual range of 0-4095 to
+    0-255 as uint8:
+
+      $ rio convert in16.tif out8.tif --dtype uint8 --scale-ratio 0.0625
+
+    Format specific creation options may also be passed using --co. To
+    tile a new GeoTIFF output file, do the following.
+
+      --co tiled=true --co blockxsize=256 --co blockysize=256
+
+    To compress it using the LZW method, add
+
+      --co compress=LZW
+
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+
+    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+
+        outputfile, files = resolve_inout(files=files, output=output)
+        inputfile = files[0]
+
+        with rasterio.open(inputfile) as src:
+
+            # Use the input file's profile, updated by CLI
+            # options, as the profile for the output file.
+            profile = src.profile
+
+            if 'affine' in profile:
+                profile['transform'] = profile.pop('affine')
+
+            if driver:
+                profile['driver'] = driver
+
+            if dtype:
+                profile['dtype'] = dtype
+            dst_dtype = profile['dtype']
+
+            profile.update(**creation_options)
+
+            with rasterio.open(outputfile, 'w', **profile) as dst:
+
+                data = src.read()
+
+                if scale_ratio:
+                    # Cast to float64 before multiplying.
+                    data = data.astype('float64', casting='unsafe', copy=False)
+                    np.multiply(
+                        data, scale_ratio, out=data, casting='unsafe')
+
+                if scale_offset:
+                    # My understanding of copy=False is that this is a
+                    # no-op if the array was cast for multiplication.
+                    data = data.astype('float64', casting='unsafe', copy=False)
+                    np.add(
+                        data, scale_offset, out=data, casting='unsafe')
+
+                # Cast to the output dtype and write.
+                result = data.astype(dst_dtype, casting='unsafe', copy=False)
+                dst.write(result)
diff --git a/rasterio/rio/features.py b/rasterio/rio/features.py
index 2962096..31d84c2 100644
--- a/rasterio/rio/features.py
+++ b/rasterio/rio/features.py
@@ -2,21 +2,20 @@ import json
 import logging
 from math import ceil
 import os
-import sys
 import shutil
 
 import click
+import cligj
 from cligj import (
     precision_opt, indent_opt, compact_opt, projection_geographic_opt,
-    projection_projected_opt, sequence_opt, use_rs_opt,
-    geojson_type_feature_opt, geojson_type_bbox_opt, files_inout_arg,
-    format_opt)
+    projection_mercator_opt, projection_projected_opt, sequence_opt,
+    use_rs_opt, geojson_type_feature_opt, geojson_type_bbox_opt,
+    files_inout_arg, format_opt, geojson_type_collection_opt)
 
+from .helpers import coords, resolve_inout, write_features, to_lower
+from . import options
 import rasterio
 from rasterio.transform import Affine
-from rasterio.rio.cli import (
-    cli, coords, write_features, file_in_arg, file_out_arg, like_file_opt,
-    bounds_opt, resolution_opt, output_opt, resolve_inout)
 
 
 logger = logging.getLogger('rio')
@@ -33,9 +32,9 @@ all_touched_opt = click.option(
 
 
 # Mask command
- at cli.command(short_help='Mask in raster using features.')
- at files_inout_arg
- at output_opt
+ at click.command(short_help='Mask in raster using features.')
+ at cligj.files_inout_arg
+ at options.output_opt
 @click.option('-j', '--geojson-mask', 'geojson_mask',
               type=click.Path(), default=None,
               help='GeoJSON file to use for masking raster.  Use "-" to read '
@@ -50,6 +49,7 @@ all_touched_opt = click.option(
               help='Inverts the mask, so that areas covered by features are'
                    'masked out and areas not covered are retained.  Ignored '
                    'if using --crop')
+ at options.creation_options
 @click.pass_context
 def mask(
         ctx,
@@ -59,7 +59,8 @@ def mask(
         driver,
         all_touched,
         crop,
-        invert):
+        invert,
+        creation_options):
 
     """Masks in raster using GeoJSON features (masks out all areas not covered
     by features), and optionally crops the output raster to the extent of the
@@ -103,9 +104,10 @@ def mask(
 
     with rasterio.drivers(CPL_DEBUG=verbosity > 2):
         try:
-            geojson = json.loads(click.open_file(geojson_mask).read())
+            with click.open_file(geojson_mask) as f:
+                geojson = json.loads(f.read())
         except ValueError:
-            raise click.BadParameter('GeoJSON could not be read from  '
+            raise click.BadParameter('GeoJSON could not be read from '
                                      '--geojson-mask or stdin',
                                      param_hint='--geojson-mask')
 
@@ -150,6 +152,7 @@ def mask(
                 invert=invert)
 
             meta = src.meta.copy()
+            meta.update(**creation_options)
             meta.update({
                 'driver': driver,
                 'height': mask.shape[0],
@@ -165,9 +168,9 @@ def mask(
 
 
 # Shapes command.
- at cli.command(short_help="Write shapes extracted from bands or masks.")
+ at click.command(short_help="Write shapes extracted from bands or masks.")
 @click.argument('input', type=click.Path(exists=True))
- at output_opt
+ at options.output_opt
 @precision_opt
 @indent_opt
 @compact_opt
@@ -256,10 +259,14 @@ def shapes(
                 msk = None
 
                 # Adjust transforms.
-                if sampling == 1:
-                    transform = src.affine
-                else:
-                    transform = src.affine * Affine.scale(float(sampling))
+                transform = src.affine
+                if sampling > 1:
+                    # Decimation of the raster produces a georeferencing
+                    # shift that we correct with a translation.
+                    transform *= Affine.translation(
+                                    src.width%sampling, src.height%sampling)
+                    # And follow by scaling.
+                    transform *= Affine.scale(float(sampling))
 
                 # Most of the time, we'll use the valid data mask.
                 # We skip reading it if we're extracting every possible
@@ -354,15 +361,14 @@ def shapes(
 
 
 # Rasterize command.
- at cli.command(short_help='Rasterize features.')
+ at click.command(short_help='Rasterize features.')
 @files_inout_arg
- at output_opt
+ at options.output_opt
 @format_opt
- at like_file_opt
- at bounds_opt
- at click.option('--dimensions', nargs=2, type=int, default=None,
-              help='Output dataset width, height in number of pixels.')
- at resolution_opt
+ at options.like_file_opt
+ at options.bounds_opt
+ at options.dimensions_opt
+ at options.resolution_opt
 @click.option('--src-crs', '--src_crs', 'src_crs', default=None,
               help='Source coordinate reference system.  Limited to EPSG '
               'codes for now.  Used as output coordinate system if output '
@@ -377,6 +383,7 @@ def shapes(
 @click.option('--property', type=str, default=None, help='Property in '
               'GeoJSON features to use for rasterized values.  Any features '
               'that lack this property will be given --default_value instead.')
+ at options.creation_options
 @click.pass_context
 def rasterize(
         ctx,
@@ -391,7 +398,8 @@ def rasterize(
         all_touched,
         default_value,
         fill,
-        property):
+        property,
+        creation_options):
 
     """Rasterize GeoJSON into a new or existing raster.
 
@@ -440,7 +448,6 @@ def rasterize(
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
 
     output, files = resolve_inout(files=files, output=output)
-    input = click.open_file(files.pop(0) if files else '-')
 
     has_src_crs = src_crs is not None
     src_crs = src_crs or 'EPSG:4326'
@@ -459,7 +466,8 @@ def rasterize(
                 return feature['properties'].get(property, default_value)
             return default_value
 
-        geojson = json.loads(input.read())
+        with click.open_file(files.pop(0) if files else '-') as gj_f:
+            geojson = json.loads(gj_f.read())
         if 'features' in geojson:
             geometries = []
             for f in geojson['features']:
@@ -546,6 +554,7 @@ def rasterize(
                         raise click.BadParameter(
                             'pixel dimensions are required',
                             ctx, param=res, param_hint='--res')
+
                     elif len(res) == 1:
                         res = (res[0], res[0])
 
@@ -569,6 +578,7 @@ def rasterize(
                                         bounds[3]),
                     'driver': driver
                 }
+                kwargs.update(**creation_options)
 
             result = rasterize(
                 geometries,
@@ -588,6 +598,113 @@ def rasterize(
                 out.write_band(1, result)
 
 
+# Bounds command.
+ at click.command(short_help="Write bounding boxes to stdout as GeoJSON.")
+# One or more files, the bounds of each are a feature in the collection
+# object or feature sequence.
+ at click.argument('INPUT', nargs=-1, type=click.Path(exists=True))
+ at precision_opt
+ at indent_opt
+ at compact_opt
+ at projection_geographic_opt
+ at projection_projected_opt
+ at projection_mercator_opt
+ at click.option(
+    '--dst-crs', default='', metavar="EPSG:NNNN", callback=to_lower,
+    help="Output in specified coordinates.")
+ at sequence_opt
+ at use_rs_opt
+ at geojson_type_collection_opt(True)
+ at geojson_type_feature_opt(False)
+ at geojson_type_bbox_opt(False)
+ at click.pass_context
+def bounds(ctx, input, precision, indent, compact, projection, dst_crs,
+           sequence, use_rs, geojson_type):
+    """Write bounding boxes to stdout as GeoJSON for use with, e.g.,
+    geojsonio
+
+      $ rio bounds *.tif | geojsonio
+    
+    If a destination crs is passed via dst_crs, it takes precedence over
+    the projection parameter.
+    """
+    import rasterio.warp
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    dump_kwds = {'sort_keys': True}
+    if indent:
+        dump_kwds['indent'] = indent
+    if compact:
+        dump_kwds['separators'] = (',', ':')
+    stdout = click.get_text_stream('stdout')
+
+    # This is the generator for (feature, bbox) pairs.
+    class Collection(object):
+
+        def __init__(self):
+            self._xs = []
+            self._ys = []
+
+        @property
+        def bbox(self):
+            return min(self._xs), min(self._ys), max(self._xs), max(self._ys)
+
+        def __call__(self):
+            for i, path in enumerate(input):
+                with rasterio.open(path) as src:
+                    bounds = src.bounds
+                    xs = [bounds[0], bounds[2]]
+                    ys = [bounds[1], bounds[3]]
+                    if dst_crs:
+                        xs, ys = rasterio.warp.transform(
+                            src.crs, {'init': dst_crs}, xs, ys)
+                    elif projection == 'mercator':
+                        xs, ys = rasterio.warp.transform(
+                            src.crs, {'init': 'epsg:3857'}, xs, ys)
+                    elif projection == 'geographic':
+                        xs, ys = rasterio.warp.transform(
+                            src.crs, {'init': 'epsg:4326'}, xs, ys)
+
+                if precision >= 0:
+                    xs = [round(v, precision) for v in xs]
+                    ys = [round(v, precision) for v in ys]
+                bbox = [min(xs), min(ys), max(xs), max(ys)]
+
+                yield {
+                    'type': 'Feature',
+                    'bbox': bbox,
+                    'geometry': {
+                        'type': 'Polygon',
+                        'coordinates': [[
+                            [xs[0], ys[0]],
+                            [xs[1], ys[0]],
+                            [xs[1], ys[1]],
+                            [xs[0], ys[1]],
+                            [xs[0], ys[0]] ]]},
+                    'properties': {
+                        'id': str(i),
+                        'title': path,
+                        'filename': os.path.basename(path)} }
+
+                self._xs.extend(bbox[::2])
+                self._ys.extend(bbox[1::2])
+
+    col = Collection()
+    # Use the generator defined above as input to the generic output
+    # writing function.
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            write_features(
+                stdout, col, sequence=sequence,
+                geojson_type=geojson_type, use_rs=use_rs,
+                **dump_kwds)
+
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
+
+
 def _disjoint_bounds(bounds1, bounds2):
     return (bounds1[0] > bounds2[2] or bounds1[2] < bounds2[0] or
             bounds1[1] > bounds2[3] or bounds1[3] < bounds2[1])
+
diff --git a/rasterio/rio/helpers.py b/rasterio/rio/helpers.py
new file mode 100644
index 0000000..1300b4e
--- /dev/null
+++ b/rasterio/rio/helpers.py
@@ -0,0 +1,79 @@
+"""
+Helper objects used by multiple CLI commands.
+"""
+
+
+import json
+
+
+def coords(obj):
+    """Yield all coordinate coordinate tuples from a geometry or feature.
+    From python-geojson package."""
+    if isinstance(obj, (tuple, list)):
+        coordinates = obj
+    elif 'geometry' in obj:
+        coordinates = obj['geometry']['coordinates']
+    else:
+        coordinates = obj.get('coordinates', obj)
+    for e in coordinates:
+        if isinstance(e, (float, int)):
+            yield tuple(coordinates)
+            break
+        else:
+            for f in coords(e):
+                yield f
+
+
+def write_features(
+        fobj, collection, sequence=False, geojson_type='feature', use_rs=False,
+        **dump_kwds):
+    """Read an iterator of (feat, bbox) pairs and write to file using
+    the selected modes."""
+    # Sequence of features expressed as bbox, feature, or collection.
+    if sequence:
+        for feat in collection():
+            xs, ys = zip(*coords(feat))
+            bbox = (min(xs), min(ys), max(xs), max(ys))
+            if use_rs:
+                fobj.write(u'\u001e')
+            if geojson_type == 'feature':
+                fobj.write(json.dumps(feat, **dump_kwds))
+            elif geojson_type == 'bbox':
+                fobj.write(json.dumps(bbox, **dump_kwds))
+            else:
+                fobj.write(
+                    json.dumps({
+                        'type': 'FeatureCollection',
+                        'bbox': bbox,
+                        'features': [feat]}, **dump_kwds))
+            fobj.write('\n')
+    # Aggregate all features into a single object expressed as
+    # bbox or collection.
+    else:
+        features = list(collection())
+        if geojson_type == 'bbox':
+            fobj.write(json.dumps(collection.bbox, **dump_kwds))
+        elif geojson_type == 'feature':
+            fobj.write(json.dumps(features[0], **dump_kwds))
+        else:
+            fobj.write(json.dumps({
+                'bbox': collection.bbox,
+                'type': 'FeatureCollection',
+                'features': features},
+                **dump_kwds))
+        fobj.write('\n')
+
+
+def resolve_inout(input=None, output=None, files=None):
+    """Resolves inputs and outputs from standard args and options.
+
+    Returns `output_filename, [input_filename0, ...]`."""
+    resolved_output = output or (files[-1] if files else None)
+    resolved_inputs = (
+        [input] if input else [] +
+        list(files[:-1 if not output else None]) if files else [])
+    return resolved_output, resolved_inputs
+
+
+def to_lower(ctx, param, value):
+    return value.lower()
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
index b627fdc..f6e1890 100644
--- a/rasterio/rio/info.py
+++ b/rasterio/rio/info.py
@@ -1,40 +1,166 @@
 """Fetch and edit raster dataset metadata from the command line."""
 
+
 import json
 import logging
+import os
 import sys
 
 import click
+from cligj import precision_opt
 
+from . import options
 import rasterio
 import rasterio.crs
-from rasterio.rio.cli import cli, bidx_opt, file_in_arg, masked_opt
 from rasterio.transform import guard_transform
 
 
- at cli.command('edit-info', short_help="Edit dataset metadata.")
- at file_in_arg
- at click.option('--nodata', type=float, default=None,
+# Handlers for info module options.
+
+def from_like_context(ctx, param, value):
+    """Return the value for an option from the context if the option 
+    or `--all` is given, else return None."""
+    if ctx.obj and ctx.obj.get('like') and (
+            value == 'like' or ctx.obj.get('all_like')):
+        return ctx.obj['like'][param.name]
+    else:
+        return None
+
+
+def all_handler(ctx, param, value):
+    """Get tags from a template file or command line."""
+    if ctx.obj and ctx.obj.get('like') and value is not None:
+        ctx.obj['all_like'] = value
+        value = ctx.obj.get('like')
+    return value
+
+
+def crs_handler(ctx, param, value):
+    """Get crs value from a template file or command line."""
+    retval = from_like_context(ctx, param, value)
+    if retval is None and value:
+        try:
+            retval = json.loads(value)
+        except ValueError:
+            retval = value
+        if not rasterio.crs.is_valid_crs(retval):
+            raise click.BadParameter(
+                "'%s' is not a recognized CRS." % retval,
+                param=param, param_hint='crs')
+    return retval
+
+
+def like_handler(ctx, param, value):
+    """Copy a dataset's meta property to the command context for access
+    from other callbacks."""
+    if ctx.obj is None:
+        ctx.obj = {}
+    if value:
+        with rasterio.open(value) as src:
+            metadata = src.meta
+            ctx.obj['like'] = metadata
+            ctx.obj['like']['transform'] = metadata['affine']
+            ctx.obj['like']['tags'] = src.tags()
+
+
+def nodata_handler(ctx, param, value):
+    """Get nodata value from a template file or command line."""
+    retval = from_like_context(ctx, param, value)
+    if retval is None and value is not None:
+        try:
+            retval = float(value)
+        except:
+            raise click.BadParameter(
+                "%s is not a number." % repr(value),
+                param=param, param_hint='nodata')
+    return retval
+
+
+def tags_handler(ctx, param, value):
+    """Get tags from a template file or command line."""
+    retval = from_like_context(ctx, param, value)
+    if retval is None and value:
+        try:
+            retval = dict(p.split('=') for p in value)
+        except:
+            raise click.BadParameter(
+                "'%s' contains a malformed tag." % value,
+                param=param, param_hint='transform')
+    return retval
+
+
+def transform_handler(ctx, param, value):
+    """Get transform value from a template file or command line."""
+    retval = from_like_context(ctx, param, value)
+    if retval is None and value:
+        try:
+            value = json.loads(value)
+        except ValueError:
+            pass
+        try:
+            retval = guard_transform(value)
+        except:
+            raise click.BadParameter(
+                "'%s' is not recognized as an Affine or GDAL "
+                "geotransform array." % value,
+                param=param, param_hint='transform')
+    return retval
+
+
+# The edit-info command.
+
+ at click.command('edit-info', short_help="Edit dataset metadata.")
+ at options.file_in_arg
+ at click.option('--nodata', callback=nodata_handler, default=None,
               help="New nodata value")
- at click.option('--crs', help="New coordinate reference system")
- at click.option('--transform', help="New affine transform matrix")
- at click.option('--tag', 'tags', multiple=True, metavar='KEY=VAL',
-              help="New tag.")
+ at click.option('--crs', callback=crs_handler, default=None,
+              help="New coordinate reference system")
+ at click.option('--transform', callback=transform_handler,
+              help="New affine transform matrix")
+ at click.option('--tag', 'tags', callback=tags_handler, multiple=True,
+              metavar='KEY=VAL', help="New tag.")
+ at click.option('--all', 'allmd', callback=all_handler, flag_value='like',
+              is_eager=True, default=False,
+              help="Copy all metadata items from the template file.")
+ at click.option(
+    '--like',
+    type=click.Path(exists=True),
+    callback=like_handler,
+    is_eager=True,
+    help="Raster dataset to use as a template for obtaining affine "
+         "transform (bounds and resolution), crs, and nodata values.")
 @click.pass_context
-def edit(ctx, input, nodata, crs, transform, tags):
+def edit(ctx, input, nodata, crs, transform, tags, allmd, like):
     """Edit a dataset's metadata: coordinate reference system, affine
     transformation matrix, nodata value, and tags.
 
-    CRS may be either a PROJ.4 or EPSG:nnnn string, or a JSON-encoded
-    PROJ.4 object.
+    The coordinate reference system may be either a PROJ.4 or EPSG:nnnn
+    string,
+    
+      --crs 'EPSG:4326'
+    
+    or a JSON text-encoded PROJ.4 object.
+
+      --crs '{"proj": "utm", "zone": 18, ...}'
+
+    Transforms are either JSON-encoded Affine objects (preferred),
+
+      --transform '[300.038, 0.0, 101985.0, 0.0, -300.042, 2826915.0]'
+
+    or JSON text-encoded GDAL geotransform arrays.
+
+      --transform '[101985.0, 300.038, 0.0, 2826915.0, 0.0, -300.042]'
+
+    Metadata items may also be read from an existing dataset using a
+    combination of the --like option with at least one of --all,
+    `--crs like`, `--nodata like`, and `--transform like`.
 
-    Transforms are either JSON-encoded Affine objects (preferred) like
+      rio edit-info example.tif --like template.tif --all
 
-      [300.038, 0.0, 101985.0, 0.0, -300.042, 2826915.0]
+    To get just the transform from the template:
 
-    or JSON-encoded GDAL geotransform arrays like
+      rio edit-info example.tif --like template.tif --transform like
 
-      [101985.0, 300.038, 0.0, 2826915.0, 0.0, -300.042]
     """
     import numpy as np
 
@@ -48,64 +174,35 @@ def edit(ctx, input, nodata, crs, transform, tags):
         return rng.min <= value <= rng.max
 
     with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
+
         with rasterio.open(input, 'r+') as dst:
 
-            # Update nodata.
-            if nodata is not None:
+            if allmd:
+                nodata = allmd['nodata']
+                crs = allmd['crs']
+                transform = allmd['transform']
+                tags = allmd['tags']
 
+            if nodata is not None:
                 dtype = dst.dtypes[0]
                 if not in_dtype_range(nodata, dtype):
                     raise click.BadParameter(
                         "outside the range of the file's "
                         "data type (%s)." % dtype,
                         param=nodata, param_hint='nodata')
-
                 dst.nodata = nodata
 
-            # Update CRS. Value might be a PROJ.4 string or a JSON
-            # encoded dict.
             if crs:
-                new_crs = crs.strip()
-                try:
-                    new_crs = json.loads(crs)
-                except ValueError:
-                    pass
-
-                if not (rasterio.crs.is_geographic_crs(new_crs) or 
-                        rasterio.crs.is_projected_crs(new_crs)):
-                    raise click.BadParameter(
-                        "'%s' is not a recognized CRS." % crs,
-                        param=crs, param_hint='crs')
-
-                dst.crs = new_crs
+                dst.crs = crs
 
-            # Update transform. Value might be a JSON encoded
-            # Affine object or a GDAL geotransform array.
             if transform:
-                try:
-                    transform_obj = json.loads(transform)
-                except ValueError:
-                    raise click.BadParameter(
-                        "'%s' is not a JSON array." % transform,
-                        param=transform, param_hint='transform')
+                dst.transform = transform
 
-                try:
-                    transform_obj = guard_transform(transform_obj)
-                except:
-                    raise click.BadParameter(
-                        "'%s' is not recognized as an Affine or GDAL "
-                        "geotransform array." % transform,
-                        param=transform, param_hint='transform')
-
-                dst.transform = transform_obj
-
-            # Update tags.
             if tags:
-                tags = dict(p.split('=') for p in tags)
                 dst.update_tags(**tags)
 
 
- at cli.command(short_help="Print information about the rio environment.")
+ at click.command(short_help="Print information about the rio environment.")
 @click.option('--formats', 'key', flag_value='formats', default=True,
               help="Enumerate the available formats.")
 @click.pass_context
@@ -123,8 +220,8 @@ def env(ctx, key):
             stdout.write('\n')
 
 
- at cli.command(short_help="Print information about a data file.")
- at file_in_arg
+ at click.command(short_help="Print information about a data file.")
+ at options.file_in_arg
 @click.option('--meta', 'aspect', flag_value='meta', default=True,
               help="Show data file structure (default).")
 @click.option('--tags', 'aspect', flag_value='tags',
@@ -162,11 +259,11 @@ def env(ctx, key):
                    "(use --bidx).")
 @click.option('-v', '--tell-me-more', '--verbose', is_flag=True,
               help="Output extra information.")
- at bidx_opt
- at masked_opt
+ at options.bidx_opt
+ at options.masked_opt
 @click.pass_context
 def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
-        masked):
+         masked):
     """Print metadata about the dataset as JSON.
 
     Optionally print a single metadata item as a string.
@@ -210,8 +307,94 @@ def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
                     else:
                         click.echo(json.dumps(info, indent=indent))
                 elif aspect == 'tags':
-                    click.echo(json.dumps(src.tags(ns=namespace),
-                                            indent=indent))
+                    click.echo(
+                        json.dumps(src.tags(ns=namespace), indent=indent))
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
+
+
+# Insp command.
+ at click.command(short_help="Open a data file and start an interpreter.")
+ at options.file_in_arg
+ at click.option('--ipython', 'interpreter', flag_value='ipython',
+              help="Use IPython as interpreter.")
+ at click.option(
+    '-m',
+    '--mode',
+    type=click.Choice(['r', 'r+']),
+    default='r',
+    help="File mode (default 'r').")
+ at click.pass_context
+def insp(ctx, input, mode, interpreter):
+    """ Open the input file in a Python interpreter.
+
+    IPython will be used as the default interpreter, if available.
+    """
+    import rasterio.tool
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+            with rasterio.open(input, mode) as src:
+                rasterio.tool.main(
+                    'Rasterio %s Interactive Inspector (Python %s)\n'
+                    'Type "src.meta", "src.read(1)", or "help(src)" '
+                    'for more information.' % (
+                        rasterio.__version__,
+                        '.'.join(map(str, sys.version_info[:3]))),
+                    src, interpreter)
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
+
+
+# Transform command.
+ at click.command(short_help="Transform coordinates.")
+ at click.argument('INPUT', default='-', required=False)
+ at click.option('--src-crs', '--src_crs', default='EPSG:4326',
+              help="Source CRS.")
+ at click.option('--dst-crs', '--dst_crs', default='EPSG:4326',
+              help="Destination CRS.")
+ at precision_opt
+ at click.pass_context
+def transform(ctx, input, src_crs, dst_crs, precision):
+    import rasterio.warp
+
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+
+    # Handle the case of file, stream, or string input.
+    try:
+        src = click.open_file(input).readlines()
+    except IOError:
+        src = [input]
+
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+            if src_crs.startswith('EPSG'):
+                src_crs = {'init': src_crs}
+            elif os.path.exists(src_crs):
+                with rasterio.open(src_crs) as f:
+                    src_crs = f.crs
+            if dst_crs.startswith('EPSG'):
+                dst_crs = {'init': dst_crs}
+            elif os.path.exists(dst_crs):
+                with rasterio.open(dst_crs) as f:
+                    dst_crs = f.crs
+            for line in src:
+                coords = json.loads(line)
+                xs = coords[::2]
+                ys = coords[1::2]
+                xs, ys = rasterio.warp.transform(src_crs, dst_crs, xs, ys)
+                if precision >= 0:
+                    xs = [round(v, precision) for v in xs]
+                    ys = [round(v, precision) for v in ys]
+                result = [0]*len(coords)
+                result[::2] = xs
+                result[1::2] = ys
+                print(json.dumps(result))
+
     except Exception:
         logger.exception("Exception caught during processing")
         raise click.Abort()
diff --git a/rasterio/rio/main.py b/rasterio/rio/main.py
index 910f893..2732654 100644
--- a/rasterio/rio/main.py
+++ b/rasterio/rio/main.py
@@ -1,32 +1,38 @@
-# main: loader of all the command entry points.
+"""
+Main click group for CLI
+"""
 
-import sys
-import traceback
 
+import logging
 from pkg_resources import iter_entry_points
+import sys
+
+import click
+import cligj
+import cligj.plugins
+
+import rasterio
+
+
+def configure_logging(verbosity):
+    log_level = max(10, 30 - 10*verbosity)
+    logging.basicConfig(stream=sys.stderr, level=log_level)
+
+
+ at cligj.plugins.group(plugins=(
+        ep for ep in list(iter_entry_points('rasterio.rio_commands')) +
+                     list(iter_entry_points('rasterio.rio_plugins'))))
+ at cligj.verbose_opt
+ at cligj.quiet_opt
+ at click.version_option(version=rasterio.__version__, message='%(version)s')
+ at click.pass_context
+def main_group(ctx, verbose, quiet):
+
+    """
+    Rasterio command line interface.
+    """
 
-from rasterio.rio.cli import BrokenCommand, cli
-
-
-# Find and load all entry points in the rasterio.rio_commands group.
-# This includes the standard commands included with Rasterio as well
-# as commands provided by other packages.
-#
-# At a mimimum, commands must use the rasterio.rio.cli.cli command
-# group decorator like so:
-#
-#   from rasterio.rio.cli import cli
-#
-#   @cli.command()
-#   def foo(...):
-#       ...
-
-for entry_point in iter_entry_points('rasterio.rio_commands'):
-    try:
-        entry_point.load()
-    except Exception:
-        # Catch this so a busted plugin doesn't take down the CLI.
-        # Handled by registering a dummy command that does nothing
-        # other than explain the error.
-        cli.add_command(
-            BrokenCommand(entry_point.name))
+    verbosity = verbose - quiet
+    configure_logging(verbosity)
+    ctx.obj = {}
+    ctx.obj['verbosity'] = verbosity
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
index a4c1afe..0d15e97 100644
--- a/rasterio/rio/merge.py
+++ b/rasterio/rio/merge.py
@@ -1,30 +1,31 @@
 # Merge command.
 
+
 import logging
 import math
 import os.path
-import sys
 import warnings
 
 import click
 from cligj import files_inout_arg, format_opt
 
+from .helpers import resolve_inout
+from . import options
 import rasterio
-from rasterio.rio.cli import cli, bounds_opt, output_opt, resolve_inout
 from rasterio.transform import Affine
 
 
- at cli.command(short_help="Merge a stack of raster datasets.")
+ at click.command(short_help="Merge a stack of raster datasets.")
 @files_inout_arg
- at output_opt
+ at options.output_opt
 @format_opt
- at bounds_opt
- at click.option('-r', '--res', nargs=2, type=float, default=None,
-              help="Output dataset resolution: pixel width, pixel height")
+ at options.bounds_opt
+ at options.resolution_opt
 @click.option('--nodata', type=float, default=None,
               help="Override nodata values defined in input datasets")
+ at options.creation_options
 @click.pass_context
-def merge(ctx, files, output, driver, bounds, res, nodata):
+def merge(ctx, files, output, driver, bounds, res, nodata, creation_options):
     """Copy valid pixels from input files to an output file.
 
     All files must have the same number of bands, data type, and
@@ -37,12 +38,19 @@ def merge(ctx, files, output, driver, bounds, res, nodata):
     Geospatial bounds and resolution of a new output file in the
     units of the input file coordinate reference system may be provided
     and are otherwise taken from the first input file.
+
+    Note: --res changed from 2 parameters in 0.25.
+      --res 0.1 0.1  => --res 0.1 (square)
+      --res 0.1 0.2  => --res 0.1 --res 0.2  (rectangular)
     """
     import numpy as np
 
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
     logger = logging.getLogger('rio')
 
+    if len(res) == 1:
+        res = (res[0], res[0])
+
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
             output, files = resolve_inout(files=files, output=output)
@@ -50,6 +58,7 @@ def merge(ctx, files, output, driver, bounds, res, nodata):
             with rasterio.open(files[0]) as first:
                 first_res = first.res
                 kwargs = first.meta
+                kwargs.update(**creation_options)
                 kwargs.pop('affine')
                 nodataval = first.nodatavals[0]
                 dtype = first.dtypes[0]
@@ -86,7 +95,7 @@ def merge(ctx, files, output, driver, bounds, res, nodata):
                 output_width = int(math.ceil((bounds[2]-bounds[0])/res[0]))
                 output_height = int(math.ceil((bounds[3]-bounds[1])/res[1]))
 
-                kwargs['driver'] == driver
+                kwargs['driver'] = driver
                 kwargs['transform'] = output_transform
                 kwargs['width'] = output_width
                 kwargs['height'] = output_height
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
new file mode 100644
index 0000000..1bd6521
--- /dev/null
+++ b/rasterio/rio/options.py
@@ -0,0 +1,156 @@
+"""
+Registry of common rio CLI options.  See cligj for more options.
+
+-a, --all: Use all pixels touched by features.  In rio-mask, rio-rasterize
+--as-mask/--not-as-mask: interpret band as mask or not.  In rio-shapes
+--band/--mask: use band or mask.  In rio-shapes
+--bbox:
+-b, --bidx: band index(es) (singular or multiple value versions).
+    In rio-info, rio-sample, rio-shapes, rio-stack (different usages)
+--bounds: bounds in world coordinates.
+    In rio-info, rio-rasterize (different usages)
+--count: count of bands.  In rio-info
+--crop: Crop raster to extent of features.  In rio-mask
+--crs: CRS of input raster.  In rio-info
+--default-value: default for rasterized pixels.  In rio-rasterize
+--dimensions: Output width, height.  In rio-rasterize
+--dst-crs: destination CRS.  In rio-transform
+--fill: fill value for pixels not covered by features.  In rio-rasterize
+--formats: list available formats.  In rio-info
+--height: height of raster.  In rio-info
+-i, --invert: Invert mask created from features: In rio-mask
+-j, --geojson-mask: GeoJSON for masking raster.  In rio-mask
+--lnglat: geograhpic coordinates of center of raster.  In rio-info
+--masked/--not-masked: read masked data from source file.
+    In rio-calc, rio-info
+-m, --mode: output file mode (r, r+).  In rio-insp
+--name: input file name alias.  In rio-calc
+--nodata: nodata value.  In rio-info, rio-merge (different usages)
+--photometric: photometric interpretation.  In rio-stack
+--property: GeoJSON property to use as values for rasterize.  In rio-rasterize
+-r, --res: output resolution.
+    In rio-info, rio-rasterize (different usages.  TODO: try to combine
+    usages, prefer rio-rasterize version)
+--sampling: Inverse of sampling fraction.  In rio-shapes
+--shape: shape (width, height) of band.  In rio-info
+--src-crs: source CRS.
+    In rio-insp, rio-rasterize (different usages.  TODO: consolidate usages)
+--stats: print raster stats.  In rio-inf
+-t, --dtype: data type.  In rio-calc, rio-info (different usages)
+--width: width of raster.  In rio-info
+--with-nodata/--without-nodata: include nodata regions or not.  In rio-shapes.
+-v, --tell-me-more, --verbose
+"""
+
+
+# TODO: move file_in_arg and file_out_arg to cligj
+
+
+import click
+
+
+def _cb_key_val(ctx, param, value):
+
+    """
+    click callback to validate `--opt KEY1=VAL1 --opt KEY2=VAL2` and collect
+    in a dictionary like the one below, which is what the CLI function receives.
+    If no value or `None` is received then an empty dictionary is returned.
+
+        {
+            'KEY1': 'VAL1',
+            'KEY2': 'VAL2'
+        }
+
+    Note: `==VAL` breaks this as `str.split('=', 1)` is used.
+    """
+
+    if not value:
+        return {}
+    else:
+        out = {}
+        for pair in value:
+            if '=' not in pair:
+                raise click.BadParameter("Invalid syntax for KEY=VAL arg: {}".format(pair))
+            else:
+                k, v = pair.split('=', 1)
+                out[k] = v
+
+        return out
+
+
+# Singular input file
+file_in_arg = click.argument(
+    'INPUT',
+    type=click.Path(exists=True, resolve_path=True))
+
+# Singular output file
+file_out_arg = click.argument(
+    'OUTPUT',
+    type=click.Path(resolve_path=True))
+
+bidx_opt = click.option(
+    '-b', '--bidx',
+    type=int,
+    default=1,
+    help="Input file band index (default: 1)")
+
+bidx_mult_opt = click.option(
+    '-b', '--bidx',
+    multiple=True,
+    help="Indexes of input file bands.")
+
+# TODO: may be better suited to cligj
+bounds_opt = click.option(
+    '--bounds',
+    nargs=4, type=float, default=None,
+    help='Output bounds: left bottom right top.')
+
+dimensions_opt = click.option(
+    '--dimensions',
+    nargs=2, type=int, default=None,
+    help='Output dataset width, height in number of pixels.')
+
+dtype_opt = click.option(
+    '-t', '--dtype',
+    type=click.Choice([
+        'ubyte', 'uint8', 'uint16', 'int16', 'uint32', 'int32',
+        'float32', 'float64']),
+    default=None,
+    help="Output data type.")
+
+like_file_opt = click.option(
+    '--like',
+    type=click.Path(exists=True),
+    help='Raster dataset to use as a template for obtaining affine '
+         'transform (bounds and resolution), crs, data type, and driver '
+         'used to create the output.')
+
+masked_opt = click.option(
+    '--masked/--not-masked',
+    default=True,
+    help="Evaluate expressions using masked arrays (the default) or ordinary "
+         "numpy arrays.")
+
+output_opt = click.option(
+    '-o', '--output',
+    default=None,
+    type=click.Path(resolve_path=True),
+    help="Path to output file (optional alternative to a positional arg "
+         "for some commands).")
+
+resolution_opt = click.option(
+    '-r', '--res',
+    multiple=True, type=float, default=None,
+    help='Output dataset resolution in units of coordinate '
+         'reference system. Pixels assumed to be square if this option '
+         'is used once, otherwise use: '
+         '--res pixel_width --res pixel_height')
+
+creation_options = click.option(
+    '--co', 'creation_options',
+    metavar='NAME=VALUE',
+    multiple=True,
+    callback=_cb_key_val,
+    help="Driver specific creation options."
+         "See the documentation for the selected output driver for "
+         "more information.")
\ No newline at end of file
diff --git a/rasterio/rio/overview.py b/rasterio/rio/overview.py
new file mode 100644
index 0000000..1c7892c
--- /dev/null
+++ b/rasterio/rio/overview.py
@@ -0,0 +1,101 @@
+# coding: utf-8
+"""Manage overviews of a dataset."""
+
+from functools import reduce
+import logging
+import operator
+
+import click
+
+import rasterio
+from rasterio.enums import Resampling
+
+from . import options
+
+
+def build_handler(ctx, param, value):
+    if value:
+        try:
+            if '^' in value:
+                base, exp_range = value.split('^')
+                exp_min, exp_max = (int(v) for v in exp_range.split('..'))
+                value = [pow(int(base), k) for k in range(exp_min, exp_max+1)]
+            else:
+                value = [int(v) for v in value.split(',')]
+        except Exception as exc:
+            raise click.BadParameter(u"must match 'n,n,n,…' or 'n^n..n'.")
+    return value
+
+
+ at click.command('overview', short_help="Construct overviews in an existing dataset.")
+ at options.file_in_arg
+ at click.option('--build', callback=build_handler, metavar=u"f1,f2,…|b^min..max",
+              help="A sequence of decimation factors specied as "
+                   "comma-separated list of numbers or a base and range of "
+                   "exponents.")
+ at click.option('--ls', help="Print the overviews for each band.",
+              is_flag=True, default=False)
+ at click.option('--rebuild', help="Reconstruct existing overviews.",
+              is_flag=True, default=False)
+ at click.option('--resampling', help="Resampling algorithm.",
+              type=click.Choice([item.name for item in Resampling]),
+              default='nearest', show_default=True)
+ at click.pass_context
+def overview(ctx, input, build, ls, rebuild, resampling):
+    """Construct overviews in an existing dataset.
+
+    A pyramid of overviews computed once and stored in the dataset can
+    improve performance in some applications.
+
+    The decimation levels at which to build overviews can be specified as
+    a comma separated list
+
+      rio pyramid --build 2,4,8,16
+
+    or a base and range of exponents.
+
+      rio pyramid --build 2^1..4
+
+    Note that overviews can not currently be removed and are not 
+    automatically updated when the dataset's primary bands are
+    modified.
+
+    Information about existing overviews can be printed using the --ls
+    option.
+
+      rio pyramid --ls
+
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+
+    with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
+        with rasterio.open(input, 'r+') as dst:
+
+            if ls:
+                resampling_method = dst.tags(
+                    ns='rio_overview').get('resampling') or 'unknown'
+
+                click.echo("Overview factors:")
+                for idx in dst.indexes:
+                    click.echo("  Band %d: %s (method: '%s')" % (
+                        idx, dst.overviews(idx) or 'None', resampling_method))
+
+            elif rebuild:
+                # Build the same overviews for all bands.
+                factors = reduce(
+                    operator.or_,
+                    [set(dst.overviews(i)) for i in dst.indexes])
+
+                # Attempt to recover the resampling method from dataset tags.
+                resampling_method = dst.tags(
+                    ns='rio_overview').get('resampling') or resampling
+
+                dst.build_overviews(
+                    list(factors), Resampling[resampling_method])
+
+            elif build:
+                dst.build_overviews(build, Resampling[resampling])
+
+                # Save the resampling method to a tag.
+                dst.update_tags(ns='rio_overview', resampling=resampling)
diff --git a/rasterio/rio/rio.py b/rasterio/rio/rio.py
deleted file mode 100644
index b8cd2b9..0000000
--- a/rasterio/rio/rio.py
+++ /dev/null
@@ -1,203 +0,0 @@
-"""Rasterio command line interface"""
-
-import functools
-import json
-import logging
-import os.path
-import pprint
-import sys
-import warnings
-
-import click
-from cligj import (
-    precision_opt, indent_opt, compact_opt, projection_geographic_opt,
-    projection_projected_opt, projection_mercator_opt,
-    sequence_opt, use_rs_opt, geojson_type_collection_opt,
-    geojson_type_feature_opt, geojson_type_bbox_opt)
-
-import rasterio
-from rasterio.rio.cli import cli, write_features, file_in_arg
-
-
-warnings.simplefilter('default')
-
-
-# Commands are below.
-#
-# Command bodies less than ~20 lines, e.g. info() below, can go in this
-# module. Longer ones, e.g. insp() shall call functions imported from
-# rasterio.tool.
-
-# Insp command.
- at cli.command(short_help="Open a data file and start an interpreter.")
- at file_in_arg
- at click.option(
-    '-m',
-    '--mode',
-    type=click.Choice(['r', 'r+']),
-    default='r',
-    help="File mode (default 'r').")
- at click.pass_context
-def insp(ctx, input, mode):
-    import rasterio.tool
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-    try:
-        with rasterio.drivers(CPL_DEBUG=verbosity>2):
-            with rasterio.open(input, mode) as src:
-                rasterio.tool.main(
-                    "Rasterio %s Interactive Inspector (Python %s)\n"
-                    'Type "src.meta", "src.read_band(1)", or "help(src)" '
-                    'for more information.' %  (
-                        rasterio.__version__,
-                        '.'.join(map(str, sys.version_info[:3]))),
-                    src)
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
-
-
-# Bounds command.
- at cli.command(short_help="Write bounding boxes to stdout as GeoJSON.")
-# One or more files, the bounds of each are a feature in the collection
-# object or feature sequence.
- at click.argument('INPUT', nargs=-1, type=click.Path(exists=True))
- at precision_opt
- at indent_opt
- at compact_opt
- at projection_geographic_opt
- at projection_projected_opt
- at projection_mercator_opt
- at sequence_opt
- at use_rs_opt
- at geojson_type_collection_opt(True)
- at geojson_type_feature_opt(False)
- at geojson_type_bbox_opt(False)
- at click.pass_context
-def bounds(ctx, input, precision, indent, compact, projection, sequence,
-        use_rs, geojson_type):
-    """Write bounding boxes to stdout as GeoJSON for use with, e.g.,
-    geojsonio
-
-      $ rio bounds *.tif | geojsonio
-
-    """
-    import rasterio.warp
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-    dump_kwds = {'sort_keys': True}
-    if indent:
-        dump_kwds['indent'] = indent
-    if compact:
-        dump_kwds['separators'] = (',', ':')
-    stdout = click.get_text_stream('stdout')
-
-    # This is the generator for (feature, bbox) pairs.
-    class Collection(object):
-
-        def __init__(self):
-            self._xs = []
-            self._ys = []
-
-        @property
-        def bbox(self):
-            return min(self._xs), min(self._ys), max(self._xs), max(self._ys)
-
-        def __call__(self):
-            for i, path in enumerate(input):
-                with rasterio.open(path) as src:
-                    bounds = src.bounds
-                    xs = [bounds[0], bounds[2]]
-                    ys = [bounds[1], bounds[3]]
-                    if projection == 'geographic':
-                        xs, ys = rasterio.warp.transform(
-                            src.crs, {'init': 'epsg:4326'}, xs, ys)
-                    if projection == 'mercator':
-                        xs, ys = rasterio.warp.transform(
-                            src.crs, {'init': 'epsg:3857'}, xs, ys)
-                if precision >= 0:
-                    xs = [round(v, precision) for v in xs]
-                    ys = [round(v, precision) for v in ys]
-                bbox = [min(xs), min(ys), max(xs), max(ys)]
-
-                yield {
-                    'type': 'Feature',
-                    'bbox': bbox,
-                    'geometry': {
-                        'type': 'Polygon',
-                        'coordinates': [[
-                            [xs[0], ys[0]],
-                            [xs[1], ys[0]],
-                            [xs[1], ys[1]],
-                            [xs[0], ys[1]],
-                            [xs[0], ys[0]] ]]},
-                    'properties': {
-                        'id': str(i),
-                        'title': path,
-                        'filename': os.path.basename(path)} }
-
-                self._xs.extend(bbox[::2])
-                self._ys.extend(bbox[1::2])
-
-    col = Collection()
-    # Use the generator defined above as input to the generic output
-    # writing function.
-    try:
-        with rasterio.drivers(CPL_DEBUG=verbosity>2):
-            write_features(
-                stdout, col, sequence=sequence,
-                geojson_type=geojson_type, use_rs=use_rs,
-                **dump_kwds)
-
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
-
-
-# Transform command.
- at cli.command(short_help="Transform coordinates.")
- at click.argument('INPUT', default='-', required=False)
- at click.option('--src-crs', '--src_crs', default='EPSG:4326', help="Source CRS.")
- at click.option('--dst-crs', '--dst_crs', default='EPSG:4326', help="Destination CRS.")
- at precision_opt
- at click.pass_context
-def transform(ctx, input, src_crs, dst_crs, precision):
-    import rasterio.warp
-
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-
-    # Handle the case of file, stream, or string input.
-    try:
-        src = click.open_file(input).readlines()
-    except IOError:
-        src = [input]
-
-    try:
-        with rasterio.drivers(CPL_DEBUG=verbosity>2):
-            if src_crs.startswith('EPSG'):
-                src_crs = {'init': src_crs}
-            elif os.path.exists(src_crs):
-                with rasterio.open(src_crs) as f:
-                    src_crs = f.crs
-            if dst_crs.startswith('EPSG'):
-                dst_crs = {'init': dst_crs}
-            elif os.path.exists(dst_crs):
-                with rasterio.open(dst_crs) as f:
-                    dst_crs = f.crs
-            for line in src:
-                coords = json.loads(line)
-                xs = coords[::2]
-                ys = coords[1::2]
-                xs, ys = rasterio.warp.transform(src_crs, dst_crs, xs, ys)
-                if precision >= 0:
-                    xs = [round(v, precision) for v in xs]
-                    ys = [round(v, precision) for v in ys]
-                result = [0]*len(coords)
-                result[::2] = xs
-                result[1::2] = ys
-                print(json.dumps(result))
-
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
diff --git a/rasterio/rio/sample.py b/rasterio/rio/sample.py
index 96eef67..8054bec 100644
--- a/rasterio/rio/sample.py
+++ b/rasterio/rio/sample.py
@@ -1,18 +1,16 @@
 import json
 import logging
-import sys
 import warnings
 
 import click
 
 import rasterio
-from rasterio.rio.cli import cli
 
 
 warnings.simplefilter('default')
 
 
- at cli.command(short_help="Sample a dataset.")
+ at click.command(short_help="Sample a dataset.")
 @click.argument('files', nargs=-1, required=True, metavar='FILE "[x, y]"')
 @click.option('-b', '--bidx', default=None, help="Indexes of input file bands.")
 @click.pass_context
diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py
new file mode 100644
index 0000000..80a060d
--- /dev/null
+++ b/rasterio/rio/warp.py
@@ -0,0 +1,208 @@
+import logging
+from math import ceil
+import click
+from cligj import format_opt
+
+from . import options
+import rasterio
+from rasterio import crs
+from rasterio.transform import Affine
+from rasterio.warp import (reproject, RESAMPLING, calculate_default_transform,
+   transform_bounds)
+
+
+logger = logging.getLogger('rio')
+
+
+ at click.command(short_help='Warp a raster dataset.')
+ at options.file_in_arg
+ at options.file_out_arg
+ at format_opt
+ at options.like_file_opt
+ at click.option('--dst-crs', default=None,
+              help='Target coordinate reference system.')
+ at options.dimensions_opt
+ at options.bounds_opt
+ at options.resolution_opt
+ at click.option('--resampling', type=click.Choice(['nearest', 'bilinear', 'cubic',
+                'cubic_spline','lanczos', 'average', 'mode']),
+              default='nearest', help='Resampling method (default: nearest).')
+ at click.option('--threads', type=int, default=1,
+              help='Number of processing threads.')
+ at options.creation_options
+ at click.pass_context
+# TODO: add NODATA options and support for existing output rasters
+def warp(
+        ctx,
+        input,
+        output,
+        driver,
+        like,
+        dst_crs,
+        dimensions,
+        bounds,
+        res,
+        resampling,
+        threads,
+        creation_options):
+    """
+    Warp a raster dataset.
+
+    Currently, the output is always overwritten.  This will be changed in a
+    later version.
+
+    If a template raster is provided using the --like option, the coordinate
+    reference system, affine transform, and dimensions of that raster will
+    be used for the output.  In this case --dst-crs, --bounds, --res, and
+    --dimensions options are ignored.
+
+    \b
+        $ rio warp input.tif output.tif --like template.tif
+
+    The output coordinate reference system may be either a PROJ.4 or EPSG:nnnn
+    string,
+
+    \b
+        --dst-crs EPSG:4326
+        --dst-crs '+proj=longlat +ellps=WGS84 +datum=WGS84'
+
+    or a JSON text-encoded PROJ.4 object.
+
+    \b
+        --dst-crs '{"proj": "utm", "zone": 18, ...}'
+
+    If --dimensions are provided, --res and --bounds are ignored.  Resolution
+    is calculated based on the relationship between the raster bounds in the
+    target coordinate system and the dimensions, and may produce rectangular
+    rather than square pixels.
+
+    \b
+        $ rio warp input.tif output.tif --dimensions 100 200 --dst-crs EPSG:4326
+
+    If --bounds are provided, --res is required if --dst-crs is provided
+    (defaults to source raster resolution otherwise).  Bounds are in the source
+    coordinate reference system.
+
+    \b
+        $ rio warp input.tif output.tif --bounds -78 22 -76 24 --dst-crs \\
+          EPSG:4326 --res 0.1
+    """
+
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    resampling = getattr(RESAMPLING, resampling)  # get integer code for method
+
+    if not len(res):
+        # Click sets this as an empty tuple if not provided
+        res = None
+    else:
+        # Expand one value to two if needed
+        res = (res[0], res[0]) if len(res) == 1 else res
+
+    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+        with rasterio.open(input) as src:
+            l, b, r, t = src.bounds
+            out_kwargs = src.meta.copy()
+            out_kwargs['driver'] = driver
+
+            if like:
+                with rasterio.open(like) as template_ds:
+                    dst_crs = template_ds.crs
+                    dst_transform = template_ds.affine
+                    dst_height = template_ds.height
+                    dst_width = template_ds.width
+
+            elif dst_crs:
+                try:
+                    dst_crs = crs.from_string(dst_crs)
+                except ValueError:
+                    raise click.BadParameter('invalid crs format',
+                                             param=dst_crs, param_hint=dst_crs)
+
+                if dimensions:
+                    # Calculate resolution appropriate for dimensions in target
+                    dst_width, dst_height = dimensions
+                    xmin, ymin, xmax, ymax = transform_bounds(src.crs, dst_crs,
+                                                              *src.bounds)
+                    dst_transform = Affine(
+                        (xmax - xmin) / float(dst_width),
+                        0, xmin, 0,
+                        (ymin - ymax) / float(dst_height),
+                        ymax
+                    )
+
+                elif bounds:
+                    if not res:
+                        raise click.BadParameter('Required when using --bounds',
+                            param='res', param_hint='res')
+
+                    xmin, ymin, xmax, ymax = transform_bounds(src.crs, dst_crs,
+                                                              *bounds)
+                    dst_transform = Affine(res[0], 0, xmin, 0, -res[1], ymax)
+                    dst_width = max(int(ceil((xmax - xmin) / res[0])), 1)
+                    dst_height = max(int(ceil((ymax - ymin) / res[1])), 1)
+
+                else:
+                    dst_transform, dst_width, dst_height = calculate_default_transform(
+                        src.crs, dst_crs, src.width, src.height, *src.bounds,
+                        resolution=res)
+
+            elif dimensions:
+                # Same projection, different dimensions, calculate resolution
+                dst_crs = src.crs
+                dst_width, dst_height = dimensions
+                dst_transform = Affine(
+                    (r - l) / float(dst_width),
+                    0, l, 0,
+                    (b - t) / float(dst_height),
+                    t
+                )
+
+            elif bounds:
+                # Same projection, different dimensions and possibly different
+                # resolution
+                if not res:
+                    res = (src.affine.a, -src.affine.e)
+
+                dst_crs = src.crs
+                xmin, ymin, xmax, ymax = bounds
+                dst_transform = Affine(res[0], 0, xmin, 0, -res[1], ymax)
+                dst_width = max(int(ceil((xmax - xmin) / res[0])), 1)
+                dst_height = max(int(ceil((ymax - ymin) / res[1])), 1)
+
+            elif res:
+                # Same projection, different resolution
+                dst_crs = src.crs
+                dst_transform = Affine(res[0], 0, l, 0, -res[1], t)
+                dst_width = max(int(ceil((r - l) / res[0])), 1)
+                dst_height = max(int(ceil((t - b) / res[1])), 1)
+
+            else:
+                dst_crs = src.crs
+                dst_transform = src.affine
+                dst_width = src.width
+                dst_height = src.height
+
+            out_kwargs.update({
+                'crs': dst_crs,
+                'transform': dst_transform,
+                'affine': dst_transform,
+                'width': dst_width,
+                'height': dst_height
+            })
+
+            out_kwargs.update(**creation_options)
+
+            with rasterio.open(output, 'w', **out_kwargs) as dst:
+                for i in range(1, src.count + 1):
+
+                    reproject(
+                        source=rasterio.band(src, i),
+                        destination=rasterio.band(dst, i),
+                        src_transform=src.affine,
+                        src_crs=src.crs,
+                        # src_nodata=#TODO
+                        dst_transform=out_kwargs['transform'],
+                        dst_crs=out_kwargs['crs'],
+                        # dst_nodata=#TODO
+                        resampling=resampling,
+                        num_threads=threads)
\ No newline at end of file
diff --git a/rasterio/sample.py b/rasterio/sample.py
new file mode 100644
index 0000000..aad74ad
--- /dev/null
+++ b/rasterio/sample.py
@@ -0,0 +1,10 @@
+# Workaround for issue #378. A pure Python generator.
+
+def sample_gen(dataset, xy, indexes=None):
+    index = dataset.index
+    read = dataset.read
+    for x, y in xy:
+        r, c = index(x, y)
+        window = ((r, r+1), (c, c+1))
+        data = read(indexes, window=window, masked=False, boundless=True)
+        yield data[:,0,0]
diff --git a/rasterio/tool.py b/rasterio/tool.py
index 6601cfc..cb6364f 100644
--- a/rasterio/tool.py
+++ b/rasterio/tool.py
@@ -2,7 +2,6 @@
 import code
 import collections
 import logging
-import sys
 
 try:
     import matplotlib.pyplot as plt
@@ -49,10 +48,16 @@ def stats(source):
     return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
 
 
-def main(banner, dataset):
-    """ Main entry point for use with interpreter """
-    code.interact(
-        banner,
-        local=dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt))
+def main(banner, dataset, alt_interpreter=None):
+    """ Main entry point for use with python interpreter """
+    local = dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt)
+    if not alt_interpreter:
+        code.interact(banner, local=local)
+    elif alt_interpreter == 'ipython':
+        import IPython
+        IPython.InteractiveShell.banner1 = banner
+        IPython.start_ipython(argv=[], user_ns=local)
+    else:
+        raise ValueError("Unsupported interpreter '%s'" % alt_interpreter)
 
     return 0
diff --git a/rasterio/warp.py b/rasterio/warp.py
index 219eac7..34d384f 100644
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -81,7 +81,7 @@ def transform_geom(
         precision)
 
 
-def transform_bounds(left, bottom, right, top, src_crs, dst_crs, densify_pts=21):
+def transform_bounds(src_crs, dst_crs, left, bottom, right, top, densify_pts=21):
     """
     Transforms bounds from src_crs to dst_crs, optionally densifying the edges
     (to account for nonlinear transformations along these edges) and extracting
@@ -91,13 +91,13 @@ def transform_bounds(left, bottom, right, top, src_crs, dst_crs, densify_pts=21)
 
     Parameters
     ----------
-    left, bottom, right, top: float
-        Bounding coordinates in src_crs, from the bounds property of a raster.
     src_crs: dict
         Source coordinate reference system, in rasterio dict format.
         Example: {'init': 'EPSG:4326'}
     dst_crs: dict
         Target coordinate reference system.
+    left, bottom, right, top: float
+        Bounding coordinates in src_crs, from the bounds property of a raster.
     densify_pts: uint, optional
         Number of points to add to each edge to account for nonlinear
         edges produced by the transform process.  Large numbers will produce
@@ -230,14 +230,14 @@ def reproject(
 
 
 def calculate_default_transform(
+        src_crs,
+        dst_crs,
+        width,
+        height,
         left,
         bottom,
         right,
         top,
-        width,
-        height,
-        src_crs,
-        dst_crs,
         resolution=None,
         densify_pts=21):
     """
@@ -257,13 +257,17 @@ def calculate_default_transform(
 
     Parameters
     ----------
-    left, bottom, right, top: float
-        Bounding coordinates in src_crs, from the bounds property of a raster.
     src_crs: dict
         Source coordinate reference system, in rasterio dict format.
         Example: {'init': 'EPSG:4326'}
     dst_crs: dict
         Target coordinate reference system.
+    width: int
+        Source raster width.
+    height: int
+        Source raster height.
+    left, bottom, right, top: float
+        Bounding coordinates in src_crs, from the bounds property of a raster.
     resolution: tuple (x resolution, y resolution) or float, optional
         Target resolution, in units of target coordinate reference system.
     densify_pts: uint, optional
@@ -277,7 +281,7 @@ def calculate_default_transform(
     """
 
     xmin, ymin, xmax, ymax = transform_bounds(
-        left, bottom, right, top, src_crs, dst_crs, densify_pts)
+        src_crs, dst_crs, left, bottom, right, top, densify_pts)
 
     x_dif = xmax - xmin
     y_dif = ymax - ymin
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 69196b1..5b7ff43 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,7 @@
 affine
 cligj
 coveralls>=0.4
-cython==0.21.2
+cython>=0.21.2
 delocate
 enum34
 numpy>=1.8.0
diff --git a/setup.py b/setup.py
index 7bed441..a45f8b4 100755
--- a/setup.py
+++ b/setup.py
@@ -130,6 +130,11 @@ ext_options = dict(
     libraries=libraries,
     extra_link_args=extra_link_args)
 
+if not os.name == "nt":
+    # These options fail on Windows if using Visual Studio
+    ext_options['extra_compile_args'] = ['-Wno-unused-parameter',
+                                         '-Wno-unused-function']
+
 log.debug('ext_options:\n%s', pprint.pformat(ext_options))
 
 # When building from a repo, Cython is required.
@@ -190,7 +195,7 @@ with open('README.rst') as f:
 # Runtime requirements.
 inst_reqs = [
     'affine>=1.0',
-    'cligj',
+    'cligj>=0.2.0',
     'Numpy>=1.7',
     'snuggs>=1.3.1']
 
@@ -224,27 +229,32 @@ setup_args = dict(
     packages=['rasterio', 'rasterio.rio'],
     entry_points='''
         [console_scripts]
-        rio=rasterio.rio.main:cli
-        
+        rio=rasterio.rio.main:main_group
+
         [rasterio.rio_commands]
-        bounds=rasterio.rio.rio:bounds
+        bounds=rasterio.rio.features:bounds
         calc=rasterio.rio.calc:calc
+        convert=rasterio.rio.convert:convert
         edit-info=rasterio.rio.info:edit
         env=rasterio.rio.info:env
         info=rasterio.rio.info:info
-        insp=rasterio.rio.rio:insp
+        insp=rasterio.rio.info:insp
         mask=rasterio.rio.features:mask
         merge=rasterio.rio.merge:merge
+        overview=rasterio.rio.overview:overview
         rasterize=rasterio.rio.features:rasterize
         sample=rasterio.rio.sample:sample
         shapes=rasterio.rio.features:shapes
         stack=rasterio.rio.bands:stack
-        transform=rasterio.rio.rio:transform
+        warp=rasterio.rio.warp:warp
+        transform=rasterio.rio.info:transform
     ''',
     include_package_data=True,
     ext_modules=ext_modules,
     zip_safe=False,
-    install_requires=inst_reqs)
+    install_requires=inst_reqs,
+    extras_require={
+        'ipython': ['ipython>=2.0']})
 
 if os.environ.get('PACKAGE_DATA'):
     setup_args['package_data'] = {'rasterio': ['gdal_data/*', 'proj_data/*']}
diff --git a/tests/data/RGB.byte.tif b/tests/data/RGB.byte.tif
index 1efaf4a..9396ba2 100644
Binary files a/tests/data/RGB.byte.tif and b/tests/data/RGB.byte.tif differ
diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py
new file mode 100644
index 0000000..c98ba15
--- /dev/null
+++ b/tests/test_cli_main.py
@@ -0,0 +1,21 @@
+from pkg_resources import iter_entry_points
+
+from click.testing import CliRunner
+
+import rasterio
+from rasterio.rio.main import main_group
+
+
+def test_version():
+    runner = CliRunner()
+    result = runner.invoke(main_group, ['--version'])
+    assert result.exit_code == 0
+    assert rasterio.__version__ in result.output
+
+
+def test_all_registered():
+    # This test makes sure that all of the subcommands defined in the
+    # rasterio.rio_commands entry-point are actually registered to the main
+    # cli group.
+    for ep in iter_entry_points('rasterio.rio_commands'):
+        assert ep.name in main_group.commands
diff --git a/tests/test_colormap.py b/tests/test_colormap.py
index e5899fb..112ebfa 100644
--- a/tests/test_colormap.py
+++ b/tests/test_colormap.py
@@ -7,6 +7,21 @@ import rasterio
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
+
+def test_write_colormap_err(tmpdir):
+
+    with rasterio.drivers():
+
+        with rasterio.open('tests/data/shade.tif') as src:
+            meta = src.meta
+
+        tiffname = str(tmpdir.join('foo.tif'))
+
+        with rasterio.open(tiffname, 'w', **meta) as dst:
+            with pytest.raises(ValueError):
+                dst.write_colormap(1, {0: (255, 0, 0, 255), 255: (0, 0, 0, 0)})
+
+
 def test_write_colormap(tmpdir):
 
     with rasterio.drivers():
@@ -15,19 +30,20 @@ def test_write_colormap(tmpdir):
             shade = src.read_band(1)
             meta = src.meta
 
-        tiffname = str(tmpdir.join('foo.tif'))
-        
+        tiffname = str(tmpdir.join('foo.png'))
+        meta['driver'] = 'PNG'
+
         with rasterio.open(tiffname, 'w', **meta) as dst:
             dst.write_band(1, shade)
-            dst.write_colormap(1, {0: (255, 0, 0, 255), 255: (0, 0, 255, 255)})
+            dst.write_colormap(1, {0: (255, 0, 0, 255), 255: (0, 0, 0, 0)})
             cmap = dst.colormap(1)
             assert cmap[0] == (255, 0, 0, 255)
-            assert cmap[255] == (0, 0, 255, 255)
+            assert cmap[255] == (0, 0, 0, 0)
 
         with rasterio.open(tiffname) as src:
             cmap = src.colormap(1)
             assert cmap[0] == (255, 0, 0, 255)
-            assert cmap[255] == (0, 0, 255, 255)
+            assert cmap[255] == (0, 0, 0, 0)
 
     # subprocess.call(['open', tiffname])
 
diff --git a/tests/test_crs.py b/tests/test_crs.py
index eb6da86..300b5cd 100644
--- a/tests/test_crs.py
+++ b/tests/test_crs.py
@@ -2,13 +2,17 @@ import logging
 import pytest
 import subprocess
 import sys
+import json
 
 import rasterio
 from rasterio import crs
-from rasterio._base import is_geographic_crs, is_projected_crs, is_same_crs
+from rasterio.crs import (
+    is_geographic_crs, is_projected_crs, is_same_crs, is_valid_crs)
+
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
+
 # When possible, Rasterio gives you the CRS in the form of an EPSG code.
 def test_read_epsg(tmpdir):
     with rasterio.drivers():
@@ -57,6 +61,16 @@ def test_write_3857(tmpdir):
     AUTHORITY["EPSG","3857"]]""" in info.decode('utf-8')
 
 
+def test_from_proj4_json():
+    json_str = '{"proj": "longlat", "ellps": "WGS84", "datum": "WGS84"}'
+    crs_dict = crs.from_string(json_str)
+    assert crs_dict == json.loads(json_str)
+
+    # Test with invalid JSON code
+    with pytest.raises(ValueError):
+        assert crs.from_string('{foo: bar}')
+
+
 def test_from_epsg():
     crs_dict = crs.from_epsg(4326)
     assert crs_dict['init'].lower() == 'epsg:4326'
@@ -66,6 +80,15 @@ def test_from_epsg():
         assert crs.from_epsg(0)
 
 
+def test_from_epsg_string():
+    crs_dict = crs.from_string('epsg:4326')
+    assert crs_dict['init'].lower() == 'epsg:4326'
+
+    # Test with invalid EPSG code
+    with pytest.raises(ValueError):
+        assert crs.from_string('epsg:xyz')
+
+
 def test_bare_parameters():
     """ Make sure that bare parameters (e.g., no_defs) are handled properly,
     even if they come in with key=True.  This covers interaction with pyproj,
@@ -117,4 +140,16 @@ def test_is_same_crs():
     # Make sure that same projection with different parameter are not equal
     lcc_crs1 = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=49 +lat_0=0')
     lcc_crs2 = crs.from_string('+lon_0=-95 +ellps=GRS80 +y_0=0 +no_defs=True +proj=lcc +x_0=0 +units=m +lat_2=77 +lat_1=45 +lat_0=0')
-    assert is_same_crs(lcc_crs1, lcc_crs2) is False
\ No newline at end of file
+    assert is_same_crs(lcc_crs1, lcc_crs2) is False
+
+
+def test_to_string():
+    assert crs.to_string({'init': 'EPSG:4326'}) == "+init=EPSG:4326"
+
+
+def test_is_valid_false():
+    assert not is_valid_crs('EPSG:432600')
+
+
+def test_is_valid():
+    assert is_valid_crs('EPSG:4326')
diff --git a/tests/test_indexing.py b/tests/test_indexing.py
index f536302..c1c5f73 100644
--- a/tests/test_indexing.py
+++ b/tests/test_indexing.py
@@ -36,9 +36,16 @@ def test_window():
         left, bottom, right, top = src.bounds
         dx, dy = src.res
         eps = 1.0e-8
-        assert src.window(left+eps, bottom+eps, right-eps, top-eps) == ((0, src.height-1), 
-                                                        (0, src.width-1))
+        assert src.window(
+            left+eps, bottom+eps, right-eps, top-eps) == ((0, src.height),
+                                                          (0, src.width))
         assert src.index(left+400, top-400) == (1, 1)
         assert src.index(left+dx+eps, top-dy-eps) == (1, 1)
         assert src.window(left, top-400, left+400, top) == ((0, 1), (0, 1))
         assert src.window(left, top-2*dy-eps, left+2*dx+eps, top) == ((0, 2), (0, 2))
+
+
+def test_window_bounds_roundtrip():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert ((100, 200), (100, 200)) == src.window(
+            *src.window_bounds(((100, 200), (100, 200))))
diff --git a/tests/test_meta.py b/tests/test_meta.py
index 5e59c39..68070d1 100644
--- a/tests/test_meta.py
+++ b/tests/test_meta.py
@@ -3,26 +3,23 @@
 import rasterio
 
 
-def test_blocksize_rgb(tmpdir):
+def test_copy_meta(tmpdir):
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         kwds = src.meta
-        assert kwds['blockxsize'] == 791
-        assert kwds['blockysize'] == 3
-        assert kwds['tiled'] is False
+    with rasterio.open(
+            str(tmpdir.join('test_copy_meta.tif')), 'w', **kwds) as dst:
+        assert dst.meta['count'] == 3
 
-def test_blocksize_shade(tmpdir):
-    with rasterio.open('tests/data/shade.tif') as src:
-        kwds = src.meta
-        assert kwds['blockxsize'] == 1024
-        assert kwds['blockysize'] == 8
-        assert kwds['tiled'] is False
 
-def test_copy_meta(tmpdir):
+def test_blacklisted_keys(tmpdir):
+    # Some keys were removed from .meta when they were found to clash with
+    # creation options.
+    # https://github.com/mapbox/rasterio/issues/402
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         kwds = src.meta
     with rasterio.open(
             str(tmpdir.join('test_copy_meta.tif')), 'w', **kwds) as dst:
-        assert dst.meta['count'] == 3
-        assert dst.meta['blockxsize'] == 791
-        assert dst.meta['blockysize'] == 3
-        assert dst.meta['tiled'] is False
+        keys = map(lambda x: x.lower(), dst.meta.keys())
+        assert 'blockxsize' not in keys
+        assert 'blockysize' not in keys
+        assert 'tiled' not in keys
diff --git a/tests/test_options.py b/tests/test_options.py
new file mode 100644
index 0000000..38ee698
--- /dev/null
+++ b/tests/test_options.py
@@ -0,0 +1,20 @@
+import click
+import pytest
+from rasterio.rio import options
+
+
+def test_cb_key_val():
+
+    pairs = ['KEY=val', '1==']
+    expected = {
+        'KEY': 'val',
+        '1': '=',
+    }
+    assert options._cb_key_val(None, None, pairs) == expected
+
+    # Make sure None or an empty list returns an empty dict
+    assert options._cb_key_val(None, None, None) == {}
+    assert options._cb_key_val(None, None, ()) == {}
+
+    with pytest.raises(click.BadParameter):
+        options._cb_key_val(None, None, 'bad_val')
diff --git a/tests/test_overviews.py b/tests/test_overviews.py
new file mode 100644
index 0000000..381a06e
--- /dev/null
+++ b/tests/test_overviews.py
@@ -0,0 +1,40 @@
+"""Tests of overview counting and creation."""
+
+import logging
+import sys
+
+from click.testing import CliRunner
+
+import rasterio
+from rasterio.enums import Resampling
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_count_overviews_zero(data):
+    inputfile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(inputfile) as src:
+        assert src.overviews(1) == []
+        assert src.overviews(2) == []
+        assert src.overviews(3) == []
+
+
+def test_build_overviews_one(data):
+    inputfile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(inputfile, 'r+') as src:
+        overview_factors = [2]
+        src.build_overviews(overview_factors, resampling=Resampling.nearest)
+        assert src.overviews(1) == [2]
+        assert src.overviews(2) == [2]
+        assert src.overviews(3) == [2]
+
+
+def test_build_overviews_two(data):
+    inputfile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(inputfile, 'r+') as src:
+        overview_factors = [2, 4]
+        src.build_overviews(overview_factors, resampling=Resampling.nearest)
+        assert src.overviews(1) == [2, 4]
+        assert src.overviews(2) == [2, 4]
+        assert src.overviews(3) == [2, 4]
diff --git a/tests/test_profile.py b/tests/test_profile.py
index c277da5..32979c4 100644
--- a/tests/test_profile.py
+++ b/tests/test_profile.py
@@ -77,3 +77,13 @@ def test_profile_overlay():
     assert kwds['tiled']
     assert kwds['compress'] == 'lzw'
     assert kwds['count'] == 3
+
+
+def test_dataset_profile_property(data):
+    tiffile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(tiffile, 'r+') as src:
+        src.update_tags(ns='rio_creation_kwds', foo='bar')
+        assert src.profile['blockxsize'] == 791
+        assert src.profile['blockysize'] == 3
+        assert src.profile['tiled'] == False
+        assert src.profile['foo'] == 'bar'
diff --git a/tests/test_rio_cli.py b/tests/test_rio_cli.py
deleted file mode 100644
index 875e3fa..0000000
--- a/tests/test_rio_cli.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from rasterio.rio import cli
-
-
-def test_resolve_files_inout__output():
-    assert cli.resolve_inout(input='in', output='out') == ('out', ['in'])
-
-
-def test_resolve_files_inout__input():
-    assert cli.resolve_inout(input='in') == (None, ['in'])
-
-
-def test_resolve_files_inout__inout_files():
-    assert cli.resolve_inout(files=('a', 'b', 'c')) == ('c', ['a', 'b'])
-
-
-def test_resolve_files_inout__inout_files_output_o():
-    assert cli.resolve_inout(
-        files=('a', 'b', 'c'), output='out') == ('out', ['a', 'b', 'c'])
diff --git a/tests/test_rio_convert.py b/tests/test_rio_convert.py
new file mode 100644
index 0000000..91590a2
--- /dev/null
+++ b/tests/test_rio_convert.py
@@ -0,0 +1,120 @@
+import sys
+import os
+import logging
+import click
+from click.testing import CliRunner
+
+import rasterio
+from rasterio.rio.convert import convert
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+# Tests: format and type conversion, --format and --dtype
+
+def test_format(tmpdir):
+    outputname = str(tmpdir.join('test.jpg'))
+    runner = CliRunner()
+    result = runner.invoke(
+        convert,
+        ['tests/data/RGB.byte.tif', outputname, '--format', 'JPEG'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.driver == 'JPEG'
+
+
+def test_format_short(tmpdir):
+    outputname = str(tmpdir.join('test.jpg'))
+    runner = CliRunner()
+    result = runner.invoke(
+        convert,
+        ['tests/data/RGB.byte.tif', outputname, '-f', 'JPEG'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.driver == 'JPEG'
+
+
+def test_output_opt(tmpdir):
+    outputname = str(tmpdir.join('test.jpg'))
+    runner = CliRunner()
+    result = runner.invoke(
+        convert,
+        ['tests/data/RGB.byte.tif', '-o', outputname, '-f', 'JPEG'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.driver == 'JPEG'
+
+
+def test_dtype(tmpdir):
+    outputname = str(tmpdir.join('test.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        convert,
+        ['tests/data/RGB.byte.tif', outputname, '--dtype', 'uint16'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        assert src.dtypes == ['uint16']*3
+
+
+def test_dtype_rescaling_uint8_full(tmpdir):
+    """Rescale uint8 [0, 255] to uint8 [0, 255]"""
+    outputname = str(tmpdir.join('test.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        convert,
+        ['tests/data/RGB.byte.tif', outputname, '--scale-ratio', '1.0'])
+    assert result.exit_code == 0
+
+    src_stats = [
+        {"max": 255.0, "mean": 44.434478650699106, "min": 1.0},
+        {"max": 255.0, "mean": 66.02203484105824, "min": 1.0},
+        {"max": 255.0, "mean": 71.39316199120559, "min": 1.0}]
+
+    with rasterio.open(outputname) as src:
+        for band, expected in zip(src.read(masked=True), src_stats):
+            assert round(band.min() - expected['min'], 6) == 0.0
+            assert round(band.max() - expected['max'], 6) == 0.0
+            assert round(band.mean() - expected['mean'], 6) == 0.0
+
+
+def test_dtype_rescaling_uint8_half(tmpdir):
+    """Rescale uint8 [0, 255] to uint8 [0, 127]"""
+    outputname = str(tmpdir.join('test.tif'))
+    runner = CliRunner()
+    result = runner.invoke(convert, [
+        'tests/data/RGB.byte.tif', outputname, '--scale-ratio', '0.5'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        for band in src.read():
+            assert round(band.min() - 0, 6) == 0.0
+            assert round(band.max() - 127, 6) == 0.0
+
+
+def test_dtype_rescaling_uint16(tmpdir):
+    """Rescale uint8 [0, 255] to uint16 [0, 4095]"""
+    # NB: 255 * 16 is 4080, we don't actually get to 4095.
+    outputname = str(tmpdir.join('test.tif'))
+    runner = CliRunner()
+    result = runner.invoke(convert, [
+        'tests/data/RGB.byte.tif', outputname, '--dtype', 'uint16',
+        '--scale-ratio', '16'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        for band in src.read():
+            assert round(band.min() - 0, 6) == 0.0
+            assert round(band.max() - 4080, 6) == 0.0
+
+
+def test_dtype_rescaling_float64(tmpdir):
+    """Rescale uint8 [0, 255] to float64 [-1, 1]"""
+    outputname = str(tmpdir.join('test.tif'))
+    runner = CliRunner()
+    result = runner.invoke(convert, [
+        'tests/data/RGB.byte.tif', outputname, '--dtype', 'float64',
+        '--scale-ratio', str(2.0/255), '--scale-offset', '-1.0'])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as src:
+        for band in src.read():
+            assert round(band.min() + 1.0, 6) == 0.0
+            assert round(band.max() - 1.0, 6) == 0.0
diff --git a/tests/test_rio_helpers.py b/tests/test_rio_helpers.py
new file mode 100644
index 0000000..5e60740
--- /dev/null
+++ b/tests/test_rio_helpers.py
@@ -0,0 +1,23 @@
+from rasterio.rio import helpers
+
+
+def test_resolve_files_inout__output():
+    assert helpers.resolve_inout(input='in', output='out') == ('out', ['in'])
+
+
+def test_resolve_files_inout__input():
+    assert helpers.resolve_inout(input='in') == (None, ['in'])
+
+
+def test_resolve_files_inout__inout_files():
+    assert helpers.resolve_inout(files=('a', 'b', 'c')) == ('c', ['a', 'b'])
+
+
+def test_resolve_files_inout__inout_files_output_o():
+    assert helpers.resolve_inout(
+        files=('a', 'b', 'c'), output='out') == ('out', ['a', 'b', 'c'])
+
+
+def test_to_lower():
+    
+    assert helpers.to_lower(None, None, 'EPSG:3857') == 'epsg:3857'
\ No newline at end of file
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
index c1a5d91..79c5184 100644
--- a/tests/test_rio_info.py
+++ b/tests/test_rio_info.py
@@ -3,10 +3,13 @@ import logging
 import sys
 
 import click
+from click import Context
 from click.testing import CliRunner
+import pytest
 
 import rasterio
-from rasterio.rio import cli, info
+from rasterio.rio import info
+from rasterio.rio.main import main_group
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -111,9 +114,189 @@ def test_edit_tags(data):
         assert src.tags()['lol'] == '1'
         assert src.tags()['wut'] == '2'
 
+
+class MockContext:
+
+    def __init__(self):
+        self.obj = {}
+
+
+class MockOption:
+
+    def __init__(self, name):
+        self.name = name
+
+
+def test_like_dataset_callback(data):
+    ctx = MockContext()
+    info.like_handler(ctx, 'like', str(data.join('RGB.byte.tif')))
+    assert ctx.obj['like']['crs'] == {'init': 'epsg:32618'}
+
+
+def test_all_callback_pass(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'transform': 'foo'}
+    assert info.all_handler(ctx, None, None) == None
+
+
+def test_all_callback(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'transform': 'foo'}
+    assert info.all_handler(ctx, None, True) == {'transform': 'foo'}
+
+
+def test_all_callback_None(data):
+    ctx = MockContext()
+    assert info.all_handler(ctx, None, None) is None
+
+
+def test_transform_callback_pass(data):
+    """Always return None if the value is None"""
+    ctx = MockContext()
+    ctx.obj['like'] = {'transform': 'foo'}
+    assert info.transform_handler(ctx, MockOption('transform'), None) is None
+
+
+def test_transform_callback_err(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'transform': 'foo'}
+    with pytest.raises(click.BadParameter):
+        info.transform_handler(ctx, MockOption('transform'), '?')
+
+
+def test_transform_callback(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'transform': 'foo'}
+    assert info.transform_handler(ctx, MockOption('transform'), 'like') == 'foo'
+
+
+def test_nodata_callback_err(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'nodata': 'lolwut'}
+    with pytest.raises(click.BadParameter):
+        info.nodata_handler(ctx, MockOption('nodata'), 'lolwut')
+
+
+def test_nodata_callback_pass(data):
+    """Always return None if the value is None"""
+    ctx = MockContext()
+    ctx.obj['like'] = {'nodata': -1}
+    assert info.nodata_handler(ctx, MockOption('nodata'), None) is None
+
+
+def test_nodata_callback_0(data):
+    ctx = MockContext()
+    assert info.nodata_handler(ctx, MockOption('nodata'), '0') == 0.0
+
+
+def test_nodata_callback(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'nodata': -1}
+    assert info.nodata_handler(ctx, MockOption('nodata'), 'like') == -1.0
+
+
+def test_crs_callback_pass(data):
+    """Always return None if the value is None"""
+    ctx = MockContext()
+    ctx.obj['like'] = {'crs': 'foo'}
+    assert info.crs_handler(ctx, MockOption('crs'), None) is None
+
+
+def test_crs_callback(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'crs': 'foo'}
+    assert info.crs_handler(ctx, MockOption('crs'), 'like') == 'foo'
+
+
+def test_tags_callback_err(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'tags': {'foo': 'bar'}}
+    with pytest.raises(click.BadParameter):
+        info.tags_handler(ctx, MockOption('tags'), '?') == {'foo': 'bar'}
+
+
+def test_tags_callback(data):
+    ctx = MockContext()
+    ctx.obj['like'] = {'tags': {'foo': 'bar'}}
+    assert info.tags_handler(ctx, MockOption('tags'), 'like') == {'foo': 'bar'}
+
+
+def test_edit_crs_like(data):
+    runner = CliRunner()
+
+    # Set up the file to be edited.
+    inputfile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(inputfile, 'r+') as dst:
+        dst.crs = {'init': 'epsg:32617'}
+        dst.nodata = 1.0
+
+    # Double check.
+    with rasterio.open(inputfile) as src:
+        assert src.crs == {'init': 'epsg:32617'}
+        assert src.nodata == 1.0
+
+    # The test.
+    templatefile = 'tests/data/RGB.byte.tif'
+    result = runner.invoke(info.edit, [
+        inputfile, '--like', templatefile, '--crs', 'like'])
+    assert result.exit_code == 0
+    with rasterio.open(inputfile) as src:
+        assert src.crs == {'init': 'epsg:32618'}
+        assert src.nodata == 1.0
+
+
+def test_edit_nodata_like(data):
+    runner = CliRunner()
+
+    # Set up the file to be edited.
+    inputfile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(inputfile, 'r+') as dst:
+        dst.crs = {'init': 'epsg:32617'}
+        dst.nodata = 1.0
+
+    # Double check.
+    with rasterio.open(inputfile) as src:
+        assert src.crs == {'init': 'epsg:32617'}
+        assert src.nodata == 1.0
+
+    # The test.
+    templatefile = 'tests/data/RGB.byte.tif'
+    result = runner.invoke(info.edit, [
+        inputfile, '--like', templatefile, '--nodata', 'like'])
+    assert result.exit_code == 0
+    with rasterio.open(inputfile) as src:
+        assert src.crs == {'init': 'epsg:32617'}
+        assert src.nodata == 0.0
+
+
+def test_edit_all_like(data):
+    runner = CliRunner()
+
+    inputfile = str(data.join('RGB.byte.tif'))
+    with rasterio.open(inputfile, 'r+') as dst:
+        dst.crs = {'init': 'epsg:32617'}
+        dst.nodata = 1.0
+
+    # Double check.
+    with rasterio.open(inputfile) as src:
+        assert src.crs == {'init': 'epsg:32617'}
+        assert src.nodata == 1.0
+
+    templatefile = 'tests/data/RGB.byte.tif'
+    result = runner.invoke(info.edit, [
+        inputfile, '--like', templatefile, '--all'])
+    assert result.exit_code == 0
+    with rasterio.open(inputfile) as src:
+        assert src.crs == {'init': 'epsg:32618'}
+        assert src.nodata == 0.0
+
+
 def test_env():
     runner = CliRunner()
-    result = runner.invoke(info.env, ['--formats'])
+    result = runner.invoke(main_group, [
+        'env',
+        '--formats'
+    ])
     assert result.exit_code == 0
     assert 'GTiff' in result.output
 
@@ -137,17 +320,21 @@ def test_info():
 
 def test_info_verbose():
     runner = CliRunner()
-    result = runner.invoke(
-        cli.cli,
-        ['-v', 'info', 'tests/data/RGB.byte.tif'])
+    result = runner.invoke(main_group, [
+        '-v',
+        'info',
+        'tests/data/RGB.byte.tif'
+    ])
     assert result.exit_code == 0
 
 
 def test_info_quiet():
     runner = CliRunner()
-    result = runner.invoke(
-        cli.cli,
-        ['-q', 'info', 'tests/data/RGB.byte.tif'])
+    result = runner.invoke(main_group, [
+        '-q',
+        'info',
+        'tests/data/RGB.byte.tif'
+    ])
     assert result.exit_code == 0
 
 
@@ -206,7 +393,8 @@ def test_mo_info():
 
 def test_info_stats():
     runner = CliRunner()
-    result = runner.invoke(info.info, ['tests/data/RGB.byte.tif', '--tell-me-more'])
+    result = runner.invoke(
+        info.info, ['tests/data/RGB.byte.tif', '--tell-me-more'])
     assert result.exit_code == 0
     assert '"max": 255.0' in result.output
     assert '"min": 1.0' in result.output
@@ -215,6 +403,241 @@ def test_info_stats():
 
 def test_info_stats_only():
     runner = CliRunner()
-    result = runner.invoke(info.info, ['tests/data/RGB.byte.tif', '--stats', '--bidx', '2'])
+    result = runner.invoke(
+        info.info, ['tests/data/RGB.byte.tif', '--stats', '--bidx', '2'])
     assert result.exit_code == 0
     assert result.output.startswith('1.000000 255.000000 66.02')
+
+
+def test_transform_err():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'transform'
+    ], "[-78.0]")
+    assert result.exit_code == 1
+
+
+def test_transform_point():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'transform',
+        '--dst-crs', 'EPSG:32618',
+        '--precision', '2'
+    ], "[-78.0, 23.0]", catch_exceptions=False)
+    assert result.exit_code == 0
+    assert result.output.strip() == '[192457.13, 2546667.68]'
+
+
+def test_transform_point_dst_file():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'transform',
+        '--dst-crs', 'tests/data/RGB.byte.tif', '--precision', '2'
+    ], "[-78.0, 23.0]")
+    assert result.exit_code == 0
+    assert result.output.strip() == '[192457.13, 2546667.68]'
+
+
+def test_transform_point_src_file():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'transform',
+        '--src-crs',
+        'tests/data/RGB.byte.tif',
+        '--precision', '2'
+    ], "[192457.13, 2546667.68]")
+    assert result.exit_code == 0
+    assert result.output.strip() == '[-78.0, 23.0]'
+
+
+def test_transform_point_2():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'transform',
+        '[-78.0, 23.0]',
+        '--dst-crs', 'EPSG:32618',
+        '--precision', '2'
+    ])
+    assert result.exit_code == 0
+    assert result.output.strip() == '[192457.13, 2546667.68]'
+
+
+def test_transform_point_multi():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'transform',
+        '--dst-crs', 'EPSG:32618',
+        '--precision', '2'
+    ], "[-78.0, 23.0]\n[-78.0, 23.0]", catch_exceptions=False)
+    assert result.exit_code == 0
+    assert result.output.strip() == (
+        '[192457.13, 2546667.68]\n[192457.13, 2546667.68]')
+
+
+def test_bounds_defaults():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif'
+    ])
+    assert result.exit_code == 0
+    assert 'FeatureCollection' in result.output
+
+
+def test_bounds_err():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests'
+    ])
+    assert result.exit_code == 1
+
+
+def test_bounds_feature():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--feature'
+    ])
+    assert result.exit_code == 0
+    assert result.output.count('Polygon') == 1
+
+
+def test_bounds_obj_bbox():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--bbox',
+        '--precision', '2'
+    ])
+    assert result.exit_code == 0
+    assert result.output.strip() == '[-78.9, 23.56, -76.6, 25.55]'
+
+
+def test_bounds_compact():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--bbox',
+        '--precision', '2',
+        '--compact'
+    ])
+    assert result.exit_code == 0
+    assert result.output.strip() == '[-78.9,23.56,-76.6,25.55]'
+
+
+def test_bounds_indent():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--bbox',
+        '--indent', '2',
+        '--precision', '2'
+    ])
+    assert result.exit_code == 0
+    assert len(result.output.split('\n')) == 7
+
+
+def test_bounds_obj_bbox_mercator():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--bbox',
+        '--mercator',
+        '--precision', '3'
+    ])
+    assert result.exit_code == 0
+    assert result.output.strip() == (
+        '[-8782900.033, 2700489.278, -8527010.472, 2943560.235]')
+
+
+def test_bounds_obj_bbox_projected():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--bbox',
+        '--projected',
+        '--precision', '3'
+    ])
+    assert result.exit_code == 0
+    assert result.output.strip() == (
+        '[101985.0, 2611485.0, 339315.0, 2826915.0]')
+
+
+def test_bounds_crs_bbox():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        '--bbox',
+        '--dst-crs', 'EPSG:32618',
+        '--precision', '3'
+    ])
+    assert result.exit_code == 0
+    assert result.output.strip() == (
+        '[101985.0, 2611485.0, 339315.0, 2826915.0]')
+
+
+def test_bounds_seq():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        'tests/data/RGB.byte.tif',
+        '--sequence'
+    ])
+    assert result.exit_code == 0
+    assert result.output.count('Polygon') == 2
+
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        'tests/data/RGB.byte.tif',
+        '--sequence',
+        '--bbox',
+        '--precision', '2'
+    ])
+    assert result.exit_code == 0
+    assert result.output == (
+        '[-78.9, 23.56, -76.6, 25.55]\n[-78.9, 23.56, -76.6, 25.55]\n')
+    assert '\x1e' not in result.output
+
+
+def test_bounds_seq_rs():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'bounds',
+        'tests/data/RGB.byte.tif',
+        'tests/data/RGB.byte.tif',
+        '--sequence',
+        '--rs',
+        '--bbox',
+        '--precision', '2'
+    ])
+    assert result.exit_code == 0
+    assert result.output == (
+        '\x1e[-78.9, 23.56, -76.6, 25.55]\n\x1e[-78.9, 23.56, -76.6, 25.55]\n')
+
+
+def test_insp():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'insp',
+        'tests/data/RGB.byte.tif'
+    ])
+    assert result.exit_code == 0
+
+
+def test_insp_err():
+    runner = CliRunner()
+    result = runner.invoke(main_group, [
+        'insp',
+        'tests'
+    ])
+    assert result.exit_code == 1
diff --git a/tests/test_rio_merge.py b/tests/test_rio_merge.py
index 5fa5f91..d492373 100644
--- a/tests/test_rio_merge.py
+++ b/tests/test_rio_merge.py
@@ -329,3 +329,23 @@ def test_merge_tiny_output_opt(tiffs):
         assert (data[0][0:2,3] == 90).all()
         assert data[0][2][1] == 60
         assert data[0][3][0] == 40
+
+
+def test_merge_tiny_res(tiffs):
+    outputname = str(tiffs.join('merged.tif'))
+    inputs = [str(x) for x in tiffs.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname, '--res', 2])
+    assert result.exit_code == 0
+
+    # Output should be
+    # [[[120  90]
+    #   [  0   0]]]
+
+    with rasterio.open(outputname) as src:
+        data = src.read()
+        print(data)
+        assert data[0, 0, 0] == 120
+        assert data[0, 0, 1] == 90
+        assert (data[0, 1, 0:1] == 0).all()
diff --git a/tests/test_rio_overview.py b/tests/test_rio_overview.py
new file mode 100644
index 0000000..718f6d6
--- /dev/null
+++ b/tests/test_rio_overview.py
@@ -0,0 +1,67 @@
+import logging
+import sys
+
+from click.testing import CliRunner
+
+import rasterio
+from rasterio.rio.main import main_group as cli
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_err(data):
+    runner = CliRunner()
+    inputfile = str(data.join('RGB.byte.tif'))
+    result = runner.invoke(cli, ['overview', inputfile, '--build', 'a^2'])
+    assert result.exit_code == 2
+    assert "must match" in result.output
+
+
+def test_ls_none(data):
+    runner = CliRunner()
+    inputfile = str(data.join('RGB.byte.tif'))
+    result = runner.invoke(cli, ['overview', inputfile, '--ls'])
+    assert result.exit_code == 0
+    expected = "Overview factors:\n  Band 1: None (method: 'unknown')\n  Band 2: None (method: 'unknown')\n  Band 3: None (method: 'unknown')\n"
+    assert result.output == expected
+
+
+def test_build_ls(data):
+    runner = CliRunner()
+    inputfile = str(data.join('RGB.byte.tif'))
+    result = runner.invoke(cli, ['overview', inputfile, '--build', '2,4,8'])
+    assert result.exit_code == 0
+    result = runner.invoke(cli, ['overview', inputfile, '--ls'])
+    assert result.exit_code == 0
+    expected = "  Band 1: [2, 4, 8] (method: 'nearest')\n  Band 2: [2, 4, 8] (method: 'nearest')\n  Band 3: [2, 4, 8] (method: 'nearest')\n"
+    assert result.output.endswith(expected)
+
+
+def test_build_pow_ls(data):
+    runner = CliRunner()
+    inputfile = str(data.join('RGB.byte.tif'))
+    result = runner.invoke(cli, ['overview', inputfile, '--build', '2^1..3'])
+    assert result.exit_code == 0
+    result = runner.invoke(cli, ['overview', inputfile, '--ls'])
+    assert result.exit_code == 0
+    expected = "  Band 1: [2, 4, 8] (method: 'nearest')\n  Band 2: [2, 4, 8] (method: 'nearest')\n  Band 3: [2, 4, 8] (method: 'nearest')\n"
+    assert result.output.endswith(expected)
+
+
+def test_rebuild_ls(data):
+    runner = CliRunner()
+    inputfile = str(data.join('RGB.byte.tif'))
+
+    result = runner.invoke(cli,
+        ['overview', inputfile, '--build', '2,4,8', '--resampling', 'cubic'])
+    assert result.exit_code == 0
+
+    result = runner.invoke(cli, ['overview', inputfile, '--rebuild'])
+    assert result.exit_code == 0
+
+    result = runner.invoke(cli, ['overview', inputfile, '--ls'])
+    assert result.exit_code == 0
+
+    expected = "  Band 1: [2, 4, 8] (method: 'cubic')\n  Band 2: [2, 4, 8] (method: 'cubic')\n  Band 3: [2, 4, 8] (method: 'cubic')\n"
+    assert result.output.endswith(expected)
diff --git a/tests/test_rio_rio.py b/tests/test_rio_rio.py
deleted file mode 100644
index b8e7ef3..0000000
--- a/tests/test_rio_rio.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import click
-from click.testing import CliRunner
-
-
-import rasterio
-from rasterio.rio import cli, rio
-
-
-def test_version():
-    runner = CliRunner()
-    result = runner.invoke(cli.cli, ['--version'])
-    assert result.exit_code == 0
-    assert rasterio.__version__ in result.output
-
-
-def test_insp():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.insp,
-        ['tests/data/RGB.byte.tif'])
-    assert result.exit_code == 0
-
-
-def test_insp_err():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.insp,
-        ['tests'])
-    assert result.exit_code == 1
-
-
-def test_bounds_defaults():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif'])
-    assert result.exit_code == 0
-    assert 'FeatureCollection' in result.output
-
-
-def test_bounds_err():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests'])
-    assert result.exit_code == 1
-
-
-def test_bounds_feature():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', '--feature'])
-    assert result.exit_code == 0
-    assert result.output.count('Polygon') == 1
-
-
-def test_bounds_obj_bbox():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', '--bbox', '--precision', '2'])
-    assert result.exit_code == 0
-    assert result.output.strip() == '[-78.9, 23.56, -76.6, 25.55]'
-
-
-def test_bounds_compact():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', '--bbox', '--precision', '2', '--compact'])
-    assert result.exit_code == 0
-    assert result.output.strip() == '[-78.9,23.56,-76.6,25.55]'
-
-
-def test_bounds_indent():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', '--bbox', '--indent', '2', '--precision', '2'])
-    assert result.exit_code == 0
-    assert len(result.output.split('\n')) == 7
-
-
-def test_bounds_obj_bbox_mercator():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', '--bbox', '--mercator', '--precision', '3'])
-    assert result.exit_code == 0
-    assert result.output.strip() == '[-8782900.033, 2700489.278, -8527010.472, 2943560.235]'
-
-
-def test_bounds_obj_bbox_projected():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', '--bbox', '--projected', '--precision', '3'])
-    assert result.exit_code == 0
-    assert result.output.strip() == '[101985.0, 2611485.0, 339315.0, 2826915.0]'
-
-
-def test_bounds_seq():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence'])
-    assert result.exit_code == 0
-    assert result.output.count('Polygon') == 2
-
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence', '--bbox', '--precision', '2'])
-    assert result.exit_code == 0
-    assert result.output == '[-78.9, 23.56, -76.6, 25.55]\n[-78.9, 23.56, -76.6, 25.55]\n'
-    assert '\x1e' not in result.output
-
-
-def test_bounds_seq_rs():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.bounds,
-        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence', '--rs', '--bbox', '--precision', '2'])
-    assert result.exit_code == 0
-    assert result.output == '\x1e[-78.9, 23.56, -76.6, 25.55]\n\x1e[-78.9, 23.56, -76.6, 25.55]\n'
-
-
-def test_transform_err():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.transform,
-        [], "[-78.0]")
-    assert result.exit_code == 1
-
-
-def test_transform_point():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.transform,
-        ['--dst-crs', 'EPSG:32618', '--precision', '2'],
-        "[-78.0, 23.0]", catch_exceptions=False)
-    assert result.exit_code == 0
-    assert result.output.strip() == '[192457.13, 2546667.68]'
-
-
-def test_transform_point_dst_file():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.transform,
-        ['--dst-crs', 'tests/data/RGB.byte.tif', '--precision', '2'],
-        "[-78.0, 23.0]")
-    assert result.exit_code == 0
-    assert result.output.strip() == '[192457.13, 2546667.68]'
-
-
-def test_transform_point_src_file():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.transform,
-        ['--src-crs', 'tests/data/RGB.byte.tif', '--precision', '2'],
-        "[192457.13, 2546667.68]")
-    assert result.exit_code == 0
-    assert result.output.strip() == '[-78.0, 23.0]'
-
-
-def test_transform_point_2():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.transform,
-        ['[-78.0, 23.0]', '--dst-crs', 'EPSG:32618', '--precision', '2'])
-    assert result.exit_code == 0
-    assert result.output.strip() == '[192457.13, 2546667.68]'
-
-
-def test_transform_point_multi():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.transform,
-        ['--dst-crs', 'EPSG:32618', '--precision', '2'],
-        "[-78.0, 23.0]\n[-78.0, 23.0]", catch_exceptions=False)
-    assert result.exit_code == 0
-    assert result.output.strip() == '[192457.13, 2546667.68]\n[192457.13, 2546667.68]'
diff --git a/tests/test_rio_warp.py b/tests/test_rio_warp.py
new file mode 100644
index 0000000..b5a57fc
--- /dev/null
+++ b/tests/test_rio_warp.py
@@ -0,0 +1,238 @@
+import logging
+import os
+import re
+import sys
+import numpy
+
+import rasterio
+from rasterio.rio import warp
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_warp_no_reproject(runner, tmpdir):
+    """ When called without parameters, output should be same as source """
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.count == src.count
+            assert output.crs == src.crs
+            assert output.nodata == src.nodata
+            assert numpy.allclose(output.bounds, src.bounds)
+            assert output.affine.almost_equals(src.affine)
+            assert numpy.allclose(output.read(1), src.read(1))
+
+
+def test_warp_no_reproject_dimensions(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dimensions', '100', '100'])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.crs == src.crs
+            assert output.width == 100
+            assert output.height == 100
+            assert numpy.allclose([97.839396, 97.839396],
+                                  [output.affine.a, -output.affine.e])
+
+
+def test_warp_no_reproject_res(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--res', 30])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.crs == src.crs
+            assert numpy.allclose([30, 30], [output.affine.a, -output.affine.e])
+            assert output.width == 327
+            assert output.height == 327
+
+
+def test_warp_no_reproject_bounds(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    out_bounds = [-11850000, 4810000, -11849000, 4812000]
+    result = runner.invoke(warp.warp,[srcname, outputname,
+                                      '--bounds'] + out_bounds)
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.crs == src.crs
+            assert numpy.allclose(output.bounds, out_bounds)
+            assert numpy.allclose([src.affine.a, src.affine.e],
+                                  [output.affine.a, output.affine.e])
+            assert output.width == 105
+            assert output.height == 210
+
+
+def test_warp_no_reproject_bounds_res(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    out_bounds = [-11850000, 4810000, -11849000, 4812000]
+    result = runner.invoke(warp.warp,[srcname, outputname,
+                                      '--res', 30,
+                                      '--bounds', ] + out_bounds)
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.crs == src.crs
+            assert numpy.allclose(output.bounds, out_bounds)
+            assert numpy.allclose([30, 30], [output.affine.a, -output.affine.e])
+            assert output.width == 34
+            assert output.height == 67
+
+
+def test_warp_reproject_dst_crs(runner, tmpdir):
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', 'EPSG:4326'])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.count == src.count
+            assert output.crs == {'init': 'epsg:4326'}
+            assert output.width == 824
+            assert output.height == 686
+            assert numpy.allclose(output.bounds,
+                                  [-78.95864996545055, 23.564424693996177,
+                                   -76.57259451863895, 25.550873767433984])
+
+def test_warp_reproject_dst_crs_error(runner, tmpdir):
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', '{foo: bar}'])
+    assert result.exit_code == 2
+    assert 'invalid crs format' in result.output
+
+
+def test_warp_reproject_dst_crs_proj4(runner, tmpdir):
+    proj4 = '+proj=longlat +ellps=WGS84 +datum=WGS84'
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', proj4])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(outputname) as output:
+        assert output.crs == {'init': 'epsg:4326'}  # rasterio converts to EPSG
+
+
+def test_warp_reproject_res(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', 'EPSG:4326',
+                                       '--res', 0.01])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(outputname) as output:
+        assert output.crs == {'init': 'epsg:4326'}
+        assert numpy.allclose([0.01, 0.01], [output.affine.a, -output.affine.e])
+        assert output.width == 9
+        assert output.height == 7
+
+
+def test_warp_reproject_dimensions(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', 'EPSG:4326',
+                                       '--dimensions', '100', '100'])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.crs == {'init': 'epsg:4326'}
+            assert output.width == 100
+            assert output.height == 100
+            assert numpy.allclose([0.0008789062498762235, 0.0006771676143921468],
+                                  [output.affine.a, -output.affine.e])
+
+
+def test_warp_reproject_bounds_no_res(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    out_bounds = [-11850000, 4810000, -11849000, 4812000]
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', 'EPSG:4326',
+                                       '--bounds', ] + out_bounds)
+    assert result.exit_code == 2
+
+
+def test_warp_reproject_bounds_res(runner, tmpdir):
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    out_bounds = [-11850000, 4810000, -11849000, 4812000]
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', 'EPSG:4326',
+                                       '--res', 0.001, '--bounds', ]
+                                       + out_bounds)
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert output.crs == {'init': 'epsg:4326'}
+            assert numpy.allclose(output.bounds[:],
+                                  [-106.45036, 39.6138, -106.44136, 39.6278])
+            assert numpy.allclose([0.001, 0.001],
+                                  [output.affine.a, -output.affine.e])
+            assert output.width == 9
+            assert output.height == 14
+
+
+def test_warp_reproject_like(runner, tmpdir):
+    likename = str(tmpdir.join('like.tif'))
+    kwargs = {
+        "crs": {'init': 'epsg:4326'},
+        "transform": (-106.523, 0.001, 0, 39.6395, 0, -0.001),
+        "count": 1,
+        "dtype": rasterio.uint8,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10,
+        "nodata": 0
+    }
+
+    with rasterio.drivers():
+        with rasterio.open(likename, 'w', **kwargs) as dst:
+            data = numpy.zeros((10, 10), dtype=rasterio.uint8)
+            dst.write_band(1, data)
+
+    srcname = 'tests/data/shade.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--like', likename])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(outputname) as output:
+        assert output.crs == {'init': 'epsg:4326'}
+        assert numpy.allclose([0.001, 0.001], [output.affine.a, -output.affine.e])
+        assert output.width == 10
+        assert output.height == 10
diff --git a/tests/test_sampling.py b/tests/test_sampling.py
index 02eef7e..6d73737 100644
--- a/tests/test_sampling.py
+++ b/tests/test_sampling.py
@@ -6,12 +6,21 @@ def test_sampling():
         data = next(src.sample([(220650.0, 2719200.0)]))
         assert list(data) == [18, 25, 14]
 
+
 def test_sampling_beyond_bounds():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         data = next(src.sample([(-10, 2719200.0)]))
         assert list(data) == [0, 0, 0]
 
+
 def test_sampling_indexes():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         data = next(src.sample([(220650.0, 2719200.0)], indexes=[2]))
         assert list(data) == [25]
+
+
+def test_sampling_type():
+    """See https://github.com/mapbox/rasterio/issues/378."""
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        sampler = src.sample([(220650.0, 2719200.0)], indexes=[2])
+        assert type(sampler)
diff --git a/tests/test_warp.py b/tests/test_warp.py
index 7ba8cb0..d64dfae 100644
--- a/tests/test_warp.py
+++ b/tests/test_warp.py
@@ -30,7 +30,7 @@ class ReprojectParams(object):
 
         with rasterio.drivers():
             dt, dw, dh = calculate_default_transform(
-                left, bottom, right, top, width, height, src_crs, dst_crs)
+                src_crs, dst_crs, width, height, left, bottom, right, top)
             self.dst_transform = dt
             self.dst_width = dw
             self.dst_height = dh
@@ -68,7 +68,7 @@ def test_transform_bounds():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             l, b, r, t = src.bounds
             assert numpy.allclose(
-                transform_bounds(l, b, r, t, src.crs, {'init': 'EPSG:4326'}),
+                transform_bounds(src.crs, {'init': 'EPSG:4326'}, l, b, r, t),
                 (
                     -78.95864996545055, 23.564991210854686,
                     -76.57492370013823, 25.550873767433984
@@ -83,9 +83,9 @@ def test_transform_bounds_densify():
     dst_crs = {'init': 'EPSG:32610'}
     assert numpy.allclose(
         transform_bounds(
-            -120, 40, -80, 64,
             src_crs,
             dst_crs,
+            -120, 40, -80, 64,
             densify_pts=0
         ),
         (
@@ -96,9 +96,9 @@ def test_transform_bounds_densify():
 
     assert numpy.allclose(
         transform_bounds(
-            -120, 40, -80, 64,
             src_crs,
             dst_crs,
+            -120, 40, -80, 64,
             densify_pts=100
         ),
         (
@@ -114,7 +114,7 @@ def test_transform_bounds_no_change():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             l, b, r, t = src.bounds
             assert numpy.allclose(
-                transform_bounds(l, b, r, t, src.crs, src.crs),
+                transform_bounds(src.crs, src.crs, l, b, r, t),
                 src.bounds
             )
 
@@ -122,9 +122,9 @@ def test_transform_bounds_no_change():
 def test_transform_bounds_densify_out_of_bounds():
     with pytest.raises(ValueError):
         transform_bounds(
-            -120, 40, -80, 64,
             {'init': 'EPSG:4326'},
             {'init': 'EPSG:32610'},
+            -120, 40, -80, 64,
             densify_pts=-10
         )
 
@@ -136,10 +136,9 @@ def test_calculate_default_transform():
     )
     with rasterio.drivers():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
-            l, b, r, t = src.bounds
             wgs84_crs = {'init': 'EPSG:4326'}
             dst_transform, width, height = calculate_default_transform(
-                l, b, r, t, src.width, src.height, src.crs, wgs84_crs)
+                src.crs, wgs84_crs, src.width, src.height, *src.bounds)
 
             assert dst_transform.almost_equals(target_transform)
             assert width == 824
@@ -156,8 +155,8 @@ def test_calculate_default_transform_single_resolution():
                 0.0, -target_resolution, 25.550873767433984
             )
             dst_transform, width, height = calculate_default_transform(
-                l, b, r, t, src.width, src.height, src.crs,
-                {'init': 'EPSG:4326'}, resolution=target_resolution
+                src.crs, {'init': 'EPSG:4326'}, src.width, src.height,
+                *src.bounds, resolution=target_resolution
             )
 
             assert dst_transform.almost_equals(target_transform)
@@ -168,7 +167,6 @@ def test_calculate_default_transform_single_resolution():
 def test_calculate_default_transform_multiple_resolutions():
     with rasterio.drivers():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
-            l, b, r, t = src.bounds
             target_resolution = (0.2, 0.1)
             target_transform = Affine(
                 target_resolution[0], 0.0, -78.95864996545055,
@@ -176,8 +174,8 @@ def test_calculate_default_transform_multiple_resolutions():
             )
 
             dst_transform, width, height = calculate_default_transform(
-                l, b, r, t, src.width, src.height, src.crs,
-                {'init': 'EPSG:4326'}, resolution=target_resolution
+                src.crs, {'init': 'EPSG:4326'}, src.width, src.height,
+                *src.bounds, resolution=target_resolution
             )
 
             assert dst_transform.almost_equals(target_transform)
diff --git a/tests/test_write.py b/tests/test_write.py
index 25f5178..b8a7e20 100644
--- a/tests/test_write.py
+++ b/tests/test_write.py
@@ -148,6 +148,26 @@ def test_write_crs_transform(tmpdir):
     # (precision varies slightly by platform)
     assert re.search("Pixel Size = \(300.03792\d+,-300.04178\d+\)", info)
 
+def test_write_crs_transform_affine(tmpdir):
+    name = str(tmpdir.join("test_write_crs_transform.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    transform = [101985.0, 300.0379266750948, 0.0,
+                       2826915.0, 0.0, -300.041782729805]
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=1,
+            crs={'units': 'm', 'no_defs': True, 'ellps': 'WGS84', 
+                 'proj': 'utm', 'zone': 18},
+            affine=transform,
+            dtype=rasterio.ubyte) as s:
+        s.write_band(1, a)
+    assert s.crs == {'init': 'epsg:32618'}
+    info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
+    assert 'PROJCS["UTM Zone 18, Northern Hemisphere",' in info
+    # make sure that pixel size is nearly the same as transform
+    # (precision varies slightly by platform)
+    assert re.search("Pixel Size = \(300.03792\d+,-300.04178\d+\)", info)
+
 def test_write_crs_transform_2(tmpdir):
     """Using 'EPSG:32618' as CRS."""
     name = str(tmpdir.join("test_write_crs_transform.tif"))

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/rasterio.git



More information about the Pkg-grass-devel mailing list