[rasterio] 01/03: Imported Upstream version 0.15

Johan Van de Wauw johanvdw-guest at moszumanska.debian.org
Mon Oct 27 21:06:28 UTC 2014


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

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

commit 341a0acc1f8ab5fab0d7eeef6edb8a6da61e3322
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date:   Sun Oct 26 19:00:12 2014 +0100

    Imported Upstream version 0.15
---
 .travis.yml                      |   19 +
 AUTHORS.txt                      |   13 +
 CHANGES.txt                      |  164 ++++
 LICENSE.txt                      |   27 +
 MANIFEST.in                      |    8 +
 README.rst                       |  260 +++++++
 benchmarks/ndarray.py            |   73 ++
 docs/cli.rst                     |  221 ++++++
 docs/colormaps.rst               |   47 ++
 docs/concurrency.rst             |  162 ++++
 docs/datasets.rst                |  186 +++++
 docs/features.rst                |   91 +++
 docs/georeferencing.rst          |   81 ++
 docs/masks.rst                   |  122 +++
 docs/options.rst                 |   22 +
 docs/reproject.rst               |   64 ++
 docs/tags.rst                    |   90 +++
 docs/windowed-rw.rst             |  231 ++++++
 examples/async-rasterio.py       |  100 +++
 examples/concurrent-cpu-bound.py |   95 +++
 examples/decimate.py             |   31 +
 examples/features.ipynb          |  187 +++++
 examples/introduction.ipynb      |  393 ++++++++++
 examples/polygonize.py           |   10 +
 examples/rasterio_polygonize.py  |   74 ++
 examples/rasterize_geometry.py   |   27 +
 examples/reproject.py            |   62 ++
 examples/sieve.py                |   38 +
 examples/total.py                |   38 +
 rasterio/__init__.py             |  164 ++++
 rasterio/_base.pxd               |   25 +
 rasterio/_base.pyx               |  611 +++++++++++++++
 rasterio/_copy.pyx               |   55 ++
 rasterio/_drivers.pyx            |  120 +++
 rasterio/_err.pyx                |   70 ++
 rasterio/_example.pyx            |   24 +
 rasterio/_features.pxd           |   29 +
 rasterio/_features.pyx           |  586 +++++++++++++++
 rasterio/_gdal.pxd               |  215 ++++++
 rasterio/_io.pxd                 |  248 +++++++
 rasterio/_io.pyx                 | 1522 ++++++++++++++++++++++++++++++++++++++
 rasterio/_ogr.pxd                |   99 +++
 rasterio/_warp.pyx               |  532 +++++++++++++
 rasterio/coords.py               |    5 +
 rasterio/crs.py                  |  186 +++++
 rasterio/dtypes.py               |   98 +++
 rasterio/enums.py                |   19 +
 rasterio/features.py             |  318 ++++++++
 rasterio/five.py                 |   15 +
 rasterio/rio/__init__.py         |    1 +
 rasterio/rio/bands.py            |  129 ++++
 rasterio/rio/cli.py              |   83 +++
 rasterio/rio/features.py         |  156 ++++
 rasterio/rio/info.py             |  100 +++
 rasterio/rio/main.py             |    8 +
 rasterio/rio/merge.py            |   75 ++
 rasterio/rio/options.py          |   21 +
 rasterio/rio/rio.py              |  220 ++++++
 rasterio/tool.py                 |   53 ++
 rasterio/transform.py            |   29 +
 rasterio/warp.py                 |   46 ++
 requirements-dev.txt             |    7 +
 requirements.txt                 |    5 +
 setup.cfg                        |    8 +
 setup.py                         |  167 +++++
 tests/__init__.py                |    1 +
 tests/conftest.py                |   21 +
 tests/data/README.rst            |    8 +
 tests/data/RGB.byte.tif          |  Bin 0 -> 1713704 bytes
 tests/data/float.tif             |  Bin 0 -> 334 bytes
 tests/data/float_nan.tif         |  Bin 0 -> 600 bytes
 tests/data/shade.tif             |  Bin 0 -> 1050093 bytes
 tests/test_band.py               |   10 +
 tests/test_blocks.py             |  123 +++
 tests/test_cli.py                |  115 +++
 tests/test_colorinterp.py        |   25 +
 tests/test_colormap.py           |   33 +
 tests/test_coords.py             |   34 +
 tests/test_copy.py               |   26 +
 tests/test_crs.py                |   66 ++
 tests/test_driver_management.py  |   47 ++
 tests/test_dtypes.py             |   14 +
 tests/test_features_rasterize.py |  157 ++++
 tests/test_features_shapes.py    |  110 +++
 tests/test_features_sieve.py     |  141 ++++
 tests/test_indexing.py           |   25 +
 tests/test_nodata.py             |   46 ++
 tests/test_pad.py                |   15 +
 tests/test_png.py                |   20 +
 tests/test_read.py               |  242 ++++++
 tests/test_revolvingdoor.py      |   37 +
 tests/test_rio_bands.py          |   79 ++
 tests/test_rio_info.py           |   57 ++
 tests/test_rio_options.py        |   15 +
 tests/test_rio_rio.py            |  170 +++++
 tests/test_tags.py               |   56 ++
 tests/test_transform.py          |   11 +
 tests/test_update.py             |   61 ++
 tests/test_warp.py               |  184 +++++
 tests/test_write.py              |  244 ++++++
 100 files changed, 11178 insertions(+)

diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7180aed
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+language: python
+python:
+  - "2.7"
+  - "3.3"
+  - "3.4"
+before_install:
+  - sudo add-apt-repository -y ppa:ubuntugis/ppa
+  - sudo apt-get update -qq
+  - sudo apt-get install -y libgdal1h gdal-bin libgdal-dev
+install:
+  - "pip install -r requirements-dev.txt"
+  - "pip install pytest"
+  - "pip install coveralls"
+  - "pip install -e ."
+script: 
+  - py.test
+  - coverage run --source=rasterio --omit='*.pxd,*.pyx,*/tests/*,*/docs/*,*/examples/*,*/benchmarks/*' -m py.test
+after_success:
+  - coveralls
diff --git a/AUTHORS.txt b/AUTHORS.txt
new file mode 100644
index 0000000..54f3fca
--- /dev/null
+++ b/AUTHORS.txt
@@ -0,0 +1,13 @@
+Authors
+=======
+
+Sean Gillies <sean at mapbox.com>
+Brendan Ward https://github.com/brendan-ward
+Asger Skovbo Petersen https://github.com/AsgerPetersen
+James Seppi https://github.com/jseppi
+Chrisophe Gohlke https://github.com/cgohlke
+Robin Wilson https://github.com/robintw
+Mike Toews https://github.com/mwtoews
+Amit Kapadia https://github.com/kapadia
+
+See also https://github.com/mapbox/rasterio/graphs/contributors.
diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..0482830
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,164 @@
+Changes
+=======
+
+0.15
+----
+- Support for more data types in seive() (#159).
+- Handle unexpected PROJ.4 values like "+no_defs=True" (#173).
+- Support for writing PNG, JPEG, etc using GDALCreateCopy (#177).
+- New rio-stack command (#180).
+- Moved rio CLI main entry point to rasterio/rio/main:cli.
+- Add rio-env command and --version option to rio.
+- Make -f and --format aliases for --driver in CLI options (#183).
+- Remove older rio_* scripts (#184).
+- `out` keyword arg supercedes `output` in rasterio.features (#179).
+
+0.14.1 (2014-10-02)
+-------------------
+- Allow update of nodata values in r+ mode (#167).
+
+0.14 (2014-10-01)
+-----------------
+- Fixed tag update crasher (#145).
+- Add --mask and --bidx options to rio shapes (#150).
+- Faster geometry transforms and antimeridian cutting (#163).
+- Support for more data types in shapes() and rasterize() (#155, #158).
+- Switch to Cython 0.20+ for development (#151).
+
+0.13.2 (2014-09-23)
+-------------------
+- Add enum34 to requirements (#149).
+- Make rasterize() more robust (#146).
+- Pin Cython>=0.20 and Numpy>=1.8 (#151).
+
+0.13.1 (2014-09-13)
+-------------------
+- Read unprojected images with less flailing (#117).
+
+0.13 (2014-09-09)
+-----------------
+- Add single value options to rio info command (#139, #143).
+- Switch to console scripts entry points for rio, &c (#137).
+- Avoid unnecessary imports of Numpy in info command, elsewhere (#140).
+
+0.12.1 (2014-09-02)
+-------------------
+- Add missing rasterio.rio package (#135).
+
+0.12 (2014-09-02)
+-----------------
+- Add --mercator option for rio bounds (#126).
+- Add option for RS as a JSON text sequence separator (#127).
+- Add rio merge command (#131).
+- Change layout of tests (#134).
+
+0.11.1 (2014-08-19)
+-------------------
+- Add --bbox option for rio bounds (#124).
+
+0.11 (2014-08-06)
+-----------------
+- Add rio shapes command (#115).
+- Accept CRS strings like 'EPSG:3857' (#116).
+- Write multiple bands at a time (#95).
+
+0.10.1 (2014-07-21)
+-------------------
+- Numpy.require C-contiguous data when writing bands (#108).
+
+0.10 (2014-07-18)
+-----------------
+- Add rio bounds command (#111).
+- Add rio transform command (#112).
+
+0.9 (2014-07-16)
+----------------
+- Add meta and tag dumping options to rio_insp.
+- Leave GDAL finalization to the DLL's destructor (#91).
+- Add pad() function (#84).
+- New read() method, returns 3D arrays (#83).
+- New affine attribute and AffineMatrix object (#80, #86).
+- Removal of rasterio.insp script (#51).
+- Read_band() is now a special case of read() (#96).
+- Add support for multi-band reprojection (#98).
+- Support for GDAL CInt16 datasets (#97).
+- Fix loss of projection information (#102).
+- Fix for loss of nodata values (#109).
+- Permit other than C-contiguous arrays (#108).
+
+0.8 (2014-03-31)
+----------------
+- Add rasterize(), the inverse of shapes() (#45, #62).
+- Change the sense of mask for shapes(). Masks are always positive in
+  rasterio, so we extract shapes only where mask is True.
+
+0.7.3 (2014-03-22)
+------------------
+- Fix sieve() bug (#57).
+
+0.7.2 (2014-03-20)
+------------------
+- Add rio_insp, deprecation warning in rasterio.insp (#50, #52).
+- Fix transform bug in shapes() (#54).
+
+0.7.1 (2014-03-15)
+------------------
+- Source distribution bug fix (#48).
+
+0.7 (2014-03-14)
+----------------
+- Add a Band object, providing a shortcut for shapes() and sieve() functions
+  (#34).
+- Reprojection of rasters (#12).
+- Enhancements to the rasterio.insp console: module aliases, shortcut for
+  show().
+- Add index() method.
+- Reading and writing of GDAL mask bands (#41).
+- Add rio_cp program.
+- Enable r+ mode for GeoTIFFs (#46).
+
+0.6 (2014-02-10)
+----------------
+- Add support for dataset and band tags (#32).
+- Add testing dependence on pytest (#33).
+- Add support for simple RGBA colormaps (#34).
+- Fix for a crasher that occurs when a file is sent through a write-read
+  revolving door.
+- New docs for tags and colormaps.
+
+0.5.1 (2014-02-02)
+------------------
+- Add mask option to shapes() function (#26).
+- Add rasterio.insp interactive interpreter.
+
+0.5 (2014-01-22)
+----------------
+- Access to shapes of raster features via GDALPolygonize (#20).
+- Raster feature sieving (#21).
+- Registration and de-registration of drivers via context managers (#22).
+
+0.4 (2013-12-19)
+----------------
+- Add nodatavals property (#13).
+- Allow nodata to be set when opening file to write (#17).
+
+0.3 (2013-12-15)
+----------------
+- Drop six dependency (#9)
+- Add crs_wkt attribute (#10).
+- Add bounds attribute and ul() method (#11).
+- Add block_windows property (#7).
+- Enable windowed reads and writes (#6).
+- Use row,column ordering in window tuples as in Numpy (#13).
+- Add documentation on windowed reading and writing.
+
+0.2 (2013-11-24)
+----------------
+- Band indexes start at 1 (#2).
+- Decimation or replication of pixels on read and write (#3).
+- Add rasterio.copy() (#5).
+
+0.1 (2013-11-07)
+----------------
+- Reading and writing of GeoTIFFs, with examples.
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..89d3493
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2013, MapBox
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the <organization> nor the names of its contributors may
+  be used to endorse or promote products derived from this software without
+  specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..5fa402e
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,8 @@
+exclude *.rst *.txt *.py
+include CHANGES.txt AUTHORS.txt LICENSE.txt VERSION.txt README.rst setup.py
+recursive-include examples *.py
+recursive-include tests *.py *.rst
+recursive-exclude tests/data *.tif
+recursive-include tests/data *.txt
+recursive-include docs *.rst
+exclude MANIFEST.in
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..067dc11
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,260 @@
+========
+Rasterio
+========
+
+Rasterio reads and writes geospatial raster datasets.
+
+.. image:: https://travis-ci.org/mapbox/rasterio.png?branch=master
+   :target: https://travis-ci.org/mapbox/rasterio
+
+.. image:: https://coveralls.io/repos/mapbox/rasterio/badge.png
+   :target: https://coveralls.io/r/mapbox/rasterio
+
+Rasterio employs GDAL under the hood for file I/O and raster formatting. Its
+functions typically accept and return Numpy ndarrays. Rasterio is designed to
+make working with geospatial raster data more productive and more fun.
+
+Rasterio is pronounced raw-STIER-ee-oh.
+
+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. 
+
+.. 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):
+        
+        # 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.
+
+        total = numpy.zeros(r.shape, dtype=rasterio.uint16)
+        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
+        # dtype to uint8, and specify LZW compression.
+
+        kwargs = src.meta
+        kwargs.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))
+
+    # 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'])
+
+.. 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.open('tests/data/RGB.byte.tif') as src:
+            print(src.width, src.height)
+            print(src.crs)
+            print(src.affine)
+            print(src.count)
+            print(src.indexes)
+
+    # Output:
+    # (791, 718)
+    # {u'units': u'm', u'no_defs': True, u'ellps': u'WGS84', u'proj': u'utm', u'zone': 18}
+    # Affine(300.0379266750948, 0.0, 101985.0,
+    #        0.0, -300.041782729805, 2826915.0)
+    # 3
+    # [1, 2, 3]
+
+Rasterio also affords conversion of GeoTIFFs to other formats.
+
+.. code-block:: python
+    
+    with rasterio.drivers():
+
+        rasterio.copy(
+            'example-total.tif',
+            'example-total.jpg', 
+            driver='JPEG')
+    
+    subprocess.call(['open', 'example-total.jpg'])
+
+Rasterio CLI
+============
+
+Rasterio's command line interface, named "rio", is documented at `cli.rst
+<https://github.com/mapbox/rasterio/blob/master/docs/cli.rst>`__. Its ``rio
+insp`` command opens the hood of any raster dataset so you can poke around
+using Python.
+
+.. code-block:: pycon
+
+    $ rio insp tests/data/RGB.byte.tif
+    Rasterio 0.10 Interactive Inspector (Python 3.4.1)
+    Type "src.meta", "src.read_band(1)", or "help(src)" for more information.
+    >>> src.name
+    'tests/data/RGB.byte.tif'
+    >>> src.closed
+    False
+    >>> src.shape
+    (718, 791)
+    >>> src.crs
+    {'init': 'epsg:32618'}
+    >>> b, g, r = src.read()
+    >>> b
+    masked_array(data =
+     [[-- -- -- ..., -- -- --]
+     [-- -- -- ..., -- -- --]
+     [-- -- -- ..., -- -- --]
+     ...,
+     [-- -- -- ..., -- -- --]
+     [-- -- -- ..., -- -- --]
+     [-- -- -- ..., -- -- --]],
+                 mask =
+     [[ True  True  True ...,  True  True  True]
+     [ True  True  True ...,  True  True  True]
+     [ True  True  True ...,  True  True  True]
+     ...,
+     [ True  True  True ...,  True  True  True]
+     [ True  True  True ...,  True  True  True]
+     [ True  True  True ...,  True  True  True]],
+           fill_value = 0)
+
+    >>> b.min(), b.max(), b.mean()
+    (1, 255, 44.434478650699106)
+
+Dependencies
+============
+
+C library dependecies:
+
+- GDAL 1.9+
+
+Python package dependencies (see also requirements.txt):
+
+- affine
+- Numpy
+- setuptools
+
+Development also requires (see requirements-dev.txt)
+
+- Cython
+- pytest
+
+Installation
+============
+
+Rasterio is a C extension and to install on Linux or OS X you'll need a working
+compiler (XCode on OS X etc). You'll also need Numpy preinstalled; the Numpy
+headers are required to run the rasterio setup script. Numpy has to be
+installed (via the indicated requirements file) before rasterio can be
+installed. See rasterio's Travis `configuration
+<https://github.com/mapbox/rasterio/blob/master/.travis.yml>`__ for more
+guidance.
+
+
+Linux
+-----
+
+The following commands are adapted from Rasterio's Travis-CI configuration.
+
+.. code-block:: console
+
+    $ sudo add-apt-repository ppa:ubuntugis/ppa
+    $ sudo apt-get update -qq
+    $ sudo apt-get install python-numpy libgdal1h gdal-bin libgdal-dev
+    $ pip install -r https://raw.githubusercontent.com/mapbox/rasterio/master/requirements.txt
+    $ pip install rasterio
+
+Adapt them as necessary for your Linux system.
+
+OS X
+----
+
+Wheels are available on PyPI for Homebrew based Python environments.
+
+.. code-block:: console
+
+    $ brew install gdal
+    $ pip install -r https://raw.githubusercontent.com/mapbox/rasterio/master/requirements.txt
+    $ pip install rasterio
+
+The wheels are incompatible with MacPorts. MacPorts users will need to specify
+a source installation instead: ``pip install --no-use-wheel``.
+
+Windows
+-------
+
+Windows binary packages created by Christoph Gohlke are available `here
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#rasterio>`_.
+
+Testing
+-------
+
+From the repo directory, run py.test
+
+.. code-block:: console
+
+    $ py.test
+
+Documentation
+-------------
+
+See https://github.com/mapbox/rasterio/tree/master/docs.
+
+License
+-------
+
+See LICENSE.txt
+
+Authors
+-------
+
+See AUTHORS.txt
+
+Changes
+-------
+
+See CHANGES.txt
+
diff --git a/benchmarks/ndarray.py b/benchmarks/ndarray.py
new file mode 100644
index 0000000..b898138
--- /dev/null
+++ b/benchmarks/ndarray.py
@@ -0,0 +1,73 @@
+# Benchmark for read of raster data to ndarray
+
+import timeit
+
+import rasterio
+from osgeo import gdal
+
+# GDAL
+s = """
+src = gdal.Open('rasterio/tests/data/RGB.byte.tif')
+arr = src.GetRasterBand(1).ReadAsArray()
+src = None
+"""
+
+n = 100
+
+t = timeit.timeit(s, setup='from osgeo import gdal', number=n)
+print("GDAL:")
+print("%f msec\n" % (1000*t/n))
+
+# Rasterio
+s = """
+with rasterio.open('rasterio/tests/data/RGB.byte.tif') as src:
+    arr = src.read_band(1)
+"""
+
+t = timeit.timeit(s, setup='import rasterio', number=n)
+print("Rasterio:")
+print("%f msec\n" % (1000*t/n))
+
+# GDAL Extras
+s = """
+src = gdal.Open('rasterio/tests/data/RGB.byte.tif')
+transform = src.GetGeoTransform()
+srs = osr.SpatialReference()
+srs.ImportFromWkt(src.GetProjectionRef())
+wkt = srs.ExportToWkt()
+proj = srs.ExportToProj4()
+arr = src.GetRasterBand(1).ReadAsArray()
+src = None
+"""
+
+n = 1000
+
+t = timeit.timeit(s, setup='from osgeo import gdal; from osgeo import osr', number=n)
+print("GDAL + Extras:\n")
+print("%f usec\n" % (t/n))
+
+# Rasterio
+s = """
+with rasterio.open('rasterio/tests/data/RGB.byte.tif') as src:
+    transform = src.transform
+    proj = src.crs
+    wkt = src.crs_wkt
+    arr = src.read_band(1)
+"""
+
+t = timeit.timeit(s, setup='import rasterio', number=n)
+print("Rasterio:\n")
+print("%f usec\n" % (t/n))
+
+
+import pstats, cProfile
+
+s = """
+with rasterio.open('rasterio/tests/data/RGB.byte.tif') as src:
+    arr = src.read_band(1, window=(10, 10, 10, 10))
+"""
+
+cProfile.runctx(s, globals(), locals(), "Profile.prof")
+
+s = pstats.Stats("Profile.prof")
+s.strip_dirs().sort_stats("time").print_stats()
diff --git a/docs/cli.rst b/docs/cli.rst
new file mode 100644
index 0000000..a31e0ff
--- /dev/null
+++ b/docs/cli.rst
@@ -0,0 +1,221 @@
+Command Line Interface
+======================
+
+Rasterio's new command line interface is a program named "rio".
+
+.. code-block:: console
+
+    $ rio
+    Usage: rio [OPTIONS] COMMAND [ARGS]...
+
+      Rasterio command line interface.
+
+    Options:
+      -v, --verbose  Increase verbosity.
+      -q, --quiet    Decrease verbosity.
+      --help         Show this message and exit.
+
+    Commands:
+      bounds     Write bounding boxes to stdout as GeoJSON.
+      info       Print information about a data file.
+      insp       Open a data file and start an interpreter.
+      merge      Merge a stack of raster datasets.
+      shapes     Write the shapes of features.
+      stack      Stack a number of bands into a multiband dataset.
+      transform  Transform coordinates.
+
+It is developed using the ``click`` package.
+
+
+bounds
+------
+
+New in 0.10.
+
+The bounds command writes the bounding boxes of raster datasets to GeoJSON for
+use with, e.g., `geojsonio-cli <https://github.com/mapbox/geojsonio-cli>`__.
+
+.. code-block:: console
+
+    $ rio bounds tests/data/RGB.byte.tif --indent 2
+    {
+      "features": [
+        {
+          "geometry": {
+            "coordinates": [
+              [
+                [
+                  -78.898133,
+                  23.564991
+                ],
+                [
+                  -76.599438,
+                  23.564991
+                ],
+                [
+                  -76.599438,
+                  25.550874
+                ],
+                [
+                  -78.898133,
+                  25.550874
+                ],
+                [
+                  -78.898133,
+                  23.564991
+                ]
+              ]
+            ],
+            "type": "Polygon"
+          },
+          "properties": {
+            "id": "0",
+            "title": "tests/data/RGB.byte.tif"
+          },
+          "type": "Feature"
+        }
+      ],
+      "type": "FeatureCollection"
+    }
+
+Shoot the GeoJSON into a Leaflet map using geojsonio-cli by typing 
+``rio bounds tests/data/RGB.byte.tif | geojsonio``.
+
+info
+----
+
+Rio's info command intends to serve some of the same uses as gdalinfo.
+
+.. code-block:: console
+
+    $ rio info tests/data/RGB.byte.tif
+    { 'affine': Affine(300.0379266750948, 0.0, 101985.0,
+           0.0, -300.041782729805, 2826915.0),
+      'count': 3,
+      'crs': { 'init': u'epsg:32618'},
+      'driver': u'GTiff',
+      'dtype': <type 'numpy.uint8'>,
+      'height': 718,
+      'nodata': 0.0,
+      'transform': ( 101985.0,
+                     300.0379266750948,
+                     0.0,
+                     2826915.0,
+                     0.0,
+                     -300.041782729805),
+      'width': 791}
+
+insp
+----
+
+The insp command opens a dataset and an interpreter.
+
+.. code-block:: console
+
+    $ rio insp tests/data/RGB.byte.tif
+    Rasterio 0.9 Interactive Inspector (Python 2.7.5)
+    Type "src.meta", "src.read_band(1)", or "help(src)" for more information.
+    >>> import pprint
+    >>> pprint.pprint(src.meta)
+    {'affine': Affine(300.0379266750948, 0.0, 101985.0,
+           0.0, -300.041782729805, 2826915.0),
+     'count': 3,
+     'crs': {'init': u'epsg:32618'},
+     'driver': u'GTiff',
+     'dtype': <type 'numpy.uint8'>,
+     'height': 718,
+     'nodata': 0.0,
+     'transform': (101985.0,
+                   300.0379266750948,
+                   0.0,
+                   2826915.0,
+                   0.0,
+                   -300.041782729805),
+     'width': 791}
+
+merge
+-----
+
+The merge command can be used to flatten a stack of identically layed out
+datasets.
+
+.. code-block:: console
+
+    $ rio merge rasterio/tests/data/R*.tif -o result.tif
+
+shapes
+------
+
+New in 0.11.
+
+The shapes command extracts and writes features of a specified dataset band out
+as GeoJSON.
+
+.. code-block:: console
+
+    $ rio shapes tests/data/shade.tif --bidx 1 --precision 6 > shade.geojson
+
+The resulting file, uploaded to Mapbox, looks like this: `sgillies.j1ho338j <https://a.tiles.mapbox.com/v4/sgillies.j1ho338j/page.html?access_token=pk.eyJ1Ijoic2dpbGxpZXMiLCJhIjoiWUE2VlZVcyJ9.OITHkb1GHNh9nvzIfUc9QQ#13/39.6079/-106.4822>`__.
+
+Using the ``--mask`` option you can write out the shapes of a dataset's valid
+data region.
+
+.. code-block:: console
+
+    $ rio shapes --mask --precision 6 tests/data/RGB.byte.tif | geojsonio
+
+See http://bl.ocks.org/anonymous/raw/ef244954b719dba97926/.
+
+stack
+-----
+
+New in 0.15.
+
+The rio-stack command stack a number of bands from one or more input files into
+a multiband dataset. Input datasets must be of a kind: same data type,
+dimensions, etc. The output is cloned from the first input. By default,
+rio-stack will take all bands from each input and write them in same order to
+the output. Optionally, bands for each input may be specified using a simple
+syntax:
+
+- ``--bidx N`` takes the Nth band from the input (first band is 1).
+- ``--bidx M,N,O`` takes bands M, N, and O.
+- ``--bidx M..O`` takes bands M-O, inclusive.
+- ``--bidx ..N`` takes all bands up to and including N.
+- ``--bidx N..`` takes all bands from N to the end.
+
+Examples using the Rasterio testing dataset that produce a copy of it.
+
+.. code-block:: console
+
+    $ rio stack RGB.byte.tif -o stacked.tif
+    $ rio stack RGB.byte.tif --bidx 1,2,3 -o stacked.tif
+    $ rio stack RGB.byte.tif --bidx 1..3 -o stacked.tif
+    $ rio stack RGB.byte.tif --bidx ..2 RGB.byte.tif --bidx 3.. -o stacked.tif
+
+transform
+---------
+
+New in 0.10.
+
+The transform command reads a JSON array of coordinates, interleaved, and
+writes another array of transformed coordinates to stdout.
+
+To transform a longitude, latitude point (EPSG:4326 is the default) to 
+another coordinate system with 2 decimal places of output precision, do the
+following.
+
+.. code-block:: console
+
+    $ echo "[-78.0, 23.0]" | rio transform - --dst_crs EPSG:32618 --precision 2
+    [192457.13, 2546667.68]
+
+To transform a longitude, latitude bounding box to the coordinate system of
+a raster dataset, do the following.
+
+.. code-block:: console
+
+    $ 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]
+
+Suggestions for other commands are welcome!
diff --git a/docs/colormaps.rst b/docs/colormaps.rst
new file mode 100644
index 0000000..45af887
--- /dev/null
+++ b/docs/colormaps.rst
@@ -0,0 +1,47 @@
+Colormaps
+=========
+
+Writing colormaps
+-----------------
+
+Mappings from 8-bit (rasterio.uint8) pixel values to RGBA values can be attached
+to bands using the ``write_colormap()`` method.
+
+.. code-block:: python
+
+    import rasterio
+
+    with rasterio.drivers():
+
+        with rasterio.open('tests/data/shade.tif') as src:
+            shade = src.read_band(1)
+            meta = src.meta
+
+        with rasterio.open('/tmp/colormap.tif', 'w', **meta) as dst:
+            dst.write_band(1, shade)
+            dst.write_colormap(
+                1, {
+                    0: (255, 0, 0, 255), 
+                    255: (0, 0, 255, 255) })
+            cmap = dst.colormap(1)
+            # True
+            assert cmap[0] == (255, 0, 0, 255)
+            # True
+            assert cmap[255] == (0, 0, 255, 255)
+
+    subprocess.call(['open', '/tmp/colormap.tif'])
+
+The program above (on OS X, another viewer is needed with a different OS)
+yields the image below:
+
+.. image:: http://farm8.staticflickr.com/7391/12443115173_80ecca89db_d.jpg
+   :width: 500
+   :height: 500
+
+Reading colormaps
+-----------------
+
+As shown above, the ``colormap()`` returns a dict holding the colormap for the 
+given band index. For TIFF format files, the colormap will have 256 items, and
+all but two of those would map to (0, 0, 0, 0) in the example above.
+
diff --git a/docs/concurrency.rst b/docs/concurrency.rst
new file mode 100644
index 0000000..3d43859
--- /dev/null
+++ b/docs/concurrency.rst
@@ -0,0 +1,162 @@
+Concurrent processing
+=====================
+
+Rasterio affords concurrent processing of raster data. The Python GIL is
+released when calling GDAL's ``GDALRasterIO()`` function, which means that
+datasets can read and write concurrently with other threads.
+
+The Numpy library also releases the GIL as much as it can, e.g., in applying
+universal functions to arrays, and this makes it possible to distribute
+processing of an array across cores of a processor. The Cython function below,
+included in Rasterio's ``_example`` module, simulates such a GIL-releasing
+raster processing function.
+
+.. code-block:: python
+
+    import numpy
+    cimport numpy
+
+    def compute(
+            unsigned char[:, :, :] input, 
+            unsigned char[:, :, :] output):
+        # Given input and output uint8 arrays, fakes an CPU-intensive
+        # computation.
+        cdef int I, J, K
+        cdef int i, j, k, l
+        cdef double val
+        I = input.shape[0]
+        J = input.shape[1]
+        K = input.shape[2]
+        with nogil:
+            for i in range(I):
+                for j in range(J):
+                    for k in range(K):
+                        val = <double>input[i, j, k]
+                        for l in range(2000):
+                            val += 1.0
+                        val -= 2000.0
+                        output[~i, j, k] = <unsigned char>val
+
+
+Here is the program in examples/concurrent-cpu-bound.py.
+
+.. code-block:: python
+
+    """concurrent-cpu-bound.py
+
+    Operate on a raster dataset window-by-window using a ThreadPoolExecutor.
+
+    Simulates a CPU-bound thread situation where multiple threads can improve performance.
+
+    With -j 4, the program returns in about 1/4 the time as with -j 1.
+    """
+
+    import concurrent.futures
+    import multiprocessing
+    import time
+
+    import numpy
+    import rasterio
+    from rasterio._example import compute
+
+    def main(infile, outfile, num_workers=4):
+
+        with rasterio.drivers():
+
+            # Open the source dataset.
+            with rasterio.open(infile) as src:
+
+                # Create a destination dataset based on source params.
+                # The destination will be tiled, and we'll "process" the tiles
+                # concurrently.
+                meta = src.meta
+                del meta['transform']
+                meta.update(affine=src.affine)
+                meta.update(blockxsize=256, blockysize=256, tiled='yes')
+                with rasterio.open(outfile, 'w', **meta) as dst:
+
+                    # Define a generator for data, window pairs.
+                    # We use the new read() method here to a 3D array with all
+                    # bands, but could also use read_band().
+                    def jobs():
+                        for ij, window in dst.block_windows():
+                            data = src.read(window=window)
+                            result = numpy.zeros(data.shape, dtype=data.dtype)
+                            yield data, result, window
+
+                    # Submit the jobs to the thread pool executor.
+                    with concurrent.futures.ThreadPoolExecutor(
+                            max_workers=num_workers) as executor:
+
+                        # Map the futures returned from executor.submit()
+                        # to their destination windows.
+                        #
+                        # The _example.compute function modifies no Python
+                        # objects and releases the GIL. It can execute
+                        # concurrently.
+                        future_to_window = {
+                            executor.submit(compute, data, res): (res, window)
+                            for data, res, window in jobs()}
+
+                        # As the processing jobs are completed, get the
+                        # results and write the data to the appropriate
+                        # destination window.
+                        for future in concurrent.futures.as_completed(
+                                future_to_window):
+
+                            result, window = future_to_window[future]
+
+                            # Since there's no multiband write() method yet in
+                            # Rasterio, we use write_band for each part of the
+                            # 3D data array.
+                            for i, arr in enumerate(result, 1):
+                                dst.write_band(i, arr, window=window)
+
+
+    if __name__ == '__main__':
+
+        import argparse
+
+        parser = argparse.ArgumentParser(
+            description="Concurrent raster processing demo")
+        parser.add_argument(
+            'input',
+            metavar='INPUT',
+            help="Input file name")
+        parser.add_argument(
+            'output',
+            metavar='OUTPUT',
+            help="Output file name")
+        parser.add_argument(
+            '-j',
+            metavar='NUM_JOBS',
+            type=int,
+            default=multiprocessing.cpu_count(),
+            help="Number of concurrent jobs")
+        args = parser.parse_args()
+
+        main(args.input, args.output, args.j)
+
+The code above simulates a fairly CPU-intensive process that runs faster when
+spread over multiple cores using the ``ThreadPoolExecutor`` from Python 3's
+``concurrent.futures`` module. Compared to the case of one concurrent job 
+(``-j 1``)
+
+.. code-block:: console
+
+    $ time python examples/concurrent-cpu-bound.py tests/data/RGB.byte.tif /tmp/threads.tif -j 1
+
+    real    0m3.474s
+    user    0m3.426s
+    sys     0m0.043s
+
+we get an almost 3x speed up with four concurrent jobs.
+
+.. code-block:: console
+
+    $ time python examples/concurrent-cpu-bound.py tests/data/RGB.byte.tif /tmp/threads.tif -j 4
+
+    real    0m1.335s
+    user    0m3.400s
+    sys     0m0.043s
+
diff --git a/docs/datasets.rst b/docs/datasets.rst
new file mode 100644
index 0000000..e9d097a
--- /dev/null
+++ b/docs/datasets.rst
@@ -0,0 +1,186 @@
+Datasets and ndarrays
+=====================
+
+Dataset objects provide read, read-write, and write access to raster data files
+and are obtained by calling ``rasterio.open()``. That function mimics Python's
+built-in ``open()`` and the dataset objects it returns mimic Python ``file``
+objects.
+
+.. code-block:: pycon
+
+    >>> import rasterio
+    >>> dataset = rasterio.open('tests/data/RGB.byte.tif')
+    >>> dataset
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    >>> dataset.name
+    'tests/data/RGB.byte.tif'
+    >>> dataset.mode
+    r
+    >>> dataset.closed
+    False
+
+If you attempt to access a nonexistent path, ``rasterio.open()`` does the same
+thing as ``open()``, raising an exception immediately.
+
+.. code-block:: pycon
+
+    >>> open('/lol/wut.tif')
+    Traceback (most recent call last):
+      File "<stdin>", line 1, in <module>
+    IOError: [Errno 2] No such file or directory: '/lol/wut.tif'
+    >>> rasterio.open('/lol/wut.tif')
+    Traceback (most recent call last):
+      File "<stdin>", line 1, in <module>
+    IOError: no such file or directory: '/lol/wut.tif'
+
+Attributes
+----------
+
+In addition to the file-like attributes shown above, a dataset has a number
+of other read-only attributes that help explain its role in spatial information
+systems. The ``driver`` attribute gives you the name of the GDAL format
+driver used. The ``height`` and ``width`` are the number of rows and columns of
+the raster dataset and ``shape`` is a ``height, width`` tuple as used by
+Numpy. The ``count`` attribute tells you the number of bands in the dataset.
+
+.. code-block:: pycon
+
+    >>> dataset.driver
+    u'GTiff'
+    >>> dataset.height, dataset.width
+    (718, 791)
+    >>> dataset.shape
+    (718, 791)
+    >>> dataset.count
+    3
+
+What makes geospatial raster datasets different from other raster files is
+that their pixels map to regions of the Earth. A dataset has a coordinate
+reference system and an affine transformation matrix that maps pixel
+coordinates to coordinates in that reference system.
+
+.. code-block:: pycon
+
+    >>> dataset.crs
+    {u'units': u'm', u'no_defs': True, u'ellps': u'WGS84', u'proj': u'utm', u'zone': 18}
+    >>> dataset.affine
+    Affine(300.0379266750948, 0.0, 101985.0,
+           0.0, -300.041782729805, 2826915.0)
+
+To get the ``x, y`` world coordinates for the upper left corner of any pixel,
+take the product of the affine transformation matrix and the tuple ``(col,
+row)``.  
+
+.. code-block:: pycon
+
+    >>> col, row = 0, 0
+    >>> src.affine * (col, row)
+    (101985.0, 2826915.0)
+    >>> col, row = src.width, src.height
+    >>> src.affine * (col, row)
+    (339315.0, 2611485.0)
+
+Reading data
+------------
+
+Datasets generally have one or more bands (or layers). Following the GDAL
+convention, these are indexed starting with the number 1. The first band of
+a file can be read like this:
+
+.. code-block:: pycon
+
+    >>> dataset.read_band(1)
+    array([[0, 0, 0, ..., 0, 0, 0],
+           [0, 0, 0, ..., 0, 0, 0],
+           [0, 0, 0, ..., 0, 0, 0],
+           ...,
+           [0, 0, 0, ..., 0, 0, 0],
+           [0, 0, 0, ..., 0, 0, 0],
+           [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
+
+The returned object is a 2-dimensional Numpy ndarray. The representation of
+that array at the Python prompt is just a summary; the GeoTIFF file that
+Rasterio uses for testing has 0 values in the corners, but has nonzero values
+elsewhere.
+
+.. code-block::
+
+    >>> from matplotlib import pyplot
+    >>> pyplot.imshow(dataset.read_band(1), cmap='pink')
+    <matplotlib.image.AxesImage object at 0x111195c10>
+    >>> pyplot.show()
+
+.. image:: http://farm6.staticflickr.com/5032/13938576006_b99b23271b_o_d.png
+
+The indexes, Numpy data types, and nodata values of all a dataset's bands can
+be had from its ``indexes``, ``dtypes``, and ``nodatavals`` attributes.
+
+.. code-block:: pycon
+
+    >>> for i, dtype, ndval in zip(src.indexes, src.dtypes, src.nodatavals):
+    ...     print i, dtype, nodataval
+    ...
+    1 <type 'numpy.uint8'> 0.0
+    2 <type 'numpy.uint8'> 0.0
+    3 <type 'numpy.uint8'> 0.0
+
+To close a dataset, call its ``close()`` method.
+
+.. code-block:: pycon
+
+    >>> dataset.close()
+    >>> dataset
+    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+
+After it's closed, data can no longer be read.
+
+.. code-block:: pycon
+
+    >>> dataset.read_band(1)
+    Traceback (most recent call last):
+      File "<stdin>", line 1, in <module>
+    ValueError: can't read closed raster file
+
+This is the same behavior as Python's ``file``.
+
+.. code-block:: pycon
+
+    >>> f = open('README.rst')
+    >>> f.close()
+    >>> f.read()
+    Traceback (most recent call last):
+      File "<stdin>", line 1, in <module>
+    ValueError: I/O operation on closed file
+
+As Python ``file`` objects can, Rasterio datasets can manage the entry into 
+and exit from runtime contexts created using a ``with`` statement. This 
+ensures that files are closed no matter what exceptions may be raised within
+the the block.
+
+.. code-block:: pycon
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif', 'r') as one:
+    ...     with rasterio.open('tests/data/RGB.byte.tif', 'r') as two:
+                print two
+    ... print one
+    ... print two
+    >>> print one
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+
+Writing data
+------------
+
+Opening a file in writing mode is a little more complicated than opening
+a text file in Python. The dimensions of the raster dataset, the 
+data types, and the specific format must be specified.
+
+.. code-block:: pycon
+
+   >>> with rasterio.oepn
+
+Writing data mostly works as with a Python file. There are a few format-
+specific differences. TODO: details.
+
diff --git a/docs/features.rst b/docs/features.rst
new file mode 100644
index 0000000..ef51d65
--- /dev/null
+++ b/docs/features.rst
@@ -0,0 +1,91 @@
+Features
+========
+
+Rasterio's ``features`` module provides functions to extract shapes of raster
+features and to create new features by "burning" shapes into rasters:
+``shapes()`` and ``rasterize()``. These functions expose GDAL functions in
+a very general way, using iterators over GeoJSON-like Python objects instead of
+GIS layers.
+
+Extracting shapes of raster features
+------------------------------------
+
+Consider the Python logo.
+
+.. image:: https://farm8.staticflickr.com/7018/13547682814_f2e459f7a5_o_d.png
+
+The shapes of the foreground features can be extracted like this:
+
+.. code-block:: python
+
+    import pprint
+    import rasterio
+    from rasterio import features
+
+    with rasterio.open('13547682814_f2e459f7a5_o_d.png') as src:
+        blue = src.read_band(3)
+
+    mask = blue != 255
+    shapes = features.shapes(blue, mask=mask)
+    pprint.pprint(next(shapes))
+
+    # Output
+    # pprint.pprint(next(shapes))
+    # ({'coordinates': [[(71.0, 6.0),
+    #                    (71.0, 7.0),
+    #                    (72.0, 7.0),
+    #                    (72.0, 6.0),
+    #                    (71.0, 6.0)]],
+    #   'type': 'Polygon'},
+    # 253)
+
+The shapes iterator yields ``geometry, value`` pairs. The second item is the
+value of the raster feature corresponding to the shape and the first is its
+geometry.  The coordinates of the geometries in this case are in pixel units
+with origin at the upper left of the image. If the source dataset was
+georeferenced, you would get similarly georeferenced geometries like this:
+
+.. code-block:: python
+
+    shapes = features.shapes(blue, mask=mask, transform=src.transform)
+
+Burning shapes into a raster
+----------------------------
+
+To go the other direction, use ``rasterize()`` to burn values into the pixels
+intersecting with geometries.
+
+.. code-block:: python
+
+    image = features.rasterize(
+                ((g, 255) for g, v in shapes),
+                out_shape=src.shape)
+
+Again, to burn in georeferenced shapes, pass an appropriate transform for the
+image to be created.
+
+.. code-block:: python
+
+    image = features.rasterize(
+                ((g, 255) for g, v in shapes),
+                out_shape=src.shape,
+                transform=src.transform)
+
+The values for the input shapes are replaced with ``255`` in a generator
+expression. The resulting image, written to disk like this,
+
+.. code-block:: python
+
+    with rasterio.open(
+            '/tmp/rasterized-results.tif', 'w', 
+            driver='GTiff', 
+            dtype=rasterio.uint8, 
+            count=1, 
+            width=src.width, 
+            height=src.height) as dst:
+        dst.write_band(1, image)
+
+has a black background and white foreground features.
+
+.. image:: https://farm4.staticflickr.com/3728/13547425455_79bdb5eaeb_o_d.png
+
diff --git a/docs/georeferencing.rst b/docs/georeferencing.rst
new file mode 100644
index 0000000..f8c2321
--- /dev/null
+++ b/docs/georeferencing.rst
@@ -0,0 +1,81 @@
+Georeferencing
+==============
+
+There are two parts to the georeferencing of raster datasets: the definition
+of the local, regional, or global system in which a raster's pixels are
+located; and the parameters by which pixel coordinates are transformed into
+coordinates in that system.
+
+Coordinate Reference System
+---------------------------
+
+The coordinate reference system of a dataset is accessed from its ``crs``
+attribute. Type ``rio insp tests/data/RGB.byte.tif`` from the 
+Rasterio distribution root to see.
+
+.. code-block:: pycon
+
+    Rasterio 0.9 Interactive Inspector (Python 3.4.1)
+    Type "src.meta", "src.read_band(1)", or "help(src)" for more information.
+    >>> src
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    >>> src.crs
+    {'init': 'epsg:32618'}
+
+Rasterio follows pyproj and uses PROJ.4 syntax in dict form as its native
+CRS syntax. If you want a WKT representation of the CRS, see the ``crs_wkt``
+attribute.
+
+.. code-block:: pycon
+
+    >>> src.crs_wkt
+    'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["Unknown datum based upon the WGS 84 ellipsoid",DATUM["Not_specified_based_on_WGS_84_spheroid",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing [...]
+
+When opening a new file for writing, you may also use a CRS string as an
+argument.
+
+.. code-block:: pycon
+
+   >>> with rasterio.open('/tmp/foo.tif', 'w', crs='EPSG:3857', ...) as dst:
+   ...     # write data to this Web Mercator projection dataset.
+
+Coordinate Transformation
+-------------------------
+
+A dataset's pixel coordinate system has its orgin at the "upper left" (imagine
+it displayed on your screen). Column index increases to the right, and row 
+index increases downward. The mapping of these coordinates to "world"
+coordinates in the dataset's reference system is done with an affine
+transformation matrix.
+
+.. code-block:: pycon
+
+    >>> src.affine
+    Affine(300.0379266750948, 0.0, 101985.0,
+           0.0, -300.041782729805, 2826915.0)
+
+The ``Affine`` object is a named tuple with elements ``a, b, c, d, e, f``
+corresponding to the elements in the matrix equation below, in which 
+a pixel's image coordinates are ``x, y`` and its world coordinates are
+``x', y'``.::
+
+    | x' |   | a b c | | x |
+    | y' | = | d e f | | y |
+    | 1  |   | 0 0 1 | | 1 |
+
+The ``Affine`` class has a number of useful properties and methods
+described at https://github.com/sgillies/affine.
+
+The ``affine`` attribute is new. Previous versions of Rasterio had only a
+``transform`` attribute. As explained in the warning below, Rasterio is in
+a transitional phase.
+
+.. code-block:: pycon
+
+    >>> src.transform
+    /usr/local/Cellar/python3/3.4.1/Frameworks/Python.framework/Versions/3.4/lib/python3.4/code.py:90: FutureWarning: The value of this property will change in version 1.0. Please see https://github.com/mapbox/rasterio/issues/86 for details.
+    [101985.0, 300.0379266750948, 0.0, 2826915.0, 0.0, -300.041782729805]
+
+In Rasterio 1.0, the value of a  ``transform`` attribute will be an instance
+of ``Affine`` and the ``affine`` attribute will remain as an alias.
+
diff --git a/docs/masks.rst b/docs/masks.rst
new file mode 100644
index 0000000..10a6239
--- /dev/null
+++ b/docs/masks.rst
@@ -0,0 +1,122 @@
+Masks
+=====
+
+Reading masks
+-------------
+
+There are a few different ways for raster datasets to carry valid data masks.
+Rasterio subscribes to GDAL's abstract mask band interface, so although the
+module's main test dataset has no mask band, GDAL will build one based upon
+its declared nodata value of 0.
+
+.. code-block:: python
+    
+    import rasterio
+
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        mask = src.read_mask()
+        print mask.any()
+        count = mask.shape[0] * mask.shape[1]
+        print float((mask > 0).sum())/count
+        print float((mask == 0).sum())/count
+
+Some of the elements of the mask evaluate to ``True``, meaning that there is some
+valid data. Just over 2/3 of the dataset's pixels (use of sum being a neat trick for
+computing the number of pixels in a selection) have valid data.
+
+.. code-block:: console
+
+    True
+    0.673974976142
+    0.326025023858
+
+Writing masks
+-------------
+
+Writing a mask is just as straightforward: pass an ndarray with ``True`` (or values
+that evaluate to ``True`` to indicate valid data and ``False`` to indicate no data
+to ``write_mask()``.
+
+.. code-block:: python
+
+    import os
+    import shutil
+    import tempfile
+
+    import numpy
+    import rasterio
+
+    tempdir = tempfile.mkdtemp()
+
+    with rasterio.open(
+            os.path.join(tempdir, 'example.tif'), 
+            'w', 
+            driver='GTiff', 
+            count=1, 
+            dtype=rasterio.uint8, 
+            width=10, 
+            height=10) as dst:
+        
+        dst.write_band(1, numpy.ones(dst.shape, dtype=rasterio.uint8))
+
+        mask = numpy.zeros(dst.shape, rasterio.uint8)
+        mask[5:,5:] = 255
+        dst.write_mask(mask)
+
+    print os.listdir(tempdir)
+    shutil.rmtree(tempdir)
+
+The code above masks out all of the file except the lower right quadrant and 
+writes a file with a sidecar TIFF to hold the mask.
+
+.. code-block:: console
+
+    ['example.tif', 'example.tif.msk']
+
+To use an internal TIFF mask, use the ``drivers()`` option shown below:
+
+.. code-block:: python
+
+    tempdir = tempfile.mkdtemp()
+    tiffname = os.path.join(tempdir, 'example.tif')
+
+    with rasterio.drivers(GDAL_TIFF_INTERNAL_MASK=True):
+
+        with rasterio.open(
+                tiffname,
+                'w', 
+                driver='GTiff', 
+                count=1, 
+                dtype=rasterio.uint8, 
+                width=10, 
+                height=10) as dst:
+            
+            dst.write_band(1, numpy.ones(dst.shape, dtype=rasterio.uint8))
+
+            mask = numpy.zeros(dst.shape, rasterio.uint8)
+            mask[5:,5:] = 255
+            dst.write_mask(mask)
+
+    print os.listdir(tempdir)
+    print subprocess.check_output(['gdalinfo', tiffname])
+
+The output:
+
+.. code-block:: console
+
+    ['example.tif']
+    Driver: GTiff/GeoTIFF
+    Files: /var/folders/jh/w0mgrfqd1t37n0bcqzt16bnc0000gn/T/tmpcnGV_r/example.tif
+    Size is 10, 10
+    Coordinate System is `'
+    Image Structure Metadata:
+      INTERLEAVE=BAND
+    Corner Coordinates:
+    Upper Left  (    0.0,    0.0)
+    Lower Left  (    0.0,   10.0)
+    Upper Right (   10.0,    0.0)
+    Lower Right (   10.0,   10.0)
+    Center      (    5.0,    5.0)
+    Band 1 Block=10x10 Type=Byte, ColorInterp=Gray
+      Mask Flags: PER_DATASET
+
diff --git a/docs/options.rst b/docs/options.rst
new file mode 100644
index 0000000..8d0c270
--- /dev/null
+++ b/docs/options.rst
@@ -0,0 +1,22 @@
+Options
+=======
+
+GDAL's format drivers have many [configuration
+options](https://trac.osgeo.org/gdal/wiki/ConfigOptions) The way to set options
+for rasterio is via ``rasterio.drivers()``. Options set on entering are deleted
+on exit.
+
+.. code-block:: python
+
+    import rasterio
+
+    with rasterio.drivers(GDAL_TIFF_INTERNAL_MASK=True):
+        # GeoTIFFs written here will have internal masks, not the
+        # .msk sidecars.
+        ...
+
+    # Option is gone and the default (False) returns.
+
+Use native Python forms (``True`` and ``False``) for boolean options. Rasterio
+will convert them GDAL's internal forms.
+
diff --git a/docs/reproject.rst b/docs/reproject.rst
new file mode 100644
index 0000000..ad290b4
--- /dev/null
+++ b/docs/reproject.rst
@@ -0,0 +1,64 @@
+Reprojection
+============
+
+Rasterio can map the pixels of a destination raster with an associated
+coordinate reference system and transform to the pixels of a source image with
+a different coordinate reference system and transform. This process is known as
+reprojection.
+
+Rasterio's ``rasterio.warp.reproject()`` is a very geospatial-specific analog
+to SciPy's ``scipy.ndimage.interpolation.geometric_transform()`` [1]_.
+
+The code below reprojects between two arrays, using no pre-existing GIS
+datasets.  ``rasterio.warp.reproject()`` has two positional arguments: source
+and destination.  The remaining keyword arguments parameterize the reprojection
+transform.
+
+.. code-block:: python
+
+    import numpy
+    import rasterio
+    from rasterio import Affine as A
+    from rasterio.warp import reproject, RESAMPLING
+
+    with rasterio.drivers():
+
+        # As source: a 512 x 512 raster centered on 0 degrees E and 0
+        # degrees N, each pixel covering 15".
+        rows, cols = src_shape = (512, 512)
+        d = 1.0/240 # decimal degrees per pixel
+        # The following is equivalent to 
+        # A(d, 0, -cols*d/2, 0, -d, rows*d/2).
+        src_transform = A.translation(-cols*d/2, rows*d/2) * A.scale(d, -d)
+        src_crs = {'init': 'EPSG:4326'}
+        source = numpy.ones(src_shape, numpy.uint8)*255
+
+        # Destination: a 1024 x 1024 dataset in Web Mercator (EPSG:3857)
+        # with origin at 0.0, 0.0.
+        dst_shape = (1024, 1024)
+        dst_transform = [-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0]
+        dst_crs = {'init': 'EPSG:3857'}
+        destination = numpy.zeros(dst_shape, numpy.uint8)
+
+        reproject(
+            source, 
+            destination, 
+            src_transform=src_transform,
+            src_crs=src_crs,
+            dst_transform=dst_transform,
+            dst_crs=dst_crs,
+            resampling=RESAMPLING.nearest)
+
+        # Assert that the destination is only partly filled.
+        assert destination.any()
+        assert not destination.all()
+
+See `examples/reproject.py <https://github.com/mapbox/rasterio/blob/master/examples/reproject.py>`__ for code that writes the destination array to a GeoTIFF file. I've 
+uploaded the resulting file to a Mapbox map to demonstrate that the reprojection is
+correct: https://a.tiles.mapbox.com/v3/sgillies.hfek2oko/page.html?secure=1#6/0.000/0.033
+
+References
+----------
+
+.. [1] http://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.interpolation.geometric_transform.html#scipy.ndimage.interpolation.geometric_transform
+
diff --git a/docs/tags.rst b/docs/tags.rst
new file mode 100644
index 0000000..e08f6cd
--- /dev/null
+++ b/docs/tags.rst
@@ -0,0 +1,90 @@
+Tagging datasets and bands
+==========================
+
+GDAL's `data model <http://www.gdal.org/gdal_datamodel.html>`__ includes
+collections of key, value pairs for major classes. In that model, these are
+"metadata", but since they don't have to be just for metadata, these key, value
+pairs are called "tags" in rasterio.
+
+Reading tags
+------------
+
+I'm going to use the rasterio interactive inspector in these examples below.
+
+.. code-block:: console
+
+    $ rasterio.insp tests/data/RGB.byte.tif
+    Rasterio 0.6 Interactive Inspector (Python 2.7.5)
+    Type "src.name", "src.read_band(1)", or "help(src)" for more information.
+    >>> 
+
+Tags belong to namespaces. To get a copy of a dataset's tags from the default
+namespace, just call ``tags()`` with no arguments.
+
+.. code-block:: pycon
+
+    >>>src.tags()
+    {u'AREA_OR_POINT': u'Area'}
+
+A dataset's bands may have tags, too. Here are the tags from the default namespace
+for the first band, accessed using the positional band index argument of ``tags()``.
+
+.. code-block:: pycon
+
+    >>> src.tags(1)
+    {u'STATISTICS_MEAN': u'29.947726688477', u'STATISTICS_MINIMUM': u'0', u'STATISTICS_MAXIMUM': u'255', u'STATISTICS_STDDEV': u'52.340921626611'}
+
+These are the tags that came with the sample data I'm using to test rasterio. In
+practice, maintaining stats in the tags can be unreliable as there is no automatic
+update of the tags when the band's image data changes.
+
+The 3 standard, non-default GDAL tag namespaces are 'SUBDATASETS', 'IMAGE_STRUCTURE', 
+and 'RPC'. You can get the tags from these namespaces using the `ns` keyword of
+``tags()``.
+
+.. code-block:: pycon
+
+    >>> src.tags(ns='IMAGE_STRUCTURE')
+    {u'INTERLEAVE': u'PIXEL'}
+    >>> src.tags(ns='SUBDATASETS')
+    {}
+    >>> src.tags(ns='RPC')
+    {}
+
+Writing tags
+------------
+
+You can add new tags to a dataset or band, in the default or another namespace,
+using the ``update_tags()`` method. Unicode tag values, too, at least for TIFF
+files.
+
+.. code-block:: python
+    
+    import rasterio
+
+    with rasterio.open(
+            '/tmp/test.tif', 
+            'w', 
+            driver='GTiff', 
+            count=1, 
+            dtype=rasterio.uint8, 
+            width=10, 
+            height=10) as dst:
+
+        dst.update_tags(a='1', b='2')
+        dst.update_tags(1, c=3)
+        with pytest.raises(ValueError):
+            dst.update_tags(4, d=4)
+        
+        # True
+        assert dst.tags() == {'a': '1', 'b': '2'}
+        # True
+        assert dst.tags(1) == {'c': '3' }
+        
+        dst.update_tags(ns='rasterio_testing', rus=u'другая строка')
+        # True
+        assert dst.tags(ns='rasterio_testing') == {'rus': u'другая строка'}
+
+As with image data, tags aren't written to the file on disk until the dataset
+is closed.
+
diff --git a/docs/windowed-rw.rst b/docs/windowed-rw.rst
new file mode 100644
index 0000000..d91f5a9
--- /dev/null
+++ b/docs/windowed-rw.rst
@@ -0,0 +1,231 @@
+Windowed reading and writing
+============================
+
+Beginning in rasterio 0.3, you can read and write "windows" of raster files.
+This feature allows you to operate on rasters that are larger than your
+computers RAM or process chunks of very large rasters in parallel.
+
+Windows
+-------
+
+A window is a view onto a rectangular subset of a raster dataset and is
+described in rasterio by a pair of range tuples.
+
+.. code-block:: python
+
+    ((row_start, row_stop), (col_start, col_stop))
+
+The first pair contains the indexes of the raster rows at which the window
+starts and stops. The second contains the indexes of the raster columns at
+which the window starts and stops. For example,
+
+.. code-block:: python
+
+    ((0, 4), (0, 4))
+
+Specifies a 4 x 4 window at the upper left corner of a raster dataset and
+
+.. code-block:: python
+
+    ((10, 20), (10, 20))
+
+specifies a 10 x 10 window with origin at row 10 and column 10. Use of `None`
+for a range value indicates either 0 (in the start position) or the full raster
+height or width (in the stop position). The window tuple
+
+.. code-block:: python
+
+    ((None, 4), (None, 4))
+
+also specifies a 4 x 4 window at the upper left corner of the raster and
+
+.. code-block:: python
+
+    ((4, None), (4, None))
+
+specifies a rectangular subset with upper left at row 4, column 4 and
+extending to the lower right corner of the raster dataset.
+
+Using window tuples should feel like using Python's range() and slice()
+functions. Range() selects a range of numbers from the sequence of all integers
+and slice() produces a object that can be used in slicing expressions.
+
+.. code-block:: pycon
+
+    >>> range(10, 20)
+    [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+    >>> range(10, 20)[slice(4, None)]
+    [14, 15, 16, 17, 18, 19]
+
+Reading
+-------
+
+Here is an example of reading a 100 row x 100 column subset of the rasterio
+test file.
+
+.. code-block:: pycon
+
+    >>> import rasterio
+    >>> with rasterio.open('tests/data/RGB.byte.tif') as src:
+    ...     w = src.read_band(1, window=((0, 100), (0, 100)))
+    ...
+    >>> print(w.shape)
+    (100, 100)
+
+Writing
+-------
+
+Writing works similarly. The following creates a blank 500 column x 300 row
+GeoTIFF and plops 37500 pixels with value 127 into a window 30 pixels down from
+and 50 pixels to the right of the upper left corner of the GeoTIFF.
+
+.. code-block:: python
+
+    image = numpy.ones((150, 250), dtype=rasterio.ubyte) * 127
+    
+    with rasterio.open(
+            '/tmp/example.tif', 'w',
+            driver='GTiff', width=500, height=300, count=1,
+            dtype=image.dtype) as dst:
+        dst.write_band(1, image, window=((30, 180), (50, 300)))
+    
+The result:
+
+.. image:: http://farm6.staticflickr.com/5503/11378078386_cbe2fde02e_o_d.png
+   :width: 500
+   :height: 300
+
+Decimation
+----------
+
+If the write window is smaller than the data, the data will be decimated.
+Below, the window is scaled to one third of the source image.
+
+.. code-block:: python
+
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        b, g, r = (src.read_band(k) for k in (1, 2, 3))
+    
+    write_window = (30, 269), (50, 313)
+    
+    with rasterio.open(
+            '/tmp/example.tif', 'w',
+            driver='GTiff', width=500, height=300, count=3,
+            dtype=r.dtype) as dst:
+        for k, arr in [(1, b), (2, g), (3, r)]:
+            dst.write_band(k, arr, window=write_window)
+
+And the result:
+
+.. image:: http://farm4.staticflickr.com/3804/11378361126_c034743079_o_d.png
+   :width: 500
+   :height: 300
+
+Advanced windows
+----------------
+
+Since windows are like slices, you can also use negative numbers in rasterio
+windows.
+
+.. code-block:: python
+
+    ((-4, None), (-4, None))
+
+specifies a 4 x 4 rectangular subset with upper left at 4 rows to the left of
+and 4 columns above the lower right corner of the dataset and extending to the
+lower right corner of the dataset.
+
+Below is an example of reading a raster subset and then writing it into a 
+larger subset that is defined relative to the lower right corner of the
+destination dataset.
+
+.. code-block:: python
+
+    read_window = (350, 410), (350, 450)
+    
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        b, g, r = (src.read_band(k, window=read_window) for k in (1, 2, 3))
+    
+    write_window = (-240, None), (-400, None)
+    
+    with rasterio.open(
+            '/tmp/example2.tif', 'w',
+            driver='GTiff', width=500, height=300, count=3,
+            dtype=r.dtype) as dst:
+        for k, arr in [(1, b), (2, g), (3, r)]:
+            dst.write_band(k, arr, window=write_window)
+
+This example also demonstrates decimation.
+
+.. image:: http://farm3.staticflickr.com/2827/11378772013_c8ab540f21_o_d.png
+   :width: 500
+   :height: 300
+
+Blocks
+------
+
+Raster datasets are generally composed of multiple blocks of data and
+windowed reads and writes are most efficient when the windows match the
+dataset's own block structure. When a file is opened to read, the shape
+of blocks for any band can be had from the block_shapes property.
+
+.. code-block:: pycon
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif') as src:
+    ...     for i, shape in enumerate(src.block_shapes, 1):
+    ...         print(i, shape)
+    ...
+    (1, (3, 791))
+    (2, (3, 791))
+    (3, (3, 791))
+
+
+The block windows themselves can be had from the block_windows function.
+
+.. code-block:: pycon
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif') as src:
+    ...     for ji, window in src.block_windows(1):
+    ...         print(ji, window)
+    ...
+    ((0, 0), ((0, 3), (0, 791)))
+    ((1, 0), ((3, 6), (0, 791)))
+    ...
+
+This function returns an iterator that yields a pair of values. The second is
+a window tuple that can be used in calls to read_band or write_band. The first
+is the pair of row and column indexes of this block within all blocks of the
+dataset.
+
+You may read windows of data from a file block-by-block like this.
+
+.. code-block:: pycon
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif') as src:
+    ...     for ji, window in src.block_windows(1):
+    ...         r = src.read_band(1, window=window)
+    ...         print(r.shape)
+    ...         break
+    ...
+    (3, 791)
+
+Well-bred files have identically blocked bands, but GDAL allows otherwise and
+it's a good idea to test this assumption in your code.
+
+.. code-block:: pycon
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif') as src:
+    ...     assert len(set(src.block_shapes)) == 1
+    ...     for ji, window in src.block_windows(1):
+    ...         b, g, r = (src.read_band(k, window=window) for k in (1, 2, 3))
+    ...         print(ji, r.shape, g.shape, b.shape)
+    ...         break
+    ...
+    ((0, 0), (3, 791), (3, 791), (3, 791))
+
+The block_shapes property is a band-ordered list of block shapes and
+`set(src.block_shapes)` gives you the set of unique shapes. Asserting that
+there is only one item in the set is effectively the same as asserting that all
+bands have the same block structure. If they do, you can use the same windows
+for each.
+
diff --git a/examples/async-rasterio.py b/examples/async-rasterio.py
new file mode 100644
index 0000000..4e50b84
--- /dev/null
+++ b/examples/async-rasterio.py
@@ -0,0 +1,100 @@
+"""async-rasterio.py
+
+Operate on a raster dataset window-by-window using asyncio's event loop
+and thread executor.
+
+Simulates a CPU-bound thread situation where multiple threads can improve
+performance.
+"""
+
+import asyncio
+import time
+
+import numpy
+import rasterio
+
+from rasterio._example import compute
+
+def main(infile, outfile, with_threads=False):
+    
+    with rasterio.drivers():
+
+        # Open the source dataset.
+        with rasterio.open(infile) as src:
+
+            # Create a destination dataset based on source params. The
+            # destination will be tiled, and we'll "process" the tiles
+            # concurrently.
+
+            meta = src.meta
+            del meta['transform']
+            meta.update(affine=src.affine)
+            meta.update(blockxsize=256, blockysize=256, tiled='yes')
+            with rasterio.open(outfile, 'w', **meta) as dst:
+
+                loop = asyncio.get_event_loop()
+                
+                # With the exception of the ``yield from`` statement,
+                # process_window() looks like callback-free synchronous
+                # code. With a coroutine, we can keep the read, compute,
+                # and write statements close together for
+                # maintainability. As in the concurrent-cpu-bound.py
+                # example, all of the speedup is provided by
+                # distributing raster computation across multiple
+                # threads. The difference here is that we're submitting
+                # jobs to the thread pool asynchronously.
+
+                @asyncio.coroutine
+                def process_window(window):
+                    
+                    # Read a window of data.
+                    data = src.read(window=window)
+                    
+                    # We run the raster computation in a separate thread
+                    # and pause until the computation finishes, letting
+                    # other coroutines advance.
+                    #
+                    # The _example.compute function modifies no Python
+                    # objects and releases the GIL. It can execute
+                    # concurrently.
+                    result = numpy.zeros(data.shape, dtype=data.dtype)
+                    if with_threads:
+                        yield from loop.run_in_executor(
+                                            None, compute, data, result)
+                    else:
+                        compute(data, result)
+                    
+                    # Write the result.
+                    for i, arr in enumerate(result, 1):
+                        dst.write_band(i, arr, window=window)
+
+                # Queue up the loop's tasks.
+                tasks = [asyncio.Task(process_window(window)) 
+                         for ij, window in dst.block_windows(1)]
+                
+                # Wait for all the tasks to finish, and close.
+                loop.run_until_complete(asyncio.wait(tasks))
+                loop.close()
+
+if __name__ == '__main__':
+
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description="Concurrent raster processing demo")
+    parser.add_argument(
+        'input',
+        metavar='INPUT',
+        help="Input file name")
+    parser.add_argument(
+        'output',
+        metavar='OUTPUT',
+        help="Output file name")
+    parser.add_argument(
+        '--with-workers',
+        action='store_true',
+        help="Run with a pool of worker threads")
+    args = parser.parse_args()
+    
+    main(args.input, args.output, args.with_workers)
+
diff --git a/examples/concurrent-cpu-bound.py b/examples/concurrent-cpu-bound.py
new file mode 100644
index 0000000..1ceeb7c
--- /dev/null
+++ b/examples/concurrent-cpu-bound.py
@@ -0,0 +1,95 @@
+"""concurrent-cpu-bound.py
+
+Operate on a raster dataset window-by-window using a ThreadPoolExecutor.
+
+Simulates a CPU-bound thread situation where multiple threads can improve performance.
+
+With -j 4, the program returns in about 1/4 the time as with -j 1.
+"""
+
+import concurrent.futures
+import multiprocessing
+import time
+
+import numpy
+import rasterio
+from rasterio._example import compute
+
+def main(infile, outfile, num_workers=4):
+
+    with rasterio.drivers():
+
+        # Open the source dataset.
+        with rasterio.open(infile) as src:
+
+            # Create a destination dataset based on source params.
+            # The destination will be tiled, and we'll "process" the tiles
+            # concurrently.
+            meta = src.meta
+            del meta['transform']
+            meta.update(affine=src.affine)
+            meta.update(blockxsize=256, blockysize=256, tiled='yes')
+            with rasterio.open(outfile, 'w', **meta) as dst:
+
+                # Define a generator for data, window pairs.
+                # We use the new read() method here to a 3D array with all
+                # bands, but could also use read_band().
+                def jobs():
+                    for ij, window in dst.block_windows():
+                        data = src.read(window=window)
+                        result = numpy.zeros(data.shape, dtype=data.dtype)
+                        yield data, result, window
+
+                # Submit the jobs to the thread pool executor.
+                with concurrent.futures.ThreadPoolExecutor(
+                        max_workers=num_workers) as executor:
+
+                    # Map the futures returned from executor.submit()
+                    # to their destination windows.
+                    #
+                    # The _example.compute function modifies no Python
+                    # objects and releases the GIL. It can execute
+                    # concurrently.
+                    future_to_window = {
+                        executor.submit(compute, data, res): (res, window)
+                        for data, res, window in jobs()}
+
+                    # As the processing jobs are completed, get the
+                    # results and write the data to the appropriate
+                    # destination window.
+                    for future in concurrent.futures.as_completed(
+                            future_to_window):
+
+                        result, window = future_to_window[future]
+
+                        # Since there's no multiband write() method yet in
+                        # Rasterio, we use write_band for each part of the
+                        # 3D data array.
+                        for i, arr in enumerate(result, 1):
+                            dst.write_band(i, arr, window=window)
+
+
+if __name__ == '__main__':
+
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description="Concurrent raster processing demo")
+    parser.add_argument(
+        'input',
+        metavar='INPUT',
+        help="Input file name")
+    parser.add_argument(
+        'output',
+        metavar='OUTPUT',
+        help="Output file name")
+    parser.add_argument(
+        '-j',
+        metavar='NUM_JOBS',
+        type=int,
+        default=multiprocessing.cpu_count(),
+        help="Number of concurrent jobs")
+    args = parser.parse_args()
+
+    main(args.input, args.output, args.j)
+
diff --git a/examples/decimate.py b/examples/decimate.py
new file mode 100644
index 0000000..12b2d1a
--- /dev/null
+++ b/examples/decimate.py
@@ -0,0 +1,31 @@
+import os.path
+import subprocess
+import tempfile
+
+import rasterio
+
+with rasterio.drivers():
+
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        b, g, r = (src.read_band(k) for k in (1, 2, 3))
+        meta = src.meta
+
+    tmpfilename = os.path.join(tempfile.mkdtemp(), 'decimate.tif')
+
+    meta.update(
+        width=src.width/2,
+        height=src.height/2)
+
+    with rasterio.open(
+            tmpfilename, 'w',
+            **meta
+            ) as dst:
+        for k, a in [(1, b), (2, g), (3, r)]:
+            dst.write_band(k, a)
+
+    outfilename = os.path.join(tempfile.mkdtemp(), 'decimate.jpg')
+
+    rasterio.copy(tmpfilename, outfilename, driver='JPEG', quality='30')
+
+info = subprocess.call(['open', outfilename])
+
diff --git a/examples/features.ipynb b/examples/features.ipynb
new file mode 100644
index 0000000..4c0a8aa
--- /dev/null
+++ b/examples/features.ipynb
@@ -0,0 +1,187 @@
+{
+ "metadata": {
+  "name": "",
+  "signature": "sha256:226eb42f053d4da563e4614eb832e56a383e6b4911e98a702ffb7155200d3c9d"
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+  {
+   "cells": [
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "# Interacting with raster features\n",
+      "\n",
+      "A raster feature is a continguous region of like pixels. Rasterio permits extraction of features into a vector data representation and the reverse operation, \"burning\" vector data into a raster or image."
+     ]
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "## Extracting features"
+     ]
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "## Rasterizing features\n",
+      "\n",
+      "Given a source of GeoJSON-like geometry objects or objects that provide the Python Geo Interface, you can \"burn\" these into a raster dataset."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "from rasterio.transform import Affine"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 14
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "def transform_from_corner(ulx, uly, dx, dy):\n",
+      "    return Affine.translation(ulx, uly)*Affine.scale(dx, -dy)\n",
+      "\n",
+      "print transform_from_corner(bounds[0], bounds[3], 1.0/3600, 1.0/3600).to_gdal()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "output_type": "stream",
+       "stream": "stdout",
+       "text": [
+        "(119.52, 0.0002777777777777778, 0.0, -20.5, 0.0, -0.0002777777777777778)\n"
+       ]
+      }
+     ],
+     "prompt_number": 15
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "from rasterio.features import rasterize\n",
+      "from shapely.geometry import Polygon, mapping\n",
+      "\n",
+      "# image transform\n",
+      "bounds = (119.52, -21.6, 120.90, -20.5)\n",
+      "transform = transform_from_corner(bounds[0], bounds[3], 1.0/3600, 1.0/3600)\n",
+      "\n",
+      "# Make raster image, burn in vector data which lies completely inside the bounding box\n",
+      "poly = Polygon(((120, -21), (120.5, -21), (120.5, -21.2), (120, -21.2)))\n",
+      "output = rasterize([poly], transform=transform, out_shape=(3961, 4969))\n",
+      "print output"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "output_type": "stream",
+       "stream": "stdout",
+       "text": [
+        "[[0 0 0 ..., 0 0 0]\n",
+        " [0 0 0 ..., 0 0 0]\n",
+        " [0 0 0 ..., 0 0 0]\n",
+        " ..., \n",
+        " [0 0 0 ..., 0 0 0]\n",
+        " [0 0 0 ..., 0 0 0]\n",
+        " [0 0 0 ..., 0 0 0]]\n"
+       ]
+      }
+     ],
+     "prompt_number": 16
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "%matplotlib inline"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 17
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "import matplotlib.pyplot as plt"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 18
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "plt.imshow(output)"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 19,
+       "text": [
+        "<matplotlib.image.AxesImage at 0x1245e5550>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAAUIAAAEACAYAAADGPX/7AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAF+5JREFUeJzt3W1oW+fh9/Gvgg2ltCl5McveOQGlsRxXiWqLpcperJDg\nOg/u4rlkt++6W+y0KQwHNncpZe2rNoPV7sYobTdBGR6YFBLnflE7jEW4pXH6tKrUsymr9ieCySDJ\nsmmWZTitW6fx9X+R9NxxE9uJn+T6+n3ggH1JR7ouSL45Rw85PmOMQUTEYmsKPQERkUJTCEXEegqh\niFhPIRQR6ymEImI9hVBErLesIYzH41RWVhIMBnnhhReW86lFRGbkW67PEV6+fJlNmzbx5ptv4jgO\n9913H8eOHeOee+5ZjqcXEZnRsh0Rfvjhh5SXlxMIBCguLubhhx+mt7d3uZ5eRGRGyxbCXC7H+vXr\nvd9d1yWXy [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x10f2f9990>"
+       ]
+      }
+     ],
+     "prompt_number": 19
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "import json\n",
+      "\n",
+      "output = rasterize([json.dumps(mapping(poly))], transform=transform, out_shape=(3961, 4969))\n",
+      "print output"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "output_type": "stream",
+       "stream": "stderr",
+       "text": [
+        "ERROR:rasterio:Geometry '{\"type\": \"Polygon\", \"coordinates\": [[[120.0, -21.0], [120.5, -21.0], [120.5, -21.2], [120.0, -21.2], [120.0, -21.0]]]}' at index 0 with value 255 skipped\n"
+       ]
+      }
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [],
+     "language": "python",
+     "metadata": {},
+     "outputs": []
+    }
+   ],
+   "metadata": {}
+  }
+ ]
+}
\ No newline at end of file
diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb
new file mode 100644
index 0000000..4b5bc8b
--- /dev/null
+++ b/examples/introduction.ipynb
@@ -0,0 +1,393 @@
+{
+ "metadata": {
+  "name": "",
+  "signature": "sha256:5a6908bb26106597e34dd231b1a5f453aaa6e8a3e4c9298d8c3baaf3c3e0c4a1"
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+  {
+   "cells": [
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "# An introduction to Rasterio\n",
+      "\n",
+      "The smallest interesting problems [1] addressed by Rasterio are reading raster data from files as [Numpy](http://www.numpy.org/) arrays and writing such arrays back to files. In between, you can use the world of scientific python software to analyze and process the data. Rasterio also provides a few operations that are described in the next notebooks in this series.\n",
+      "\n",
+      "This notebook demonstrates the basics of reading and writing raster data with Rasterio.\n",
+      "\n",
+      "## Overview of a dataset\n",
+      "\n",
+      "A raster dataset consists of one or more dense (as opposed to sparse) 2-D arrays of scalar values. An RGB TIFF image file is a good example of a raster dataset. It has 3 bands (or channels \u2013 we'll call them bands here) and each has a number of rows (its `height`) and columns (its `width`) and a uniform data type (unsigned 8-bit integers, 64-bit floats, etc). Geospatially referenced datasets will also possess a mapping from image to world coordinates (a `transform`) in a speci [...]
+      "\n",
+      "The Scientific Python community often imports numpy as `np`. Do this and also import rasterio."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "import numpy as np\n",
+      "\n",
+      "import rasterio"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 9
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "Rasterio uses for many of its tests a small 3-band GeoTIFF file named \"RGB.byte.tif\". Open it using the function `rasterio.open()`."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src = rasterio.open('../tests/data/RGB.byte.tif')"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 10
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "This function returns a dataset object. It has many of the same properties as a Python file object."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.name"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 11,
+       "text": [
+        "'../tests/data/RGB.byte.tif'"
+       ]
+      }
+     ],
+     "prompt_number": 11
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.mode"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 12,
+       "text": [
+        "'r'"
+       ]
+      }
+     ],
+     "prompt_number": 12
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.closed"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 13,
+       "text": [
+        "False"
+       ]
+      }
+     ],
+     "prompt_number": 13
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "Raster datasets have additional structure and a description can be had from its `meta` property or individually."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.meta"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 14,
+       "text": [
+        "{'affine': Affine(300.0379266750948, 0.0, 101985.0,\n",
+        "       0.0, -300.041782729805, 2826915.0),\n",
+        " 'count': 3,\n",
+        " 'crs': {'init': u'epsg:32618'},\n",
+        " 'driver': u'GTiff',\n",
+        " 'dtype': 'uint8',\n",
+        " 'height': 718,\n",
+        " 'nodata': 0.0,\n",
+        " 'transform': (101985.0,\n",
+        "  300.0379266750948,\n",
+        "  0.0,\n",
+        "  2826915.0,\n",
+        "  0.0,\n",
+        "  -300.041782729805),\n",
+        " 'width': 791}"
+       ]
+      }
+     ],
+     "prompt_number": 14
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.crs"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 15,
+       "text": [
+        "{'init': u'epsg:32618'}"
+       ]
+      }
+     ],
+     "prompt_number": 15
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "To close an opened dataset, use its `close()` method."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.close()\n",
+      "src.closed"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 16,
+       "text": [
+        "True"
+       ]
+      }
+     ],
+     "prompt_number": 16
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "You can't read from or write to a closed dataset, but you can continue access its properties."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.driver"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 23,
+       "text": [
+        "u'GTiff'"
+       ]
+      }
+     ],
+     "prompt_number": 23
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "## Dataset layout\n",
+      "\n",
+      "Three properties of a Rasterio dataset tell you a lot about it in Numpy terms. The `shape` of a dataset is a `height, width` tuple and is exactly the shape of Numpy arrays that would be read from it. The testing dataset has 718 rows and 791 columns."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.shape"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 26,
+       "text": [
+        "(718, 791)"
+       ]
+      }
+     ],
+     "prompt_number": 26
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "The `count` of bands in the dataset is 3."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.count"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 27,
+       "text": [
+        "3"
+       ]
+      }
+     ],
+     "prompt_number": 27
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "All three of its bands contain 8-bit unsigned integers."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "src.dtypes"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 28,
+       "text": [
+        "['uint8', 'uint8', 'uint8']"
+       ]
+      }
+     ],
+     "prompt_number": 28
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "Numpy concepts are the model here. If you wanted to create a 3-D Numpy array into which the testing data file's bands would fit without any resampling, you would use the following Python code."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "dest = np.empty((src.count,) + src.shape, dtype='uint8')\n",
+      "dest"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 25,
+       "text": [
+        "array([[[0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        ..., \n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0]],\n",
+        "\n",
+        "       [[0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        ..., \n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0]],\n",
+        "\n",
+        "       [[0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        ..., \n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0],\n",
+        "        [0, 0, 0, ..., 0, 0, 0]]], dtype=uint8)"
+       ]
+      }
+     ],
+     "prompt_number": 25
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "## References"
+     ]
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "[1]: Mike Bostock's words from his FOSS4G keynote, 2014-09-10"
+     ]
+    }
+   ],
+   "metadata": {}
+  }
+ ]
+}
\ No newline at end of file
diff --git a/examples/polygonize.py b/examples/polygonize.py
new file mode 100644
index 0000000..ae6e000
--- /dev/null
+++ b/examples/polygonize.py
@@ -0,0 +1,10 @@
+import pprint
+
+import rasterio
+import rasterio._features as ftrz
+
+with rasterio.open('box.png') as src:
+    image = src.read_band(1)
+
+pprint.pprint(
+    list(ftrz.polygonize(image)))
diff --git a/examples/rasterio_polygonize.py b/examples/rasterio_polygonize.py
new file mode 100644
index 0000000..54dd19a
--- /dev/null
+++ b/examples/rasterio_polygonize.py
@@ -0,0 +1,74 @@
+# Emulates GDAL's gdal_polygonize.py
+
+import argparse
+import logging
+import subprocess
+import sys
+
+import fiona
+import numpy as np
+import rasterio
+from rasterio.features import shapes
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.INFO)
+logger = logging.getLogger('rasterio_polygonize')
+
+
+def main(raster_file, vector_file, driver, mask_value):
+    
+    with rasterio.drivers():
+        
+        with rasterio.open(raster_file) as src:
+            image = src.read_band(1)
+        
+        if mask_value is not None:
+            mask = image == mask_value
+        else:
+            mask = None
+        
+        results = (
+            {'properties': {'raster_val': v}, 'geometry': s}
+            for i, (s, v) 
+            in enumerate(
+                shapes(image, mask=mask, transform=src.affine)))
+
+        with fiona.open(
+                vector_file, 'w', 
+                driver=driver,
+                crs=src.crs,
+                schema={'properties': [('raster_val', 'int')],
+                        'geometry': 'Polygon'}) as dst:
+            dst.writerecords(results)
+    
+    return dst.name
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser(
+        description="Writes shapes of raster features to a vector file")
+    parser.add_argument(
+        'input', 
+        metavar='INPUT', 
+        help="Input file name")
+    parser.add_argument(
+        'output', 
+        metavar='OUTPUT',
+        help="Output file name")
+    parser.add_argument(
+        '--output-driver',
+        metavar='OUTPUT DRIVER',
+        help="Output vector driver name")
+    parser.add_argument(
+        '--mask-value',
+        default=None,
+        type=int,
+        metavar='MASK VALUE',
+        help="Value to mask")
+    args = parser.parse_args()
+
+    name = main(args.input, args.output, args.output_driver, args.mask_value)
+    
+    print subprocess.check_output(
+            ['ogrinfo', '-so', args.output, name])
+
diff --git a/examples/rasterize_geometry.py b/examples/rasterize_geometry.py
new file mode 100644
index 0000000..bfbc43e
--- /dev/null
+++ b/examples/rasterize_geometry.py
@@ -0,0 +1,27 @@
+import logging
+import numpy
+import sys
+import rasterio
+from rasterio.features import rasterize
+from rasterio.transform import IDENTITY
+
+logging.basicConfig(stream=sys.stderr, level=logging.INFO)
+logger = logging.getLogger('rasterize_geometry')
+
+
+rows = cols = 10
+geometry = {'type':'Polygon','coordinates':[[(2,2),(2,4.25),(4.25,4.25),(4.25,2),(2,2)]]}
+with rasterio.drivers():
+    result = rasterize([geometry], out_shape=(rows, cols))
+    with rasterio.open(
+            "test.tif",
+            'w',
+            driver='GTiff',
+            width=cols,
+            height=rows,
+            count=1,
+            dtype=numpy.uint8,
+            nodata=0,
+            transform=IDENTITY,
+            crs={'init': "EPSG:4326"}) as out:
+        out.write_band(1, result.astype(numpy.uint8))
diff --git a/examples/reproject.py b/examples/reproject.py
new file mode 100644
index 0000000..9a8a348
--- /dev/null
+++ b/examples/reproject.py
@@ -0,0 +1,62 @@
+import os
+import shutil
+import subprocess
+import tempfile
+
+import numpy
+import rasterio
+from rasterio import Affine as A
+from rasterio.warp import reproject, RESAMPLING
+
+tempdir = '/tmp'
+tiffname = os.path.join(tempdir, 'example.tif')
+
+with rasterio.drivers():
+
+    # Consider a 512 x 512 raster centered on 0 degrees E and 0 degrees N
+    # with each pixel covering 15".
+    rows, cols = src_shape = (512, 512)
+    dpp = 1.0/240 # decimal degrees per pixel
+    # The following is equivalent to 
+    # A(dpp, 0, -cols*dpp/2, 0, -dpp, rows*dpp/2).
+    src_transform = A.translation(-cols*dpp/2, rows*dpp/2) * A.scale(dpp, -dpp)
+    src_crs = {'init': 'EPSG:4326'}
+    source = numpy.ones(src_shape, numpy.uint8)*255
+
+    # Prepare to reproject this rasters to a 1024 x 1024 dataset in
+    # Web Mercator (EPSG:3857) with origin at -8928592, 2999585.
+    dst_shape = (1024, 1024)
+    dst_transform = A.from_gdal(-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0)
+    dst_transform = dst_transform.to_gdal()
+    dst_crs = {'init': 'EPSG:3857'}
+    destination = numpy.zeros(dst_shape, numpy.uint8)
+
+    reproject(
+        source, 
+        destination, 
+        src_transform=src_transform,
+        src_crs=src_crs,
+        dst_transform=dst_transform,
+        dst_crs=dst_crs,
+        resampling=RESAMPLING.nearest)
+
+    # Assert that the destination is only partly filled.
+    assert destination.any()
+    assert not destination.all()
+
+    # Write it out to a file.
+    with rasterio.open(
+            tiffname, 
+            'w',
+            driver='GTiff',
+            width=dst_shape[1],
+            height=dst_shape[0],
+            count=1,
+            dtype=numpy.uint8,
+            nodata=0,
+            transform=dst_transform,
+            crs=dst_crs) as dst:
+        dst.write_band(1, destination)
+
+info = subprocess.call(['open', tiffname])
+
diff --git a/examples/sieve.py b/examples/sieve.py
new file mode 100644
index 0000000..b3aab7f
--- /dev/null
+++ b/examples/sieve.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+# sieve: demonstrate sieving and polygonizing of raster features.
+
+import subprocess
+
+import numpy
+import rasterio
+from rasterio.features import sieve, shapes
+
+
+# Register GDAL and OGR drivers.
+with rasterio.drivers():
+    
+    # Read a raster to be sieved.
+    with rasterio.open('tests/data/shade.tif') as src:
+        shade = src.read_band(1)
+    
+    # Print the number of shapes in the source raster.
+    print("Slope shapes: %d" % len(list(shapes(shade))))
+    
+    # Sieve out features 13 pixels or smaller.
+    sieved = sieve(shade, 13, out=numpy.zeros(src.shape, src.dtypes[0]))
+
+    # Print the number of shapes in the sieved raster.
+    print("Sieved (13) shapes: %d" % len(list(shapes(sieved))))
+
+    # Write out the sieved raster.
+    kwargs = src.meta
+    kwargs['transform'] = kwargs.pop('affine')
+    with rasterio.open('example-sieved.tif', 'w', **kwargs) as dst:
+        dst.write_band(1, sieved)
+
+# Dump out gdalinfo's report card and open (or "eog") the TIFF.
+print(subprocess.check_output(
+    ['gdalinfo', '-stats', 'example-sieved.tif']))
+subprocess.call(['open', 'example-sieved.tif'])
+
diff --git a/examples/total.py b/examples/total.py
new file mode 100644
index 0000000..d95a0fa
--- /dev/null
+++ b/examples/total.py
@@ -0,0 +1,38 @@
+import numpy
+import rasterio
+import subprocess
+
+with rasterio.drivers(CPL_DEBUG=True):
+
+    # Read raster bands directly to Numpy arrays.
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        r, g, b = src.read()
+
+    # Combine arrays using the 'iadd' ufunc. Expecting that the sum will
+    # 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.
+    total = numpy.zeros(r.shape, dtype=rasterio.uint16)
+    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 dtype to uint8, and specify
+    # LZW compression.
+    kwargs = src.meta
+    kwargs.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))
+
+# 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'])
+
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
new file mode 100644
index 0000000..ea03bb6
--- /dev/null
+++ b/rasterio/__init__.py
@@ -0,0 +1,164 @@
+# rasterio
+
+from collections import namedtuple
+import logging
+import os
+import warnings
+
+from rasterio._base import eval_window, window_shape, window_index
+from rasterio._drivers import driver_count, GDALEnv
+import rasterio.dtypes
+from rasterio.dtypes import (
+    bool_, ubyte, uint8, uint16, int16, uint32, int32, float32, float64,
+    complex_)
+from rasterio.five import string_types
+from rasterio.transform import Affine, guard_transform
+
+# Classes in rasterio._io are imported below just before we need them.
+
+__all__ = [
+    'band', 'open', 'drivers', 'copy', 'pad']
+__version__ = "0.15"
+
+log = logging.getLogger('rasterio')
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+log.addHandler(NullHandler())
+
+
+def open(
+        path, mode='r', 
+        driver=None,
+        width=None, height=None,
+        count=None,
+        crs=None, transform=None,
+        dtype=None,
+        nodata=None,
+        **kwargs):
+    """Open file at ``path`` in ``mode`` "r" (read), "r+" (read/write),
+    or "w" (write) and return a ``Reader`` or ``Updater`` object.
+    
+    In write mode, a driver name such as "GTiff" or "JPEG" (see GDAL
+    docs or ``gdal_translate --help`` on the command line), ``width``
+    (number of pixels per line) and ``height`` (number of lines), the
+    ``count`` number of bands in the new file must be specified.
+    Additionally, the data type for bands such as ``rasterio.ubyte`` for
+    8-bit bands or ``rasterio.uint16`` for 16-bit bands must be
+    specified using the ``dtype`` argument.
+
+    A coordinate reference system for raster datasets in write mode can
+    be defined by the ``crs`` argument. It takes Proj4 style mappings
+    like
+    
+      {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84',
+       'no_defs': True}
+
+    An affine transformation that maps ``col,row`` pixel coordinates to
+    ``x,y`` coordinates in the coordinate reference system can be
+    specified using the ``transform`` argument. The value may be either
+    an instance of ``affine.Affine`` or a 6-element sequence of the
+    affine transformation matrix coefficients ``a, b, c, d, e, f``.
+    These coefficients are shown in the figure below.
+
+      | x |   | a  b  c | | c |
+      | y | = | d  e  f | | r |
+      | 1 |   | 0  0  1 | | 1 |
+
+    a: rate of change of X with respect to increasing column, i.e.
+            pixel width
+    b: rotation, 0 if the raster is oriented "north up" 
+    c: X coordinate of the top left corner of the top left pixel 
+    f: Y coordinate of the top left corner of the top left pixel 
+    d: rotation, 0 if the raster is oriented "north up"
+    e: rate of change of Y with respect to increasing row, usually
+            a negative number i.e. -1 * pixel height
+    f: Y coordinate of the top left corner of the top left pixel 
+
+    Finally, additional kwargs are passed to GDAL as driver-specific
+    dataset creation parameters.
+    """
+    if not isinstance(path, string_types):
+        raise TypeError("invalid path: %r" % path)
+    if mode and not isinstance(mode, string_types):
+        raise TypeError("invalid mode: %r" % mode)
+    if driver and not isinstance(driver, string_types):
+        raise TypeError("invalid driver: %r" % driver)
+    if mode in ('r', 'r+'):
+        if not os.path.exists(path):
+            raise IOError("no such file or directory: %r" % path)
+    if transform:
+        transform = guard_transform(transform)
+
+    if mode == 'r':
+        from rasterio._io import RasterReader
+        s = RasterReader(path)
+    elif mode == 'r+':
+        from rasterio._io import writer
+        s = writer(path, mode)
+    elif mode == 'r-':
+        from rasterio._base import DatasetReader
+        s = DatasetReader(path)
+    elif mode == 'w':
+        from rasterio._io import writer
+        s = writer(path, mode, driver=driver,
+                   width=width, height=height, count=count,
+                   crs=crs, transform=transform, dtype=dtype,
+                   nodata=nodata,
+                   **kwargs)
+    else:
+        raise ValueError(
+            "mode string must be one of 'r', 'r+', or 'w', not %s" % mode)
+    s.start()
+    return s
+
+
+def copy(src, dst, **kw):
+    """Copy a source dataset to a new destination with driver specific
+    creation options.
+
+    ``src`` must be an existing file and ``dst`` a valid output file.
+
+    A ``driver`` keyword argument with value like 'GTiff' or 'JPEG' is
+    used to control the output format.
+    
+    This is the one way to create write-once files like JPEGs.
+    """
+    from rasterio._copy import RasterCopier
+    with drivers():
+        return RasterCopier()(src, dst, **kw)
+
+
+def drivers(**kwargs):
+    """Returns a gdal environment with registered drivers."""
+    if driver_count() == 0:
+        log.debug("Creating a chief GDALEnv in drivers()")
+        return GDALEnv(True, **kwargs)
+    else:
+        log.debug("Creating a not-responsible GDALEnv in drivers()")
+        return GDALEnv(False, **kwargs)
+
+
+Band = namedtuple('Band', ['ds', 'bidx', 'dtype', 'shape'])
+
+def band(ds, bidx):
+    """Wraps a dataset and a band index up as a 'Band'"""
+    return Band(
+        ds, 
+        bidx, 
+        set(ds.dtypes).pop(),
+        ds.shape)
+
+
+def pad(array, transform, pad_width, mode=None, **kwargs):
+    """Returns a padded array and shifted affine transform matrix.
+    
+    Array is padded using `numpy.pad()`."""
+    import numpy
+    transform = guard_transform(transform)
+    padded_array = numpy.pad(array, pad_width, mode, **kwargs)
+    padded_trans = list(transform)
+    padded_trans[2] -= pad_width*padded_trans[0]
+    padded_trans[5] -= pad_width*padded_trans[4]
+    return padded_array, Affine(*padded_trans[:6])
+
diff --git a/rasterio/_base.pxd b/rasterio/_base.pxd
new file mode 100644
index 0000000..ad5440b
--- /dev/null
+++ b/rasterio/_base.pxd
@@ -0,0 +1,25 @@
+# Base class.
+
+cdef class DatasetReader:
+    # Read-only access to dataset metadata. No IO!
+    
+    cdef void *_hds
+
+    cdef readonly object name
+    cdef readonly object mode
+    cdef readonly object width, height
+    cdef readonly object shape
+    cdef public object driver
+    cdef public object _count
+    cdef public object _dtypes
+    cdef public object _closed
+    cdef public object _crs
+    cdef public object _crs_wkt
+    cdef public object _transform
+    cdef public object _block_shapes
+    cdef public object _nodatavals
+    cdef public object _read
+    cdef object env
+
+    cdef void *band(self, int bidx)
+
diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx
new file mode 100644
index 0000000..87f696e
--- /dev/null
+++ b/rasterio/_base.pyx
@@ -0,0 +1,611 @@
+# The Numpy-free base classes.
+
+# cython: boundscheck=False
+
+import logging
+import math
+import sys
+import warnings
+
+from libc.stdlib cimport malloc, free
+
+from rasterio cimport _gdal, _ogr
+from rasterio._drivers import driver_count, GDALEnv
+from rasterio._err import cpl_errs
+from rasterio import dtypes
+from rasterio.coords import BoundingBox
+from rasterio.transform import Affine
+from rasterio.enums import ColorInterp
+
+
+log = logging.getLogger('rasterio')
+if 'all' in sys.warnoptions:
+    # show messages in console with: python -W all
+    logging.basicConfig()
+else:
+    # no handler messages shown
+    class NullHandler(logging.Handler):
+        def emit(self, record):
+            pass
+    log.addHandler(NullHandler())
+
+
+cdef class DatasetReader(object):
+
+    def __init__(self, path):
+        self.name = path
+        self.mode = 'r'
+        self._hds = NULL
+        self._count = 0
+        self._closed = True
+        self._dtypes = []
+        self._block_shapes = None
+        self._nodatavals = []
+        self._crs = None
+        self._crs_wkt = None
+        self._read = False
+        self.env = None
+    
+    def __repr__(self):
+        return "<%s RasterReader name='%s' mode='%s'>" % (
+            self.closed and 'closed' or 'open', 
+            self.name,
+            self.mode)
+
+    def start(self):
+        # Is there not a driver manager already?
+        if driver_count() == 0 and not self.env:
+            # create a local manager and enter
+            self.env = GDALEnv(True)
+        else:
+            # create a local manager and enter
+            self.env = GDALEnv(False)
+        self.env.start()
+
+        name_b = self.name.encode('utf-8')
+        cdef const char *fname = name_b
+        with cpl_errs:
+            self._hds = _gdal.GDALOpen(fname, 0)
+        if self._hds == NULL:
+            raise ValueError("Null dataset")
+
+        cdef void *drv
+        cdef const char *drv_name
+        drv = _gdal.GDALGetDatasetDriver(self._hds)
+        drv_name = _gdal.GDALGetDriverShortName(drv)
+        self.driver = drv_name.decode('utf-8')
+
+        self._count = _gdal.GDALGetRasterCount(self._hds)
+        self.width = _gdal.GDALGetRasterXSize(self._hds)
+        self.height = _gdal.GDALGetRasterYSize(self._hds)
+        self.shape = (self.height, self.width)
+
+        self._transform = self.read_transform()
+        self._crs = self.read_crs()
+        self._crs_wkt = self.read_crs_wkt()
+
+        # touch self.meta
+        _ = self.meta
+
+        self._closed = False
+
+    cdef void *band(self, int bidx):
+        if self._hds == NULL:
+            raise ValueError("Null dataset")
+        cdef void *hband = _gdal.GDALGetRasterBand(self._hds, bidx)
+        if hband == NULL:
+            raise ValueError("Null band")
+        return hband
+
+    def read_crs(self):
+        cdef char *proj_c = NULL
+        cdef char *auth_key = NULL
+        cdef char *auth_val = NULL
+        cdef void *osr = NULL
+        if self._hds == NULL:
+            raise ValueError("Null dataset")
+        crs = {}
+        cdef char *wkt = _gdal.GDALGetProjectionRef(self._hds)
+        if wkt is NULL:
+            raise ValueError("Unexpected NULL spatial reference")
+        wkt_b = wkt
+        if len(wkt_b) > 0:
+            osr = _gdal.OSRNewSpatialReference(wkt)
+            if osr == NULL:
+                raise ValueError("Unexpected NULL spatial reference")
+            log.debug("Got coordinate system")
+
+            retval = _gdal.OSRAutoIdentifyEPSG(osr)
+            if retval > 0:
+                log.info("Failed to auto identify EPSG: %d", retval)
+            
+            auth_key = _gdal.OSRGetAuthorityName(osr, NULL)
+            auth_val = _gdal.OSRGetAuthorityCode(osr, NULL)
+
+            if auth_key != NULL and auth_val != NULL:
+                key_b = auth_key
+                key = key_b.decode('utf-8')
+                if key == 'EPSG':
+                    val_b = auth_val
+                    val = val_b.decode('utf-8')
+                    crs['init'] = "epsg:" + val
+            else:
+                _gdal.OSRExportToProj4(osr, &proj_c)
+                if proj_c == NULL:
+                    raise ValueError("Unexpected Null spatial reference")
+                proj_b = proj_c
+                log.debug("Params: %s", proj_b)
+                value = proj_b.decode()
+                value = value.strip()
+                for param in value.split():
+                    kv = param.split("=")
+                    if len(kv) == 2:
+                        k, v = kv
+                        try:
+                            v = float(v)
+                            if v % 1 == 0:
+                                v = int(v)
+                        except ValueError:
+                            # Leave v as a string
+                            pass
+                    elif len(kv) == 1:
+                        k, v = kv[0], True
+                    else:
+                        raise ValueError(
+                            "Unexpected proj parameter %s" % param)
+                    k = k.lstrip("+")
+                    crs[k] = v
+
+            _gdal.CPLFree(proj_c)
+            _gdal.OSRDestroySpatialReference(osr)
+        else:
+            log.debug("GDAL dataset has no projection.")
+        return crs
+
+    def read_crs_wkt(self):
+        cdef char *proj_c = NULL
+        cdef char *key_c = NULL
+        cdef void *osr = NULL
+        cdef char *wkt = NULL
+        if self._hds == NULL:
+            raise ValueError("Null dataset")
+        wkt = _gdal.GDALGetProjectionRef(self._hds)
+        if wkt is NULL:
+            raise ValueError("Unexpected NULL spatial reference")
+        wkt_b = wkt
+        if len(wkt_b) > 0:
+            osr = _gdal.OSRNewSpatialReference(wkt)
+            log.debug("Got coordinate system")
+            if osr != NULL:
+                retval = _gdal.OSRAutoIdentifyEPSG(osr)
+                if retval > 0:
+                    log.info("Failed to auto identify EPSG: %d", retval)
+                _gdal.OSRExportToWkt(osr, &proj_c)
+                if proj_c == NULL:
+                    raise ValueError("Null projection")
+                proj_b = proj_c
+                crs_wkt = proj_b.decode('utf-8')
+                _gdal.CPLFree(proj_c)
+                _gdal.OSRDestroySpatialReference(osr)
+        else:
+            log.debug("GDAL dataset has no projection.")
+            crs_wkt = None
+        return crs_wkt
+
+    def read_transform(self):
+        if self._hds == NULL:
+            raise ValueError("Null dataset")
+        cdef double gt[6]
+        _gdal.GDALGetGeoTransform(self._hds, gt)
+        transform = [0]*6
+        for i in range(6):
+            transform[i] = gt[i]
+        return transform
+
+    def stop(self):
+        if self._hds != NULL:
+            _gdal.GDALFlushCache(self._hds)
+            _gdal.GDALClose(self._hds)
+        if self.env:
+            self.env.stop()
+        self._hds = NULL
+
+    def close(self):
+        self.stop()
+        self._closed = True
+    
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def __dealloc__(self):
+        if self._hds != NULL:
+            _gdal.GDALClose(self._hds)
+
+    @property
+    def closed(self):
+        return self._closed
+
+    @property
+    def count(self):
+        if not self._count:
+            if self._hds == NULL:
+                raise ValueError("Can't read closed raster file")
+            self._count = _gdal.GDALGetRasterCount(self._hds)
+        return self._count
+
+    @property
+    def indexes(self):
+        return list(range(1, self.count+1))
+
+    @property
+    def dtypes(self):
+        """Returns an ordered list of all band data types."""
+        cdef void *hband = NULL
+        if not self._dtypes:
+            if self._hds == NULL:
+                raise ValueError("can't read closed raster file")
+            for i in range(self._count):
+                hband = _gdal.GDALGetRasterBand(self._hds, i+1)
+                self._dtypes.append(
+                    dtypes.dtype_fwd[_gdal.GDALGetRasterDataType(hband)])
+        return self._dtypes
+    
+    @property
+    def block_shapes(self):
+        """Returns an ordered list of block shapes for all bands.
+        
+        Shapes are tuples and have the same ordering as the dataset's
+        shape: (count of image rows, count of image columns).
+        """
+        cdef void *hband = NULL
+        cdef int xsize, ysize
+        if self._block_shapes is None:
+            if self._hds == NULL:
+                raise ValueError("can't read closed raster file")
+            self._block_shapes = []
+            for i in range(self._count):
+                hband = _gdal.GDALGetRasterBand(self._hds, i+1)
+                if hband == NULL:
+                    raise ValueError("Null band")
+                _gdal.GDALGetBlockSize(hband, &xsize, &ysize)
+                self._block_shapes.append((ysize, xsize))
+        return self._block_shapes
+
+    def get_nodatavals(self):
+        cdef void *hband = NULL
+        cdef object val
+        cdef int success
+        if not self._nodatavals:
+            if self._hds == NULL:
+                raise ValueError("can't read closed raster file")
+            for i in range(self._count):
+                hband = _gdal.GDALGetRasterBand(self._hds, i+1)
+                if hband == NULL:
+                    raise ValueError("Null band")
+                val = _gdal.GDALGetRasterNoDataValue(hband, &success)
+                if not success:
+                    val = None
+                self._nodatavals.append(val)
+        return self._nodatavals
+
+    property nodatavals:
+        def __get__(self):
+            return self.get_nodatavals()
+
+    def block_windows(self, bidx=0):
+        """Returns an iterator over a band's block windows and their
+        indexes.
+
+        The positional parameter `bidx` takes the index (starting at 1)
+        of the desired band. Block windows are tuples
+
+            ((row_start, row_stop), (col_start, col_stop))
+
+        For example, ((0, 2), (0, 2)) defines a 2 x 2 block at the upper
+        left corner of the raster dataset.
+
+        This iterator yields blocks "left to right" and "top to bottom"
+        and is similar to Python's enumerate() in that it also returns
+        indexes.
+
+        The primary use of this function is to obtain windows to pass to
+        read_band() for highly efficient access to raster block data.
+        """
+        cdef int i, j
+        block_shapes = self.block_shapes
+        if bidx < 1:
+            if len(set(block_shapes)) > 1:
+                raise ValueError(
+                    "A band index must be provided when band block shapes"
+                    "are inhomogeneous")
+            bidx = 1
+        h, w = block_shapes[bidx-1]
+        d, m = divmod(self.height, h)
+        nrows = d + int(m>0)
+        d, m = divmod(self.width, w)
+        ncols = d + int(m>0)
+        for j in range(nrows):
+            row = j * h
+            height = min(h, self.height - row)
+            for i in range(ncols):
+                col = i * w
+                width = min(w, self.width - col)
+                yield (j, i), ((row, row+height), (col, col+width))
+
+    property bounds:
+        """Returns the lower left and upper right bounds of the dataset
+        in the units of its coordinate reference system.
+        
+        The returned value is a tuple:
+        (lower left x, lower left y, upper right x, upper right y)
+        """
+        def __get__(self):
+            a, b, c, d, e, f, _, _, _ = self.affine
+            return BoundingBox(c, f+e*self.height, c+a*self.width, f)
+    
+    property res:
+        """Returns the (width, height) of pixels in the units of its
+        coordinate reference system."""
+        def __get__(self):
+            a, b, c, d, e, f, _, _, _ = self.affine
+            if b == d == 0:
+                return a, -e
+            else:
+                return math.sqrt(a*a+d*d), math.sqrt(b*b+e*e)
+
+    def ul(self, row, col):
+        """Returns the coordinates (x, y) of the upper left corner of a 
+        pixel at `row` and `col` in the units of the dataset's
+        coordinate reference system.
+        """
+        a, b, c, d, e, f, _, _, _ = self.affine
+        if col < 0:
+            col += self.width
+        if row < 0:
+            row += self.height
+        return c+a*col, f+e*row
+
+    def index(self, x, y):
+        """Returns the (row, col) index of the pixel containing (x, y)."""
+        a, b, c, d, e, f, _, _, _ = self.affine
+        return int(round((y-f)/e)), int(round((x-c)/a))
+
+    def window(self, left, bottom, right, top):
+        """Returns the window corresponding to the world bounding box."""
+        ul = self.index(left, top)
+        lr = self.index(right, bottom)
+        if ul[0] < 0 or ul[1] < 0 or lr[0] > self.height or lr[1] > self.width:
+            raise ValueError("Bounding box overflows the dataset extents")
+        else:
+            return tuple(zip(ul, lr))
+
+    @property
+    def meta(self):
+        m = {
+            'driver': self.driver,
+            'dtype': self.dtypes[0],
+            'nodata': self.nodatavals[0],
+            'width': self.width,
+            'height': self.height,
+            'count': self.count,
+            'crs': self.crs,
+            'transform': self.affine.to_gdal(),
+            'affine': self.affine }
+        self._read = True
+        return m
+
+    
+    def get_crs(self):
+        # _read tells us that the CRS was read before and really is
+        # None.
+        if not self._read and self._crs is None:
+            self._crs = self.read_crs()
+        return self._crs
+
+    property crs:
+        """A mapping of PROJ.4 coordinate reference system params.
+        """
+        def __get__(self):
+            return self.get_crs()
+
+    property crs_wkt:
+        """An OGC WKT string representation of the coordinate reference
+        system.
+        """
+        def __get__(self):
+            if not self._read and self._crs_wkt is None:
+                self._crs = self.read_crs_wkt()
+            return self._crs_wkt
+
+    def get_transform(self):
+        """Returns a GDAL geotransform in its native form."""
+        if not self._read and self._transform is None:
+            self._transform = self.read_transform()
+        return self._transform
+
+    property transform:
+        """Coefficients of the affine transformation that maps col,row
+        pixel coordinates to x,y coordinates in the specified crs. The
+        coefficients of the augmented matrix are shown below.
+        
+          | x |   | a  b  c | | r |
+          | y | = | d  e  f | | c |
+          | 1 |   | 0  0  1 | | 1 |
+        
+        In Rasterio versions before 1.0 the value of this property
+        is a list of coefficients ``[c, a, b, f, d, e]``. This form
+        is *deprecated* beginning in 0.9 and in version 1.0 this 
+        property will be replaced by an instance of ``affine.Affine``,
+        which is a namedtuple with coefficients in the order
+        ``(a, b, c, d, e, f)``.
+
+        Please see https://github.com/mapbox/rasterio/issues/86
+        for more details.
+        """
+        def __get__(self):
+            warnings.warn(
+                    "The value of this property will change in version 1.0. "
+                    "Please see https://github.com/mapbox/rasterio/issues/86 "
+                    "for details.",
+                    FutureWarning,
+                    stacklevel=2)
+            return self.get_transform()
+
+    property affine:
+        """An instance of ``affine.Affine``. This property is a
+        transitional feature: see the docstring of ``transform``
+        (above) for more details.
+        """
+        def __get__(self):
+            return Affine.from_gdal(*self.get_transform())
+
+    def tags(self, bidx=0, ns=None):
+        """Returns a dict containing copies of the dataset or band's
+        tags.
+
+        Tags are pairs of key and value strings. Tags belong to
+        namespaces.  The standard namespaces are: default (None) and
+        'IMAGE_STRUCTURE'.  Applications can create their own additional
+        namespaces.
+
+        The optional bidx argument can be used to select the tags of
+        a specific band. The optional ns argument can be used to select
+        a namespace other than the default.
+        """
+        cdef char *item_c
+        cdef void *hobj
+        cdef const char *domain_c
+        cdef char **papszStrList
+        if self._hds == NULL:
+            raise ValueError("can't read closed raster file")
+        if bidx > 0:
+            if bidx not in self.indexes:
+                raise ValueError("Invalid band index")
+            hobj = _gdal.GDALGetRasterBand(self._hds, bidx)
+            if hobj == NULL:
+                raise ValueError("NULL band")
+        else:
+            hobj = self._hds
+        if ns:
+            domain_b = ns.encode('utf-8')
+            domain_c = domain_b
+        else:
+            domain_c = NULL
+        papszStrList = _gdal.GDALGetMetadata(hobj, domain_c)
+        num_items = _gdal.CSLCount(papszStrList)
+        retval = {}
+        for i in range(num_items):
+            item_c = papszStrList[i]
+            item_b = item_c
+            item = item_b.decode('utf-8')
+            key, value = item.split('=')
+            retval[key] = value
+        return retval
+    
+    def colorinterp(self, bidx):
+        """Returns the color interpretation for a band or None."""
+        cdef void *hBand
+        
+        if self._hds == NULL:
+          raise ValueError("can't read closed raster file")
+        if bidx > 0:
+            if bidx not in self.indexes:
+                raise ValueError("Invalid band index")
+            hBand = _gdal.GDALGetRasterBand(self._hds, bidx)
+            if hBand == NULL:
+                raise ValueError("NULL band")
+        value = _gdal.GDALGetRasterColorInterpretation(hBand)
+        return ColorInterp(value)
+    
+    def colormap(self, bidx):
+        """Returns a dict containing the colormap for a band or None."""
+        cdef void *hBand
+        cdef void *hTable
+        cdef int i
+        cdef _gdal.GDALColorEntry *color
+        if self._hds == NULL:
+            raise ValueError("can't read closed raster file")
+        if bidx > 0:
+            if bidx not in self.indexes:
+                raise ValueError("Invalid band index")
+            hBand = _gdal.GDALGetRasterBand(self._hds, bidx)
+            if hBand == NULL:
+                raise ValueError("NULL band")
+        hTable = _gdal.GDALGetRasterColorTable(hBand)
+        if hTable == NULL:
+            raise ValueError("NULL color table")
+        retval = {}
+
+        for i in range(_gdal.GDALGetColorEntryCount(hTable)):
+            color = _gdal.GDALGetColorEntry(hTable, i)
+            if color == NULL:
+                log.warn("NULL color at %d, skipping", i)
+                continue
+            log.info("Color: (%d, %d, %d, %d)", color.c1, color.c2, color.c3, color.c4)
+            retval[i] = (color.c1, color.c2, color.c3, color.c4)
+        return retval
+
+    @property
+    def kwds(self):
+        return self.tags(ns='rio_creation_kwds')
+
+# Window utils
+# A window is a 2D ndarray indexer in the form of a tuple:
+# ((row_start, row_stop), (col_start, col_stop))
+
+cpdef eval_window(object window, int height, int width):
+    """Evaluates a window tuple that might contain negative values
+    in the context of a raster height and width."""
+    cdef int r_start, r_stop, c_start, c_stop
+    try:
+        r, c = window
+        assert len(r) == 2
+        assert len(c) == 2
+    except (ValueError, TypeError, AssertionError):
+        raise ValueError("invalid window structure; expecting "
+                         "((row_start, row_stop), (col_start, col_stop))")
+    r_start = r[0] or 0
+    if r_start < 0:
+        if height < 0:
+            raise ValueError("invalid height: %d" % height)
+        r_start += height
+    r_stop = r[1] or height
+    if r_stop < 0:
+        if height < 0:
+            raise ValueError("invalid height: %d" % height)
+        r_stop += height
+    if not r_stop >= r_start:
+        raise ValueError(
+            "invalid window: row range (%d, %d)" % (r_start, r_stop))
+    c_start = c[0] or 0
+    if c_start < 0:
+        if width < 0:
+            raise ValueError("invalid width: %d" % width)
+        c_start += width
+    c_stop = c[1] or width
+    if c_stop < 0:
+        if width < 0:
+            raise ValueError("invalid width: %d" % width)
+        c_stop += width
+    if not c_stop >= c_start:
+        raise ValueError(
+            "invalid window: col range (%d, %d)" % (c_start, c_stop))
+    return (r_start, r_stop), (c_start, c_stop)
+
+def window_shape(window, height=-1, width=-1):
+    """Returns shape of a window.
+
+    height and width arguments are optional if there are no negative
+    values in the window.
+    """
+    (a, b), (c, d) = eval_window(window, height, width)
+    return b-a, d-c
+
+def window_index(window):
+    return tuple(slice(*w) for w in window)
+
+def tastes_like_gdal(t):
+    return t[2] == t[4] == 0.0 and t[1] > 0 and t[5] < 0
diff --git a/rasterio/_copy.pyx b/rasterio/_copy.pyx
new file mode 100644
index 0000000..fab8593
--- /dev/null
+++ b/rasterio/_copy.pyx
@@ -0,0 +1,55 @@
+import logging
+import os
+import os.path
+
+from rasterio cimport _gdal
+
+
+log = logging.getLogger('rasterio')
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+log.addHandler(NullHandler())
+
+
+cdef class RasterCopier:
+
+    def __call__(self, src, dst, **kw):
+        cdef char **options = NULL
+        src_b = src.encode('utf-8')
+        cdef const char *src_c = src_b
+        dst_b = dst.encode('utf-8')
+        cdef const char *dst_c = dst_b
+        cdef void *src_ds = _gdal.GDALOpen(src_c, 0)
+        if src_ds == NULL:
+            raise ValueError("NULL source dataset")
+        driver = kw.pop('driver', 'GTiff')
+        driver_b = driver.encode('utf-8')
+        cdef const char *driver_c = driver_b
+        cdef void *drv = _gdal.GDALGetDriverByName(driver_c)
+        if drv == NULL:
+            raise ValueError("NULL driver")
+        strictness = 0
+        if kw.pop('strict', None):
+            strictness = 1
+
+        # Creation options
+        for k, v in kw.items():
+            k, v = k.upper(), v.upper()
+            key_b = k.encode('utf-8')
+            val_b = v.encode('utf-8')
+            key_c = key_b
+            val_c = val_b
+            options = _gdal.CSLSetNameValue(options, key_c, val_c)
+            log.debug("Option: %r\n", (k, v))
+
+        cdef void *dst_ds = _gdal.GDALCreateCopy(
+            drv, dst_c, src_ds, strictness, NULL, NULL, NULL)
+        if dst_ds == NULL:
+            raise ValueError("NULL destination dataset")
+        _gdal.GDALClose(src_ds)
+        _gdal.GDALClose(dst_ds)
+
+        if options:
+            _gdal.CSLDestroy(options)
+
diff --git a/rasterio/_drivers.pyx b/rasterio/_drivers.pyx
new file mode 100644
index 0000000..26f6689
--- /dev/null
+++ b/rasterio/_drivers.pyx
@@ -0,0 +1,120 @@
+# The GDAL and OGR driver registry.
+# GDAL driver management.
+
+import logging
+
+from rasterio.five import string_types
+
+cdef extern from "cpl_conv.h":
+    void    CPLFree (void *ptr)
+    void    CPLSetThreadLocalConfigOption (char *key, char *val)
+    const char * CPLGetConfigOption ( const char *key, const char *default)
+
+cdef extern from "cpl_error.h":
+    void CPLSetErrorHandler (void *handler)
+
+cdef extern from "gdal.h":
+    void GDALAllRegister()
+    void GDALDestroyDriverManager()
+    int GDALGetDriverCount()
+    void * GDALGetDriver(int i)
+    const char * GDALGetDriverShortName(void *driver)
+    const char * GDALGetDriverLongName(void *driver)
+
+cdef extern from "ogr_api.h":
+    void OGRRegisterAll()
+    void OGRCleanupAll()
+    int OGRGetDriverCount()
+
+log = logging.getLogger('GDAL')
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+log.addHandler(NullHandler())
+
+level_map = {
+    0: 0,
+    1: logging.DEBUG,
+    2: logging.WARNING,
+    3: logging.ERROR,
+    4: logging.CRITICAL }
+
+code_map = {
+    0: 'CPLE_None',
+    1: 'CPLE_AppDefined',
+    2: 'CPLE_OutOfMemory',
+    3: 'CPLE_FileIO',
+    4: 'CPLE_OpenFailed',
+    5: 'CPLE_IllegalArg',
+    6: 'CPLE_NotSupported',
+    7: 'CPLE_AssertionFailed',
+    8: 'CPLE_NoWriteAccess',
+    9: 'CPLE_UserInterrupt',
+    10: 'CPLE_ObjectNull'
+}
+
+cdef void * errorHandler(int eErrClass, int err_no, char *msg):
+    log.log(level_map[eErrClass], "%s in %s", code_map[err_no], msg)
+
+def driver_count():
+    return GDALGetDriverCount() + OGRGetDriverCount()
+
+
+cdef class GDALEnv(object):
+
+    cdef object is_chef
+    cdef public object options
+
+    def __init__(self, is_chef=True, **options):
+        self.is_chef = is_chef
+        self.options = options.copy()
+
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
+        self.stop()
+
+    def start(self):
+        cdef const char *key_c
+        cdef const char *val_c
+        GDALAllRegister()
+        OGRRegisterAll()
+        CPLSetErrorHandler(<void *>errorHandler)
+        if driver_count() == 0:
+            raise ValueError("Drivers not registered")
+        for key, val in self.options.items():
+            key_b = key.upper().encode('utf-8')
+            key_c = key_b
+            if isinstance(val, string_types):
+                val_b = val.encode('utf-8')
+            else:
+                val_b = ('ON' if val else 'OFF').encode('utf-8')
+            val_c = val_b
+            CPLSetThreadLocalConfigOption(key_c, val_c)
+            log.debug("Option %s=%s", key, CPLGetConfigOption(key_c, NULL))
+        return self
+
+    def stop(self):
+        cdef const char *key_c
+        for key in self.options:
+            key_b = key.upper().encode('utf-8')
+            key_c = key_b
+            CPLSetThreadLocalConfigOption(key_c, NULL)
+        CPLSetErrorHandler(NULL)
+
+    def drivers(self):
+        cdef void *drv = NULL
+        cdef char *key = NULL
+        cdef char *val = NULL
+        cdef int i
+        result = {}
+        for i in range(GDALGetDriverCount()):
+            drv = GDALGetDriver(i)
+            key = GDALGetDriverShortName(drv)
+            key_b = key
+            val = GDALGetDriverLongName(drv)
+            val_b = val
+            result[key_b.decode('utf-8')] = val_b.decode('utf-8')
+        return result
diff --git a/rasterio/_err.pyx b/rasterio/_err.pyx
new file mode 100644
index 0000000..44bb201
--- /dev/null
+++ b/rasterio/_err.pyx
@@ -0,0 +1,70 @@
+"""rasterio._err
+
+Transformation of GDAL C API errors to Python exceptions using Python's
+``with`` statement and an error-handling context manager class.
+
+The ``cpl_errs`` error-handling context manager is intended for use in
+Rasterio's Cython code. When entering the body of a ``with`` statement,
+the context manager clears GDAL's error stack. On exit, the context
+manager pops the last error off the stack and raises an appropriate
+Python exception. It's otherwise pretty difficult to do this kind of
+thing.  I couldn't make it work with a CPL error handler, Cython's
+C code swallows exceptions raised from C callbacks.
+
+When used to wrap a call to open a PNG in update mode
+
+    with cpl_errs:
+        cdef void *hds = GDALOpen('file.png', 1)
+    if hds == NULL:
+        raise ValueError("NULL dataset")
+
+the ValueError of last resort never gets raised because the context
+manager raises a more useful and informative error:
+
+    Traceback (most recent call last):
+      File "/Users/sean/code/rasterio/scripts/rio_insp", line 65, in <module>
+        with rasterio.open(args.src, args.mode) as src:
+      File "/Users/sean/code/rasterio/rasterio/__init__.py", line 111, in open
+        s.start()
+    ValueError: The PNG driver does not support update access to existing datasets.
+"""
+
+# CPL function declarations.
+cdef extern from "cpl_error.h":
+    int CPLGetLastErrorNo()
+    const char* CPLGetLastErrorMsg()
+    int CPLGetLastErrorType()
+    void CPLErrorReset()
+
+# Map GDAL error numbers to Python exceptions.
+exception_map = {
+    1: RuntimeError,        # CPLE_AppDefined
+    2: MemoryError,         # CPLE_OutOfMemory
+    3: IOError,             # CPLE_FileIO
+    4: IOError,             # CPLE_OpenFailed
+    5: TypeError,           # CPLE_IllegalArg
+    6: ValueError,          # CPLE_NotSupported
+    7: AssertionError,      # CPLE_AssertionFailed
+    8: IOError,             # CPLE_NoWriteAccess
+    9: KeyboardInterrupt,   # CPLE_UserInterrupt
+    10: ValueError          # ObjectNull
+    }
+
+
+cdef class GDALErrCtxManager:
+    """A manager for GDAL error handling contexts."""
+
+    def __enter__(self):
+        CPLErrorReset()
+        return self
+
+    def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
+        cdef int err_type = CPLGetLastErrorType()
+        cdef int err_no = CPLGetLastErrorNo()
+        cdef char *msg = CPLGetLastErrorMsg()
+        # TODO: warn for err_type 2?
+        if err_type >= 3:
+            raise exception_map[err_no](msg)
+
+cpl_errs = GDALErrCtxManager()
+
diff --git a/rasterio/_example.pyx b/rasterio/_example.pyx
new file mode 100644
index 0000000..c203847
--- /dev/null
+++ b/rasterio/_example.pyx
@@ -0,0 +1,24 @@
+import numpy
+cimport numpy
+
+def compute(
+        unsigned char[:, :, :] input, 
+        unsigned char[:, :, :] output):
+    # Given input and output uint8 arrays, fakes an CPU-intensive
+    # computation.
+    cdef int I, J, K
+    cdef int i, j, k, l
+    cdef double val
+    I = input.shape[0]
+    J = input.shape[1]
+    K = input.shape[2]
+    with nogil:
+        for i in range(I):
+            for j in range(J):
+                for k in range(K):
+                    val = <double>input[i, j, k]
+                    for l in range(2000):
+                        val += 1.0
+                    val -= 2000.0
+                    output[~i, j, k] = <unsigned char>val
+
diff --git a/rasterio/_features.pxd b/rasterio/_features.pxd
new file mode 100644
index 0000000..9ffcacd
--- /dev/null
+++ b/rasterio/_features.pxd
@@ -0,0 +1,29 @@
+
+cdef class GeomBuilder:
+    cdef void *geom
+    cdef object code
+    cdef object geomtypename
+    cdef object ndims
+    cdef _buildCoords(self, void *geom)
+    cpdef _buildPoint(self)
+    cpdef _buildLineString(self)
+    cpdef _buildLinearRing(self)
+    cdef _buildParts(self, void *geom)
+    cpdef _buildPolygon(self)
+    cpdef _buildMultiPolygon(self)
+    cdef build(self, void *geom)
+    cpdef build_wkb(self, object wkb)
+
+
+cdef class OGRGeomBuilder:
+    cdef void * _createOgrGeometry(self, int geom_type)
+    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)
diff --git a/rasterio/_features.pyx b/rasterio/_features.pyx
new file mode 100644
index 0000000..2fc5ec6
--- /dev/null
+++ b/rasterio/_features.pyx
@@ -0,0 +1,586 @@
+# cython: profile=True
+
+import logging
+import json
+import numpy as np
+cimport numpy as np
+from rasterio._io cimport InMemoryRaster
+from rasterio cimport _gdal, _ogr, _io
+from rasterio import dtypes
+
+
+log = logging.getLogger('rasterio')
+
+
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+log.addHandler(NullHandler())
+
+
+def _shapes(image, mask, connectivity, transform):
+    """
+    Return a generator of (polygon, value) for each each set of adjacent pixels
+    of the same value.
+
+    Parameters
+    ----------
+    image : numpy ndarray or rasterio Band object
+        (RasterReader, bidx namedtuple).
+        Data type must be one of rasterio.int16, rasterio.int32,
+        rasterio.uint8, rasterio.uint16, or rasterio.float32.
+    mask : numpy ndarray or rasterio Band object
+        Values of False will be excluded from feature generation
+        Must be of type rasterio.bool_
+    connectivity : int
+        Use 4 or 8 pixel connectivity for grouping pixels into features
+    transform : Affine transformation
+        If not provided, feature coordinates will be generated based on pixel
+        coordinates
+
+    Returns
+    -------
+    Generator of (polygon, value)
+        Yields a pair of (polygon, value) for each feature found in the image.
+        Polygons are GeoJSON-like dicts and the values are the associated value
+        from the image, in the data type of the image.
+        Note: due to floating point precision issues, values returned from a
+        floating point image may not exactly match the original values.
+
+    """
+
+    cdef int retval, rows, cols
+    cdef void *hband
+    cdef void *hmaskband
+    cdef void *hfdriver
+    cdef void *hfs
+    cdef void *hlayer
+    cdef void *fielddefn
+    cdef _io.RasterReader rdr
+    cdef _io.RasterReader mrdr
+    cdef char **options = NULL
+
+    cdef InMemoryRaster mem_ds = None
+    cdef InMemoryRaster mask_ds = None
+    cdef bint is_float = np.dtype(image.dtype).kind == 'f'
+    cdef int fieldtp = 0
+
+    if is_float:
+        fieldtp = 2
+
+    if isinstance(image, np.ndarray):
+        mem_ds = InMemoryRaster(image, transform)
+        hband = mem_ds.band
+    elif isinstance(image, tuple):
+        rdr = image.ds
+        hband = rdr.band(image.bidx)
+    else:
+        raise ValueError("Invalid source image")
+
+    if isinstance(mask, np.ndarray):
+        # A boolean mask must be converted to uint8 for GDAL
+        mask_ds = InMemoryRaster(mask.astype('uint8'), transform)
+        hmaskband = mask_ds.band
+    elif isinstance(mask, tuple):
+        if mask.shape != image.shape:
+            raise ValueError("Mask must have same shape as image")
+        mrdr = mask.ds
+        hmaskband = mrdr.band(mask.bidx)
+    else:
+        hmaskband = NULL
+
+    # Create an in-memory feature store.
+    hfdriver = _ogr.OGRGetDriverByName("Memory")
+    if hfdriver == NULL:
+        raise ValueError("NULL driver")
+    hfs = _ogr.OGR_Dr_CreateDataSource(hfdriver, "temp", NULL)
+    if hfs == NULL:
+        raise ValueError("NULL feature dataset")
+
+    # And a layer.
+    hlayer = _ogr.OGR_DS_CreateLayer(hfs, "polygons", NULL, 3, NULL)
+    if hlayer == NULL:
+        raise ValueError("NULL layer")
+
+    fielddefn = _ogr.OGR_Fld_Create("image_value", fieldtp)
+    if fielddefn == NULL:
+        raise ValueError("NULL field definition")
+    _ogr.OGR_L_CreateField(hlayer, fielddefn, 1)
+    _ogr.OGR_Fld_Destroy(fielddefn)
+
+    if connectivity == 8:
+        options = _gdal.CSLSetNameValue(options, "8CONNECTED", "8")
+
+    if is_float:
+        _gdal.GDALFPolygonize(hband, hmaskband, hlayer, 0, options, NULL, NULL)
+    else:
+        _gdal.GDALPolygonize(hband, hmaskband, hlayer, 0, options, NULL, NULL)
+
+    # Yield Fiona-style features
+    cdef ShapeIterator shape_iter = ShapeIterator()
+    shape_iter.hfs = hfs
+    shape_iter.hlayer = hlayer
+    shape_iter.fieldtp = fieldtp
+    for s, v in shape_iter:
+        yield s, v
+
+    if mem_ds is not None:
+        mem_ds.close()
+    if mask_ds is not None:
+        mask_ds.close()
+    if hfs != NULL:
+        _ogr.OGR_DS_Destroy(hfs)
+    if options:
+        _gdal.CSLDestroy(options)
+
+
+def _sieve(image, size, output, mask, connectivity):
+    """
+    Replaces small polygons in `image` with the value of their largest
+    neighbor.  Polygons are found for each set of neighboring pixels of the
+    same value.
+
+    Parameters
+    ----------
+    image : numpy ndarray or rasterio Band object
+        (RasterReader, bidx namedtuple)
+        Must be of type rasterio.int16, rasterio.int32, rasterio.uint8,
+        rasterio.uint16, or rasterio.float32.
+    size : int
+        minimum polygon size (number of pixels) to retain.
+    output : numpy ndarray
+        Array of same shape and data type as `image` in which to store results.
+    mask : numpy ndarray or rasterio Band object
+        Values of False will be excluded from feature generation.
+        Must be of type rasterio.bool_.
+    connectivity : int
+        Use 4 or 8 pixel connectivity for grouping pixels into features.
+
+    """
+
+    cdef int retval, rows, cols
+    cdef InMemoryRaster in_mem_ds = None
+    cdef InMemoryRaster out_mem_ds = None
+    cdef InMemoryRaster mask_mem_ds = None
+    cdef void *in_band
+    cdef void *out_band
+    cdef void *mask_band
+    cdef _io.RasterReader rdr
+    cdef _io.RasterUpdater udr
+    cdef _io.RasterReader mask_reader
+
+    if isinstance(image, np.ndarray):
+        in_mem_ds = InMemoryRaster(image)
+        in_band = in_mem_ds.band
+    elif isinstance(image, tuple):
+        rdr = image.ds
+        hband = rdr.band(image.bidx)
+    else:
+        raise ValueError("Invalid source image")
+
+    if isinstance(output, np.ndarray):
+        out_mem_ds = InMemoryRaster(output)
+        out_band = out_mem_ds.band
+    elif isinstance(output, tuple):
+        udr = output.ds
+        out_band = udr.band(output.bidx)
+    else:
+        raise ValueError("Invalid output image")
+
+    if isinstance(mask, np.ndarray):
+        # A boolean mask must be converted to uint8 for GDAL
+        mask_mem_ds = InMemoryRaster(mask.astype('uint8'))
+        mask_band = mask_mem_ds.band
+    elif isinstance(mask, tuple):
+        if mask.shape != image.shape:
+            raise ValueError("Mask must have same shape as image")
+        mask_reader = mask.ds
+        mask_band = mask_reader.band(mask.bidx)
+    else:
+        mask_band = NULL
+
+    _gdal.GDALSieveFilter(
+        in_band,
+        mask_band,
+        out_band,
+        size,
+        connectivity,
+        NULL,
+        NULL,
+        NULL
+    )
+
+    # Read from out_band into output
+    _io.io_auto(output, out_band, False)
+
+    if in_mem_ds is not None:
+        in_mem_ds.close()
+    if out_mem_ds is not None:
+        out_mem_ds.close()
+    if mask_mem_ds is not None:
+        mask_mem_ds.close()
+
+
+def _rasterize(shapes, image, transform, all_touched):
+    """
+    Burns input geometries into `image`.
+
+    Parameters
+    ----------
+    shapes : iterable of (geometry, value) pairs
+        `geometry` is a GeoJSON-like object.
+    image : numpy ndarray
+        Array in which to store results.
+    transform : Affine transformation object, optional
+        Transformation applied to shape geometries into pixel coordinates.
+    all_touched : boolean, optional
+        If True, all pixels touched by geometries will be burned in.
+        If false, only pixels whose center is within the polygon or that are
+        selected by brezenhams line algorithm will be burned in.
+
+    """
+
+    cdef int retval
+    cdef size_t i
+    cdef size_t num_geometries = 0
+    cdef void **ogr_geoms = NULL
+    cdef char **options = NULL
+    cdef double *pixel_values = NULL  # requires one value per geometry
+    cdef InMemoryRaster mem
+
+    try:
+        if all_touched:
+            options = _gdal.CSLSetNameValue(options, "ALL_TOUCHED", "TRUE")
+
+        # GDAL needs an array of geometries.
+        # For now, we'll build a Python list on the way to building that
+        # C array. TODO: make this more efficient.
+        all_shapes = list(shapes)
+        num_geometries = len(all_shapes)
+
+        ogr_geoms = <void **>_gdal.CPLMalloc(num_geometries * sizeof(void*))
+        pixel_values = <double *>_gdal.CPLMalloc(
+                            num_geometries * sizeof(double))
+
+        for i, (geometry, value) in enumerate(all_shapes):
+            try:
+                ogr_geoms[i] = OGRGeomBuilder().build(geometry)
+                pixel_values[i] = <double>value
+            except:
+                log.error("Geometry %r at index %d with value %d skipped",
+                    geometry, i, value)
+
+        with InMemoryRaster(image, transform) as mem:
+            _gdal.GDALRasterizeGeometries(
+                        mem.dataset, 1, mem.band_ids,
+                        num_geometries, ogr_geoms,
+                        NULL, mem.transform, pixel_values,
+                        options, NULL, NULL)
+
+            # Read in-memory data back into image
+            image = mem.read()
+
+    finally:
+        for i in range(num_geometries):
+            _deleteOgrGeom(ogr_geoms[i])
+        _gdal.CPLFree(ogr_geoms)
+        _gdal.CPLFree(pixel_values)
+        if options:
+            _gdal.CSLDestroy(options)
+
+
+# Mapping of OGR integer geometry types to GeoJSON type names.
+GEOMETRY_TYPES = {
+    0: 'Unknown',
+    1: 'Point',
+    2: 'LineString',
+    3: 'Polygon',
+    4: 'MultiPoint',
+    5: 'MultiLineString',
+    6: 'MultiPolygon',
+    7: 'GeometryCollection',
+    100: 'None',
+    101: 'LinearRing',
+    0x80000001: '3D Point',
+    0x80000002: '3D LineString',
+    0x80000003: '3D Polygon',
+    0x80000004: '3D MultiPoint',
+    0x80000005: '3D MultiLineString',
+    0x80000006: '3D MultiPolygon',
+    0x80000007: '3D GeometryCollection'
+}
+
+# Mapping of GeoJSON type names to OGR integer geometry types
+GEOJSON2OGR_GEOMETRY_TYPES = dict(
+    (v, k) for k, v in GEOMETRY_TYPES.iteritems()
+)
+
+
+# Geometry related functions and classes follow.
+
+cdef void * _createOgrGeomFromWKB(object wkb) except NULL:
+    """Make an OGR geometry from a WKB string"""
+
+    geom_type = bytearray(wkb)[1]
+    cdef unsigned char *buffer = wkb
+    cdef void *cogr_geometry = _ogr.OGR_G_CreateGeometry(geom_type)
+    if cogr_geometry != NULL:
+        _ogr.OGR_G_ImportFromWkb(cogr_geometry, buffer, len(wkb))
+    return cogr_geometry
+
+
+cdef _deleteOgrGeom(void *cogr_geometry):
+    """Delete an OGR geometry"""
+
+    if cogr_geometry != NULL:
+        _ogr.OGR_G_DestroyGeometry(cogr_geometry)
+    cogr_geometry = NULL
+
+
+cdef class GeomBuilder:
+    """Builds a GeoJSON (Fiona-style) geometry from an OGR geometry."""
+
+    cdef _buildCoords(self, void *geom):
+        # Build a coordinate sequence
+        cdef int i
+        if geom == NULL:
+            raise ValueError("Null geom")
+        npoints = _ogr.OGR_G_GetPointCount(geom)
+        coords = []
+        for i in range(npoints):
+            values = [_ogr.OGR_G_GetX(geom, i), _ogr.OGR_G_GetY(geom, i)]
+            if self.ndims > 2:
+                values.append(_ogr.OGR_G_GetZ(geom, i))
+            coords.append(tuple(values))
+        return coords
+
+    cpdef _buildPoint(self):
+        return {
+            'type': 'Point',
+            'coordinates': self._buildCoords(self.geom)[0]
+        }
+
+    cpdef _buildLineString(self):
+        return {
+            'type': 'LineString',
+            'coordinates': self._buildCoords(self.geom)
+        }
+
+    cpdef _buildLinearRing(self):
+        return {
+            'type': 'LinearRing',
+            'coordinates': self._buildCoords(self.geom)
+        }
+
+    cdef _buildParts(self, void *geom):
+        cdef int j
+        cdef void *part
+        if geom == NULL:
+            raise ValueError("Null geom")
+        parts = []
+        for j in range(_ogr.OGR_G_GetGeometryCount(geom)):
+            part = _ogr.OGR_G_GetGeometryRef(geom, j)
+            parts.append(GeomBuilder().build(part))
+        return parts
+
+    cpdef _buildPolygon(self):
+        coordinates = [p['coordinates'] for p in self._buildParts(self.geom)]
+        return {'type': 'Polygon', 'coordinates': coordinates}
+
+    cpdef _buildMultiPolygon(self):
+        coordinates = [p['coordinates'] for p in self._buildParts(self.geom)]
+        return {'type': 'MultiPolygon', 'coordinates': coordinates}
+
+    cdef build(self, void *geom):
+        """Builds a GeoJSON object from an OGR geometry object."""
+
+        if geom == NULL:
+            raise ValueError("Null geom")
+
+        cdef unsigned int etype = _ogr.OGR_G_GetGeometryType(geom)
+        self.code = etype
+        self.geomtypename = GEOMETRY_TYPES[self.code & (~0x80000000)]
+        self.ndims = _ogr.OGR_G_GetCoordinateDimension(geom)
+        self.geom = geom
+        return getattr(self, '_build' + self.geomtypename)()
+
+    cpdef build_wkb(self, object wkb):
+        """Builds a GeoJSON object from a Well-Known Binary format (WKB)."""
+        # The only other method anyone needs to call
+        cdef object data = wkb
+        cdef void *cogr_geometry = _createOgrGeomFromWKB(data)
+        result = self.build(cogr_geometry)
+        _deleteOgrGeom(cogr_geometry)
+        return result
+
+
+cdef geometry(void *geom):
+    """Returns a GeoJSON object from an OGR geometry object."""
+
+    return GeomBuilder().build(geom)
+
+
+cdef class OGRGeomBuilder:
+    """
+    Builds an OGR geometry from GeoJSON geometry.
+    From Fiona: https://github.com/Toblerity/Fiona/blob/master/src/fiona/ogrext.pyx
+    """
+
+    cdef void * _createOgrGeometry(self, int geom_type) except NULL:
+        cdef void *cogr_geometry = _ogr.OGR_G_CreateGeometry(geom_type)
+        if cogr_geometry is NULL:
+            raise Exception(
+                "Could not create OGR Geometry of type: %i" % geom_type
+            )
+        return cogr_geometry
+
+    cdef _addPointToGeometry(self, void *cogr_geometry, object coordinate):
+        if len(coordinate) == 2:
+            x, y = coordinate
+            _ogr.OGR_G_AddPoint_2D(cogr_geometry, x, y)
+        else:
+            x, y, z = coordinate[:3]
+            _ogr.OGR_G_AddPoint(cogr_geometry, x, y, z)
+
+    cdef void * _buildPoint(self, object coordinates) except NULL:
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['Point']
+        )
+        self._addPointToGeometry(cogr_geometry, coordinates)
+        return cogr_geometry
+
+    cdef void * _buildLineString(self, object coordinates) except NULL:
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['LineString']
+        )
+        for coordinate in coordinates:
+            self._addPointToGeometry(cogr_geometry, coordinate)
+        return cogr_geometry
+
+    cdef void * _buildLinearRing(self, object coordinates) except NULL:
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['LinearRing']
+        )
+        for coordinate in coordinates:
+            self._addPointToGeometry(cogr_geometry, coordinate)
+        _ogr.OGR_G_CloseRings(cogr_geometry)
+        return cogr_geometry
+
+    cdef void * _buildPolygon(self, object coordinates) except NULL:
+        cdef void *cogr_ring
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['Polygon']
+        )
+        for ring in coordinates:
+            cogr_ring = self._buildLinearRing(ring)
+            _ogr.OGR_G_AddGeometryDirectly(cogr_geometry, cogr_ring)
+        return cogr_geometry
+
+    cdef void * _buildMultiPoint(self, object coordinates) except NULL:
+        cdef void *cogr_part
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['MultiPoint']
+        )
+        for coordinate in coordinates:
+            cogr_part = self._buildPoint(coordinate)
+            _ogr.OGR_G_AddGeometryDirectly(cogr_geometry, cogr_part)
+        return cogr_geometry
+
+    cdef void * _buildMultiLineString(self, object coordinates) except NULL:
+        cdef void *cogr_part
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['MultiLineString']
+        )
+        for line in coordinates:
+            cogr_part = self._buildLineString(line)
+            _ogr.OGR_G_AddGeometryDirectly(cogr_geometry, cogr_part)
+        return cogr_geometry
+
+    cdef void * _buildMultiPolygon(self, object coordinates) except NULL:
+        cdef void *cogr_part
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['MultiPolygon']
+        )
+        for part in coordinates:
+            cogr_part = self._buildPolygon(part)
+            _ogr.OGR_G_AddGeometryDirectly(cogr_geometry, cogr_part)
+        return cogr_geometry
+
+    cdef void * _buildGeometryCollection(self, object coordinates) except NULL:
+        cdef void *cogr_part
+        cdef void *cogr_geometry = self._createOgrGeometry(
+            GEOJSON2OGR_GEOMETRY_TYPES['GeometryCollection']
+        )
+        for part in coordinates:
+            cogr_part = OGRGeomBuilder().build(part)
+            _ogr.OGR_G_AddGeometryDirectly(cogr_geometry, cogr_part)
+        return cogr_geometry
+
+    cdef void * build(self, object geometry) except NULL:
+        """Builds an OGR geometry from GeoJSON geometry."""
+
+        cdef object typename = geometry['type']
+        cdef object coordinates = geometry.get('coordinates')
+        if not typename or not coordinates:
+            raise ValueError("Input is not a valid geometry object")
+        if typename == 'Point':
+            return self._buildPoint(coordinates)
+        elif typename == 'LineString':
+            return self._buildLineString(coordinates)
+        elif typename == 'LinearRing':
+            return self._buildLinearRing(coordinates)
+        elif typename == 'Polygon':
+            return self._buildPolygon(coordinates)
+        elif typename == 'MultiPoint':
+            return self._buildMultiPoint(coordinates)
+        elif typename == 'MultiLineString':
+            return self._buildMultiLineString(coordinates)
+        elif typename == 'MultiPolygon':
+            return self._buildMultiPolygon(coordinates)
+        elif typename == 'GeometryCollection':
+            coordinates = geometry.get('geometries')
+            return self._buildGeometryCollection(coordinates)
+        else:
+            raise ValueError("Unsupported geometry type %s" % typename)
+
+
+# Feature extension classes and functions follow.
+
+cdef _deleteOgrFeature(void *cogr_feature):
+    """Delete an OGR feature"""
+    if cogr_feature != NULL:
+        _ogr.OGR_F_Destroy(cogr_feature)
+    cogr_feature = NULL
+
+
+cdef class ShapeIterator:
+    """Provides an iterator over shapes in an OGR feature layer."""
+
+    # Reference to its Collection
+    cdef void *hfs
+    cdef void *hlayer
+
+    cdef int fieldtp  # OGR Field Type: 0=int, 2=double
+
+    def __iter__(self):
+        _ogr.OGR_L_ResetReading(self.hlayer)
+        return self
+
+    def __next__(self):
+        cdef long fid
+        cdef void *ftr
+        cdef void *geom
+        ftr = _ogr.OGR_L_GetNextFeature(self.hlayer)
+        if ftr == NULL:
+            raise StopIteration
+        if self.fieldtp == 0:
+            image_value = _ogr.OGR_F_GetFieldAsInteger(ftr, 0)
+        else:
+            image_value = _ogr.OGR_F_GetFieldAsDouble(ftr, 0)
+        geom = _ogr.OGR_F_GetGeometryRef(ftr)
+        if geom != NULL:
+            shape = GeomBuilder().build(geom)
+        else:
+            shape = None
+        _deleteOgrFeature(ftr)
+        return shape, image_value
diff --git a/rasterio/_gdal.pxd b/rasterio/_gdal.pxd
new file mode 100644
index 0000000..1214678
--- /dev/null
+++ b/rasterio/_gdal.pxd
@@ -0,0 +1,215 @@
+# GDAL function definitions.
+#
+
+cdef extern from "cpl_conv.h" nogil:
+    void *  CPLMalloc (size_t)
+    void    CPLFree (void *ptr)
+    void    CPLSetThreadLocalConfigOption (char *key, char *val)
+    const char *CPLGetConfigOption (char *, char *)
+
+cdef extern from "cpl_string.h":
+    int CSLCount (char **papszStrList)
+    char ** CSLAddNameValue (char **papszStrList, const char *pszName, const char *pszValue)
+    char ** CSLDuplicate (char ** papszStrList)
+
+    int CSLFindName (char **papszStrList, const char *pszName)
+    const char * CSLFetchNameValue (char **papszStrList, const char *pszName)
+    char ** CSLSetNameValue (char **list, char *name, char *val)
+    void    CSLDestroy (char **list)
+
+cdef extern from "ogr_srs_api.h":
+    void    OSRCleanup ()
+    void *  OSRClone (void *srs)
+    void    OSRDestroySpatialReference (void *srs)
+    int     OSRExportToProj4 (void *srs, char **params)
+    int     OSRExportToWkt (void *srs, char **params)
+    int     OSRImportFromEPSG (void *srs, int code)
+    int     OSRImportFromProj4 (void *srs, char *proj)
+    int     OSRSetFromUserInput (void *srs, char *input)
+    int     OSRAutoIdentifyEPSG (void *srs)
+    int     OSRFixup(void *srs)
+    const char * OSRGetAuthorityName (void *srs, const char *key)
+    const char * OSRGetAuthorityCode (void *srs, const char *key)
+    void *  OSRNewSpatialReference (char *wkt)
+    void    OSRRelease (void *srs)
+    void *  OCTNewCoordinateTransformation (void *source, void *dest)
+    void    OCTDestroyCoordinateTransformation (void *source)
+    int     OCTTransform (void *ct, int nCount, double *x, double *y, double *z)
+
+cdef extern from "gdal.h" nogil:
+    void GDALAllRegister()
+    int GDALGetDriverCount()
+    void * GDALGetDriver(int)
+    const char* GDALGetDescription (void *)
+    void GDALSetDescription (void *, const char *)
+
+    void * GDALGetDriverByName(const char *name)
+    void * GDALOpen(const char *filename, int access) # except -1
+    void GDALFlushCache (void *ds)
+    void GDALClose(void *ds)
+    void * GDALGetDatasetDriver(void *ds)
+    int GDALGetGeoTransform	(void *ds, double *transform)
+    const char * GDALGetProjectionRef(void *ds)
+    int GDALGetRasterXSize(void *ds)
+    int GDALGetRasterYSize(void *ds)
+    int GDALGetRasterCount(void *ds)
+    void * GDALGetRasterBand(void *ds, int num)
+    int GDALSetGeoTransform	(void *ds, double *transform)
+    int GDALSetProjection(void *ds, const char *wkt)
+
+    ctypedef enum GDALDataType:
+        GDT_Unknown
+        GDT_Byte
+        GDT_UInt16
+        GDT_Int16
+        GDT_UInt32
+        GDT_Int32
+        GDT_Float32
+        GDT_Float64
+        GDT_CInt16
+        GDT_CInt32
+        GDT_CFloat32
+        GDT_CFloat64
+        GDT_TypeCount
+
+    ctypedef enum GDALRWFlag:
+        GF_Read
+        GF_Write
+
+    void GDALGetBlockSize(void *band, int *xsize, int *ysize)
+    int GDALGetRasterDataType(void *band)
+    double GDALGetRasterNoDataValue(void *band, int *success)
+    int GDALSetRasterNoDataValue(void *band, double value)
+    int GDALDatasetRasterIO(void *band, int, int xoff, int yoff, int xsize, int ysize, void *buffer, int width, int height, int, int count, int *bmap, int poff, int loff, int boff)
+    int GDALRasterIO(void *band, int, int xoff, int yoff, int xsize, int ysize, void *buffer, int width, int height, int, int poff, int loff)
+
+    int GDALSetRasterNoDataValue(void *band, double value)
+
+    void * GDALCreate(void *driver, const char *filename, int width, int height, int nbands, GDALDataType dtype, const char **options)
+    void * GDALCreateCopy(void *driver, const char *filename, void *ds, int strict, char **options, void *progress_func, void *progress_data)
+    const char * GDALGetDriverShortName(void *driver)
+    const char * GDALGetDriverLongName(void *driver)
+
+    char** GDALGetMetadata (void *hObject, const char *pszDomain)
+    int GDALSetMetadata (void *hObject, char **papszMD, const char *pszDomain)
+    const char* GDALGetMetadataItem(void *hObject, const char *pszName, const char *pszDomain)
+    int GDALSetMetadataItem (void *hObject, const char *pszName, const char *pszValue, const char *pszDomain)
+
+    ctypedef struct GDALColorEntry:
+        short c1
+        short c2
+        short c3
+        short c4
+
+    const GDALColorEntry *GDALGetColorEntry (void *hTable, int)
+    void GDALSetColorEntry (void *hTable, int i, const GDALColorEntry *poEntry)
+    int GDALSetRasterColorTable (void *hBand, void *hTable)
+    void *GDALGetRasterColorTable (void *hBand)
+    void *GDALCreateColorTable (int)
+    void GDALDestroyColorTable (void *hTable)
+    int GDALGetColorEntryCount (void *hTable)
+    int GDALGetRasterColorInterpretation (void *hBand)
+    int GDALSetRasterColorInterpretation (void *hBand, int)
+
+    void *GDALGetMaskBand (void *hBand)
+    int GDALCreateMaskBand (void *hDS, int flags)
+
+cdef extern from "gdalwarper.h":
+
+    ctypedef enum GDALResampleAlg:
+        GRA_NearestNeighbour
+        GRA_Bilinear
+        GRA_Cubic
+        GRA_CubicSpline
+        GRA_Lanczos
+        GRA_Average 
+        GRA_Mode
+
+    ctypedef int (*GDALMaskFunc)( void *pMaskFuncArg,
+                 int nBandCount, int eType, 
+                 int nXOff, int nYOff, 
+                 int nXSize, int nYSize,
+                 unsigned char **papabyImageData, 
+                 int bMaskIsFloat, void *pMask )
+
+    ctypedef int (*GDALTransformerFunc)( void *pTransformerArg, 
+                        int bDstToSrc, int nPointCount, 
+                        double *x, double *y, double *z, int *panSuccess )
+
+    ctypedef struct GDALWarpOptions:
+        char **papszWarpOptions
+        double dfWarpMemoryLimit
+        GDALResampleAlg eResampleAlg
+        GDALDataType eWorkingDataType
+        void *hSrcDS
+        void *hDstDS
+        # 0 for all bands
+        int nBandCount
+        # List of source band indexes
+        int *panSrcBands
+        # List of destination band indexes
+        int *panDstBands
+        # The source band so use as an alpha (transparency) value, 0=disabled
+        int nSrcAlphaBand
+        # The dest. band so use as an alpha (transparency) value, 0=disabled
+        int nDstAlphaBand
+        # The "nodata" value real component for each input band, if NULL there isn't one */
+        double *padfSrcNoDataReal
+        # The "nodata" value imaginary component - may be NULL even if real component is provided. */
+        double *padfSrcNoDataImag
+        # The "nodata" value real component for each output band, if NULL there isn't one */
+        double *padfDstNoDataReal
+        # The "nodata" value imaginary component - may be NULL even if real component is provided. */
+        double *padfDstNoDataImag
+        # GDALProgressFunc() compatible progress reporting function, or NULL if there isn't one. */
+        void *pfnProgress
+        # Callback argument to be passed to pfnProgress. */
+        void *pProgressArg
+        # Type of spatial point transformer function */
+        GDALTransformerFunc pfnTransformer
+        # Handle to image transformer setup structure */
+        void *pTransformerArg
+        GDALMaskFunc *papfnSrcPerBandValidityMaskFunc
+        void **papSrcPerBandValidityMaskFuncArg
+        GDALMaskFunc        pfnSrcValidityMaskFunc
+        void               *pSrcValidityMaskFuncArg
+        GDALMaskFunc        pfnSrcDensityMaskFunc
+        void               *pSrcDensityMaskFuncArg
+        GDALMaskFunc        pfnDstDensityMaskFunc
+        void               *pDstDensityMaskFuncArg
+        GDALMaskFunc        pfnDstValidityMaskFunc
+        void               *pDstValidityMaskFuncArg
+        int              (*pfnPreWarpChunkProcessor)( void *pKern, void *pArg )
+        void               *pPreWarpProcessorArg
+        int              (*pfnPostWarpChunkProcessor)( void *pKern, void *pArg)
+        void               *pPostWarpProcessorArg
+        # Optional OGRPolygonH for a masking cutline. */
+        void               *hCutline
+        # Optional blending distance to apply across cutline in pixels, default is 0
+        double              dfCutlineBlendDist
+
+    GDALWarpOptions *GDALCreateWarpOptions()
+    void GDALDestroyWarpOptions(GDALWarpOptions *)
+
+cdef extern from "gdal_alg.h":
+    
+    int GDALPolygonize(void *src_band, void *mask_band, void *layer, int fidx, char **options, void *progress_func, void *progress_data)
+    int GDALFPolygonize(void *src_band, void *mask_band, void *layer, int fidx, char **options, void *progress_func, void *progress_data)
+    int GDALSieveFilter(void *src_band, void *mask_band, void *dst_band, int size, int connectivity, char **options, void *progress_func, void *progress_data)
+    int GDALRasterizeGeometries(void *out_ds, int band_count, int *dst_bands, int geom_count, void **geometries,
+                            GDALTransformerFunc transform_func, void *transform, double *pixel_values, char **options,
+                            void *progress_func, void *progress_data)
+
+    void *GDALCreateGenImgProjTransformer(void* hSrcDS, const char *pszSrcWKT,
+                                 void* hDstDS, const char *pszDstWKT,
+                                 int bGCPUseOK, double dfGCPErrorThreshold,
+                                 int nOrder )
+    int GDALGenImgProjTransform(void *pTransformArg, int bDstToSrc, int nPointCount, double *x, double *y, double *z, int *panSuccess )
+    void GDALDestroyGenImgProjTransformer( void * )
+
+    void *GDALCreateApproxTransformer( GDALTransformerFunc pfnRawTransformer, void *pRawTransformerArg, double dfMaxError )
+    int  GDALApproxTransform(void *pTransformArg, int bDstToSrc, int nPointCount, double *x, double *y, double *z, int *panSuccess )
+    void GDALDestroyApproxTransformer( void * )
+
+
+
diff --git a/rasterio/_io.pxd b/rasterio/_io.pxd
new file mode 100644
index 0000000..bc866ea
--- /dev/null
+++ b/rasterio/_io.pxd
@@ -0,0 +1,248 @@
+cimport numpy as np
+
+from rasterio cimport _base
+
+
+cdef extern from "gdal.h":
+
+    ctypedef enum GDALDataType:
+        GDT_Unknown
+        GDT_Byte
+        GDT_UInt16
+        GDT_Int16
+        GDT_UInt32
+        GDT_Int32
+        GDT_Float32
+        GDT_Float64
+        GDT_CInt16
+        GDT_CInt32
+        GDT_CFloat32
+        GDT_CFloat64
+        GDT_TypeCount
+
+    ctypedef enum GDALAccess:
+        GA_ReadOnly
+        GA_Update
+
+    ctypedef enum GDALRWFlag:
+        GF_Read
+        GF_Write
+
+
+cdef class RasterReader(_base.DatasetReader):
+    # Read-only access to raster data and metadata.
+    pass
+
+
+cdef class RasterUpdater(RasterReader):
+    # Read-write access to raster data and metadata.
+    cdef readonly object _init_dtype
+    cdef readonly object _init_nodata
+    cdef readonly object _options
+
+
+cdef class IndirectRasterUpdater(RasterUpdater):
+    pass
+
+
+cdef class InMemoryRaster:
+    cdef void *dataset
+    cdef void *band
+    cdef double transform[6]
+    cdef int band_ids[1]
+    cdef np.ndarray _image
+
+
+ctypedef np.uint8_t DTYPE_UBYTE_t
+ctypedef np.uint16_t DTYPE_UINT16_t
+ctypedef np.int16_t DTYPE_INT16_t
+ctypedef np.uint32_t DTYPE_UINT32_t
+ctypedef np.int32_t DTYPE_INT32_t
+ctypedef np.float32_t DTYPE_FLOAT32_t
+ctypedef np.float64_t DTYPE_FLOAT64_t
+
+cdef int io_ubyte(
+        void *hband, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.uint8_t[:, :] buffer)
+
+cdef int io_uint16(
+        void *hband, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height,
+        np.uint16_t[:, :] buffer)
+
+cdef int io_int16(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.int16_t[:, :] buffer)
+
+cdef int io_uint32(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.uint32_t[:, :] buffer)
+
+cdef int io_int32(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.int32_t[:, :] buffer)
+
+cdef int io_float32(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.float32_t[:, :] buffer)
+
+cdef int io_float64(
+        void *hband,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.float64_t[:, :] buffer)
+
+cdef int io_multi_ubyte(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.uint8_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_uint16(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.uint16_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_int16(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.int16_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_uint32(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.uint32_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_int32(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.int32_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_float32(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.float32_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_float64(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.float64_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil
+
+cdef int io_multi_cint16(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.complex_t[:, :, :] out,
+        long[:] indexes,
+        int count)
+
+cdef int io_multi_cint32(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.complex_t[:, :, :] out,
+        long[:] indexes,
+        int count)
+
+cdef int io_multi_cfloat32(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.complex64_t[:, :, :] out,
+        long[:] indexes,
+        int count)
+
+cdef int io_multi_cfloat64(
+        void *hds, 
+        int mode, 
+        int xoff, 
+        int yoff, 
+        int width, 
+        int height, 
+        np.complex128_t[:, :, :] out,
+        long[:] indexes,
+        int count)
+
+cdef int io_auto(image, void *hband, bint write)
diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx
new file mode 100644
index 0000000..9ae404d
--- /dev/null
+++ b/rasterio/_io.pyx
@@ -0,0 +1,1522 @@
+# cython: boundscheck=False
+
+import logging
+import math
+import os
+import os.path
+import sys
+import warnings
+
+from libc.stdlib cimport malloc, free
+import numpy as np
+cimport numpy as np
+
+from rasterio cimport _base, _gdal, _ogr, _io
+from rasterio._base import (
+    eval_window, window_shape, window_index, tastes_like_gdal)
+from rasterio._drivers import driver_count, GDALEnv
+from rasterio._err import cpl_errs
+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
+
+log = logging.getLogger('rasterio')
+if 'all' in sys.warnoptions:
+    # show messages in console with: python -W all
+    logging.basicConfig()
+else:
+    # no handler messages shown
+    class NullHandler(logging.Handler):
+        def emit(self, record):
+            pass
+
+    log.addHandler(NullHandler())
+
+# Single band IO functions.
+
+cdef int io_ubyte(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.uint8_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 1, 0, 0)
+
+cdef int io_uint16(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.uint16_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 2, 0, 0)
+
+cdef int io_int16(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.int16_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 3, 0, 0)
+
+cdef int io_uint32(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.uint32_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 4, 0, 0)
+
+cdef int io_int32(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.int32_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 5, 0, 0)
+
+cdef int io_float32(
+        void *hband, 
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.float32_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 6, 0, 0)
+
+cdef int io_float64(
+        void *hband,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height, 
+        np.float64_t[:, :] buffer):
+    with nogil:
+        return _gdal.GDALRasterIO(
+            hband, mode, xoff, yoff, width, height,
+            &buffer[0, 0], buffer.shape[1], buffer.shape[0], 7, 0, 0)
+
+# The multi-band IO functions.
+
+cdef int io_multi_ubyte(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.uint8_t[:, :, :] buffer,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buffer[0, 0, 0], buffer.shape[2], buffer.shape[1], 
+                        1, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+cdef int io_multi_uint16(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.uint16_t[:, :, :] buf,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband = NULL
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf[0, 0, 0], buf.shape[2], buf.shape[1], 
+                        2, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+cdef int io_multi_int16(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.int16_t[:, :, :] buf,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband = NULL
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf[0, 0, 0], buf.shape[2], buf.shape[1], 
+                        3, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+cdef int io_multi_uint32(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.uint32_t[:, :, :] buf,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband = NULL
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf[0, 0, 0], buf.shape[2], buf.shape[1], 
+                        4, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+cdef int io_multi_int32(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.int32_t[:, :, :] buf,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband = NULL
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf[0, 0, 0], buf.shape[2], buf.shape[1], 
+                        5, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+
+cdef int io_multi_float32(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.float32_t[:, :, :] buf,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband = NULL
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf[0, 0, 0], buf.shape[2], buf.shape[1], 
+                        6, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+cdef int io_multi_float64(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.float64_t[:, :, :] buf,
+        long[:] indexes,
+        int count) nogil:
+    cdef int i, retval=0
+    cdef void *hband = NULL
+    cdef int *bandmap
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf[0, 0, 0], buf.shape[2], buf.shape[1], 
+                        7, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+    return retval
+
+cdef int io_multi_cint16(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.complex_t[:, :, :] out,
+        long[:] indexes,
+        int count):
+    
+    cdef int retval=0
+    cdef int *bandmap
+    cdef int I, J, K
+    cdef int i, j, k
+    cdef np.int16_t real, imag
+
+    buf = np.empty(
+            (out.shape[0], 2*out.shape[2]*out.shape[1]), 
+            dtype=np.int16)
+    cdef np.int16_t[:, :] buf_view = buf
+
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf_view[0, 0], out.shape[2], out.shape[1],
+                        8, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+
+        if retval > 0:
+            return retval
+
+        I = out.shape[0]
+        J = out.shape[1]
+        K = out.shape[2]
+        for i in range(I):
+            for j in range(J):
+                for k in range(K):
+                    real = buf_view[i, 2*(j*K+k)]
+                    imag = buf_view[i, 2*(j*K+k)+1]
+                    out[i,j,k].real = real
+                    out[i,j,k].imag = imag
+
+    return retval
+
+cdef int io_multi_cint32(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.complex_t[:, :, :] out,
+        long[:] indexes,
+        int count):
+    
+    cdef int retval=0
+    cdef int *bandmap
+    cdef int I, J, K
+    cdef int i, j, k
+    cdef np.int32_t real, imag
+
+    buf = np.empty(
+            (out.shape[0], 2*out.shape[2]*out.shape[1]), 
+            dtype=np.int32)
+    cdef np.int32_t[:, :] buf_view = buf
+
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf_view[0, 0], out.shape[2], out.shape[1],
+                        9, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+
+        if retval > 0:
+            return retval
+
+        I = out.shape[0]
+        J = out.shape[1]
+        K = out.shape[2]
+        for i in range(I):
+            for j in range(J):
+                for k in range(K):
+                    real = buf_view[i, 2*(j*K+k)]
+                    imag = buf_view[i, 2*(j*K+k)+1]
+                    out[i,j,k].real = real
+                    out[i,j,k].imag = imag
+
+    return retval
+
+cdef int io_multi_cfloat32(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.complex64_t[:, :, :] out,
+        long[:] indexes,
+        int count):
+    
+    cdef int retval=0
+    cdef int *bandmap
+    cdef int I, J, K
+    cdef int i, j, k
+    cdef np.float32_t real, imag
+
+    buf = np.empty(
+            (out.shape[0], 2*out.shape[2]*out.shape[1]), 
+            dtype=np.float32)
+    cdef np.float32_t[:, :] buf_view = buf
+
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf_view[0, 0], out.shape[2], out.shape[1],
+                        10, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+
+        if retval > 0:
+            return retval
+
+        I = out.shape[0]
+        J = out.shape[1]
+        K = out.shape[2]
+        for i in range(I):
+            for j in range(J):
+                for k in range(K):
+                    real = buf_view[i, 2*(j*K+k)]
+                    imag = buf_view[i, 2*(j*K+k)+1]
+                    out[i,j,k].real = real
+                    out[i,j,k].imag = imag
+
+    return retval
+
+cdef int io_multi_cfloat64(
+        void *hds,
+        int mode,
+        int xoff,
+        int yoff,
+        int width, 
+        int height,
+        np.complex128_t[:, :, :] out,
+        long[:] indexes,
+        int count):
+    
+    cdef int retval=0
+    cdef int *bandmap
+    cdef int I, J, K
+    cdef int i, j, k
+    cdef np.float64_t real, imag
+
+    buf = np.empty(
+            (out.shape[0], 2*out.shape[2]*out.shape[1]), 
+            dtype=np.float64)
+    cdef np.float64_t[:, :] buf_view = buf
+
+    with nogil:
+        bandmap = <int *>_gdal.CPLMalloc(count*sizeof(int))
+        for i in range(count):
+            bandmap[i] = indexes[i]
+        retval = _gdal.GDALDatasetRasterIO(
+                        hds, mode, xoff, yoff, width, height,
+                        &buf_view[0, 0], out.shape[2], out.shape[1],
+                        11, count, bandmap, 0, 0, 0)
+        _gdal.CPLFree(bandmap)
+
+        if retval > 0:
+            return retval
+
+        I = out.shape[0]
+        J = out.shape[1]
+        K = out.shape[2]
+        for i in range(I):
+            for j in range(J):
+                for k in range(K):
+                    real = buf_view[i, 2*(j*K+k)]
+                    imag = buf_view[i, 2*(j*K+k)+1]
+                    out[i,j,k].real = real
+                    out[i,j,k].imag = imag
+
+    return retval
+
+
+cdef int io_auto(image, void *hband, bint write):
+    """
+    Convenience function to handle IO with a GDAL band and a 2D numpy image
+
+    :param image: a numpy 2D image
+    :param hband: an instance of GDALGetRasterBand
+    :param write: 1 (True) uses write mode, 0 (False) uses read
+    :return: the return value from the data-type specific IO function
+    """
+
+    if not len(image.shape) == 2:
+        raise ValueError("Specified image must have 2 dimensions")
+
+    cdef int width = image.shape[1]
+    cdef int height = image.shape[0]
+
+    dtype_name = image.dtype.name
+
+    if dtype_name == "float32":
+        return io_float32(hband, write, 0, 0, width, height, image)
+    elif dtype_name == "float64":
+        return io_float64(hband, write, 0, 0, width, height, image)
+    elif dtype_name == "uint8":
+        return io_ubyte(hband, write, 0, 0, width, height, image)
+    elif dtype_name == "int16":
+        return io_int16(hband, write, 0, 0, width, height, image)
+    elif dtype_name == "int32":
+        return io_int32(hband, write, 0, 0, width, height, image)
+    elif dtype_name == "uint16":
+        return io_uint16(hband, write, 0, 0, width, height, image)
+    elif dtype_name == "uint32":
+        return io_uint32(hband, write, 0, 0, width, height, image)
+    else:
+        raise ValueError("Image dtype is not supported for this function."
+                         "Must be float32, float64, int16, int32, uint8, "
+                         "uint16, or uint32")
+
+
+cdef class RasterReader(_base.DatasetReader):
+
+    def read_band(self, bidx, out=None, window=None, masked=None):
+        """Read the `bidx` band into an `out` array if provided, 
+        otherwise return a new array.
+
+        Band indexes begin with 1: read_band(1) returns the first band.
+
+        The optional `window` argument is a 2 item tuple. The first item
+        is a tuple containing the indexes of the rows at which the
+        window starts and stops and the second is a tuple containing the
+        indexes of the columns at which the window starts and stops. For
+        example, ((0, 2), (0, 2)) defines a 2x2 window at the upper left
+        of the raster dataset.
+        """
+        return self.read(bidx, out=out, window=window, masked=masked)
+
+    def read(self, indexes=None, out=None, window=None, masked=None):
+        """Read raster bands as a multidimensional array
+
+        If `indexes` is a list, the result is a 3D array, but
+        is a 2D array if it is a band index number.
+
+        Optional `out` argument is a reference to an output array with the
+        same dimensions and shape.
+
+        See `read_band` for usage of the optional `window` argument.
+
+        The return type will be either a regular NumPy array, or a masked
+        NumPy array depending on the `masked` argument. The return type is
+        forced if either `True` or `False`, but will be chosen if `None`.
+        For `masked=None` (default), the array will be the same type as
+        `out` (if used), or will be masked if any of the nodatavals are
+        not `None`.
+        """
+        cdef int height, width, xoff, yoff, aix, bidx, indexes_count
+        cdef int retval = 0
+        return2d = False
+
+        if self._hds == NULL:
+            raise ValueError("can't read closed raster file")
+        if indexes is None:  # Default: read all bands
+            indexes = self.indexes
+        elif isinstance(indexes, int):
+            indexes = [indexes]
+            return2d = True
+            if out is not None and out.ndim == 2:
+                out.shape = (1,) + out.shape
+        if not indexes:
+            raise ValueError("No indexes to read")
+        check_dtypes = set()
+        nodatavals = []
+        # Check each index before processing 3D array
+        for bidx in indexes:
+            if bidx not in self.indexes:
+                raise IndexError("band index out of range")
+            idx = self.indexes.index(bidx)
+            check_dtypes.add(self.dtypes[idx])
+            nodatavals.append(self._nodatavals[idx])
+        if len(check_dtypes) > 1:
+            raise ValueError("more than one 'dtype' found")
+        elif len(check_dtypes) == 0:
+            dtype = self.dtypes[0]
+        else:  # unique dtype; normal case
+            dtype = check_dtypes.pop()
+        out_shape = (len(indexes),) + (
+            window
+            and window_shape(window, self.height, self.width)
+            or self.shape)
+        if out is not None:
+            if out.dtype != dtype:
+                raise ValueError(
+                    "the array's dtype '%s' does not match "
+                    "the file's dtype '%s'" % (out.dtype, dtype))
+            if out.shape[0] != out_shape[0]:
+                raise ValueError(
+                    "'out' shape %s does not mach raster slice shape %s" %
+                    (out.shape, out_shape))
+            if masked is None:
+                masked = hasattr(out, 'mask')
+        if masked is None:
+            masked = any([x is not None for x in nodatavals])
+        if out is None:
+            if masked:
+                out = np.ma.empty(out_shape, dtype)
+            else:
+                out = np.empty(out_shape, dtype)
+
+        # Prepare the IO window.
+        if window:
+            window = eval_window(window, self.height, self.width)
+            yoff = <int>window[0][0]
+            xoff = <int>window[1][0]
+            height = <int>window[0][1] - yoff
+            width = <int>window[1][1] - xoff
+        else:
+            xoff = yoff = <int>0
+            width = <int>self.width
+            height = <int>self.height
+
+        # Call io_multi* functions with C type args so that they
+        # can release the GIL.
+        indexes_arr = np.array(indexes, dtype=int)
+        indexes_count = <int>indexes_arr.shape[0]
+        gdt = dtypes.dtype_rev[dtype]
+
+        if gdt == 1:
+            retval = io_multi_ubyte(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 2:
+            retval = io_multi_uint16(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 3:
+            retval = io_multi_int16(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 4:
+            retval = io_multi_uint32(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 5:
+            retval = io_multi_int32(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 6:
+            retval = io_multi_float32(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 7:
+            retval = io_multi_float64(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 8:
+            retval = io_multi_cint16(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 9:
+            retval = io_multi_cint32(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 10:
+            retval = io_multi_cfloat32(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+        elif gdt == 11:
+            retval = io_multi_cfloat64(
+                            self._hds, 0, xoff, yoff, width, height,
+                            out, indexes_arr, indexes_count)
+
+        if retval in (1, 2, 3):
+            raise IOError("Read or write failed")
+        elif retval == 4:
+            raise ValueError("NULL band")
+
+        # Masking the output. TODO: explain the logic better.
+        if masked:
+            test1nodata = set(nodatavals)
+            if len(test1nodata) == 1:
+                if nodatavals[0] is None:
+                    out = np.ma.masked_array(out, copy=False)
+                elif np.isnan(nodatavals[0]):
+                    out = np.ma.masked_where(np.isnan(out), out, copy=False)
+                else:
+                    out = np.ma.masked_equal(out, nodatavals[0], copy=False)
+            else:
+                out = np.ma.masked_array(out, copy=False)
+                for aix in range(len(indexes)):
+                    if nodatavals[aix] is None:
+                        band_mask = False
+                    elif np.isnan(nodatavals[aix]):
+                        band_mask = np.isnan(out[aix])
+                    else:
+                        band_mask = out[aix] == nodatavals[aix]
+                    out[aix].mask = band_mask
+        
+        if return2d:
+            out.shape = out.shape[1:]
+        return out
+
+    def read_mask(self, out=None, window=None):
+        """Read the mask band into an `out` array if provided, 
+        otherwise return a new array containing the dataset's
+        valid data 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
+        cdef void *hmask
+        if self._hds == NULL:
+            raise ValueError("can't write closed raster file")
+        hband = _gdal.GDALGetRasterBand(self._hds, 1)
+        if hband == NULL:
+            raise ValueError("NULL band mask")
+        hmask = _gdal.GDALGetMaskBand(hband)
+        if hmask == NULL:
+            return None
+        if out is None:
+            out_shape = (
+                window 
+                and window_shape(window, self.height, self.width) 
+                or self.shape)
+            out = np.empty(out_shape, np.uint8)
+        if window:
+            window = eval_window(window, self.height, self.width)
+            yoff = window[0][0]
+            xoff = window[1][0]
+            height = window[0][1] - yoff
+            width = window[1][1] - xoff
+        else:
+            xoff = yoff = 0
+            width = self.width
+            height = self.height
+        retval = io_ubyte(
+            hmask, 0, xoff, yoff, width, height, out)
+        return out
+
+
+cdef class RasterUpdater(RasterReader):
+    # Read-write access to raster data and metadata.
+    # TODO: the r+ mode.
+
+    def __init__(
+            self, path, mode, driver=None,
+            width=None, height=None, count=None, 
+            crs=None, transform=None, dtype=None,
+            nodata=None,
+            **kwargs):
+        # Validate write mode arguments.
+        if mode == 'w':
+            if not isinstance(driver, string_types):
+                raise TypeError("A driver name string is required.")
+            try:
+                width = int(width)
+                height = int(height)
+            except:
+                raise TypeError("Integer width and height are required.")
+            try:
+                count = int(count)
+            except:
+                raise TypeError("Integer band count is required.")
+            try:
+                assert dtype is not None
+                _ = np.dtype(dtype)
+            except:
+                raise TypeError("A valid dtype is required.")
+        self.name = path
+        self.mode = mode
+        self.driver = driver
+        self.width = width
+        self.height = height
+        self._count = count
+        self._init_dtype = np.dtype(dtype).name
+        self._init_nodata = nodata
+        self._hds = NULL
+        self._count = count
+        self._crs = crs
+        if transform is not None:
+            self._transform = transform.to_gdal()
+        self._closed = True
+        self._dtypes = []
+        self._nodatavals = []
+        self._options = kwargs.copy()
+    
+    def __repr__(self):
+        return "<%s RasterUpdater name='%s' mode='%s'>" % (
+            self.closed and 'closed' or 'open', 
+            self.name,
+            self.mode)
+
+    def start(self):
+        cdef const char *drv_name = NULL
+        cdef char **options = NULL
+        cdef char *key_c, *val_c = NULL
+        cdef void *drv = NULL
+        cdef void *hband = NULL
+        cdef int success
+        name_b = self.name.encode('utf-8')
+        cdef const char *fname = name_b
+
+        # Is there not a driver manager already?
+        if driver_count() == 0 and not self.env:
+            # create a local manager and enter
+            self.env = GDALEnv(True)
+        else:
+            self.env = GDALEnv(False)
+        self.env.start()
+        
+        kwds = []
+
+        if self.mode == 'w':
+            # GDAL can Create() GTiffs. Many other formats only support
+            # CreateCopy(). Rasterio lets you write GTiffs *only* for now.
+            if self.driver not in ['GTiff']:
+                raise ValueError("only GTiffs can be opened in 'w' mode")
+
+            # Delete existing file, create.
+            if os.path.exists(self.name):
+                os.unlink(self.name)
+            
+            driver_b = self.driver.encode('utf-8')
+            drv_name = driver_b
+            drv = _gdal.GDALGetDriverByName(drv_name)
+            if drv == NULL:
+                raise ValueError("NULL driver for %s", self.driver)
+            
+            # Find the equivalent GDAL data type or raise an exception
+            # We've mapped numpy scalar types to GDAL types so see
+            # if we can crosswalk those.
+            if hasattr(self._init_dtype, 'type'):
+                tp = self._init_dtype.type
+                if tp not in dtypes.dtype_rev:
+                    raise ValueError(
+                        "Unsupported dtype: %s" % self._init_dtype)
+                else:
+                    gdal_dtype = dtypes.dtype_rev.get(tp)
+            else:
+                gdal_dtype = dtypes.dtype_rev.get(self._init_dtype)
+
+            # Creation options
+            for k, v in self._options.items():
+                # Skip items that are definitely *not* valid driver options.
+                if k.lower() in ['affine']:
+                    continue
+                kwds.append((k.lower(), v))
+                k, v = k.upper(), str(v).upper()
+                key_b = k.encode('utf-8')
+                val_b = v.encode('utf-8')
+                key_c = key_b
+                val_c = val_b
+                options = _gdal.CSLSetNameValue(options, key_c, val_c)
+                log.debug(
+                    "Option: %r\n", 
+                    (k, _gdal.CSLFetchNameValue(options, key_c)))
+
+            self._hds = _gdal.GDALCreate(
+                drv, fname, self.width, self.height, self._count,
+                gdal_dtype, options)
+            if self._hds == NULL:
+                raise ValueError("NULL dataset")
+
+            if self._init_nodata is not None:
+                for i in range(self._count):
+                    hband = _gdal.GDALGetRasterBand(self._hds, i+1)
+                    success = _gdal.GDALSetRasterNoDataValue(
+                                    hband, self._init_nodata)
+
+            if self._transform:
+                self.write_transform(self._transform)
+            if self._crs:
+                self.set_crs(self._crs)
+        
+        elif self.mode == 'r+':
+            with cpl_errs:
+                self._hds = _gdal.GDALOpen(fname, 1)
+            if self._hds == NULL:
+                raise ValueError("NULL dataset")
+
+        drv = _gdal.GDALGetDatasetDriver(self._hds)
+        drv_name = _gdal.GDALGetDriverShortName(drv)
+        self.driver = drv_name.decode('utf-8')
+
+        self._count = _gdal.GDALGetRasterCount(self._hds)
+        self.width = _gdal.GDALGetRasterXSize(self._hds)
+        self.height = _gdal.GDALGetRasterYSize(self._hds)
+        self.shape = (self.height, self.width)
+
+        self._transform = self.read_transform()
+        self._crs = self.read_crs()
+        self._crs_wkt = self.read_crs_wkt()
+
+        if options != NULL:
+            _gdal.CSLDestroy(options)
+        
+        # touch self.meta
+        _ = self.meta
+
+        self.update_tags(ns='rio_creation_kwds', **kwds)
+        self._closed = False
+
+    def set_crs(self, crs):
+        """Writes a coordinate reference system to the dataset."""
+        cdef char *proj_c = NULL
+        cdef char *wkt = NULL
+        if self._hds == NULL:
+            raise ValueError("Can't read closed raster file")
+        cdef void *osr = _gdal.OSRNewSpatialReference(NULL)
+        if osr == NULL:
+            raise ValueError("Null spatial reference")
+        params = []
+
+        log.debug("Input CRS: %r", crs)
+
+        # Normally, we expect a CRS dict.
+        if isinstance(crs, dict):
+            # EPSG is a special case.
+            init = crs.get('init')
+            if init:
+                auth, val = init.split(':')
+                if auth.upper() == 'EPSG':
+                    _gdal.OSRImportFromEPSG(osr, int(val))
+            else:
+                crs['wktext'] = True
+                for k, v in crs.items():
+                    if v is True or (k in ('no_defs', 'wktext') and v):
+                        params.append("+%s" % k)
+                    else:
+                        params.append("+%s=%s" % (k, v))
+                proj = " ".join(params)
+                log.debug("PROJ.4 to be imported: %r", proj)
+                proj_b = proj.encode('utf-8')
+                proj_c = proj_b
+                _gdal.OSRImportFromProj4(osr, proj_c)
+        # Fall back for CRS strings like "EPSG:3857."
+        else:
+            proj_b = crs.encode('utf-8')
+            proj_c = proj_b
+            _gdal.OSRSetFromUserInput(osr, proj_c)
+
+        # Fixup, export to WKT, and set the GDAL dataset's projection.
+        _gdal.OSRFixup(osr)
+        _gdal.OSRExportToWkt(osr, &wkt)
+        wkt_b = wkt
+        log.debug("Exported WKT: %s", wkt_b.decode('utf-8'))
+        _gdal.GDALSetProjection(self._hds, wkt)
+
+        _gdal.CPLFree(wkt)
+        _gdal.OSRDestroySpatialReference(osr)
+        self._crs = crs
+        log.debug("Self CRS: %r", self._crs)
+
+    property crs:
+        """A mapping of PROJ.4 coordinate reference system params.
+        """
+
+        def __get__(self):
+            return self.get_crs()
+
+        def __set__(self, value):
+            self.set_crs(value)
+
+    def write_transform(self, transform):
+        if self._hds == NULL:
+            raise ValueError("Can't read closed raster file")
+        cdef double gt[6]
+        for i in range(6):
+            gt[i] = transform[i]
+        retval = _gdal.GDALSetGeoTransform(self._hds, gt)
+        self._transform = transform
+
+    property transform:
+        """An affine transformation that maps pixel row/column
+        coordinates to coordinates in the specified crs. The affine
+        transformation is represented by a six-element sequence.
+        Reference system coordinates can be calculated by the
+        following formula
+
+        X = Item 0 + Column * Item 1 + Row * Item 2
+        Y = Item 3 + Column * Item 4 + Row * Item 5
+
+        See also this class's ul() method.
+        """
+
+        def __get__(self):
+            return Affine.from_gdal(*self.get_transform())
+
+        def __set__(self, value):
+            self.write_transform(value.to_gdal())
+
+    def set_nodatavals(self, vals):
+        cdef void *hband = NULL
+        cdef double val
+        for i, val in zip(self.indexes, vals):
+            hband = _gdal.GDALGetRasterBand(self._hds, i)
+            success = _gdal.GDALSetRasterNoDataValue(hband, val)
+            if success:
+                raise ValueError("Invalid nodata values")
+        self._nodatavals = vals
+
+    property nodatavals:
+        """A list by band of a dataset's nodata values.
+        """
+
+        def __get__(self):
+            return self.get_nodatavals()
+
+        def __set__(self, value):
+            self.set_nodatavals(value)
+
+
+    def write(self, src, indexes=None, window=None):
+        """Write the src array into indexed bands of the dataset.
+
+        If `indexes` is a list, the src must be a 3D array of
+        matching shape. If an int, the src must be a 2D array.
+
+        See `read()` for usage of the optional `window` argument.
+        """
+        cdef int height, width, xoff, yoff, indexes_count
+        cdef int retval = 0
+
+        if self._hds == NULL:
+            raise ValueError("can't write to closed raster file")
+
+        if indexes is None:
+            indexes = self.indexes
+        elif isinstance(indexes, int):
+            indexes = [indexes]
+            src = np.array([src])
+        if len(src.shape) != 3 or src.shape[0] != len(indexes):
+            raise ValueError(
+                "Source shape is inconsistent with given indexes")
+
+        check_dtypes = set()
+        # Check each index before processing 3D array
+        for bidx in indexes:
+            if bidx not in self.indexes:
+                raise IndexError("band index out of range")
+            idx = self.indexes.index(bidx)
+            check_dtypes.add(self.dtypes[idx])
+        if len(check_dtypes) > 1:
+            raise ValueError("more than one 'dtype' found")
+        elif len(check_dtypes) == 0:
+            dtype = self.dtypes[0]
+        else:  # unique dtype; normal case
+            dtype = check_dtypes.pop()
+
+        if src is not None and src.dtype != dtype:
+            raise ValueError(
+                "the array's dtype '%s' does not match "
+                "the file's dtype '%s'" % (src.dtype, dtype))
+
+        # Require C-continguous arrays (see #108).
+        src = np.require(src, dtype=dtype, requirements='C')
+
+        # Prepare the IO window.
+        if window:
+            window = eval_window(window, self.height, self.width)
+            yoff = <int>window[0][0]
+            xoff = <int>window[1][0]
+            height = <int>window[0][1] - yoff
+            width = <int>window[1][1] - xoff
+        else:
+            xoff = yoff = <int>0
+            width = <int>self.width
+            height = <int>self.height
+
+        # Call io_multi* functions with C type args so that they
+        # can release the GIL.
+        indexes_arr = np.array(indexes, dtype=int)
+        indexes_count = <int>indexes_arr.shape[0]
+        gdt = dtypes.dtype_rev[dtype]
+        if gdt == 1:
+            retval = io_multi_ubyte(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 2:
+            retval = io_multi_uint16(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 3:
+            retval = io_multi_int16(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 4:
+            retval = io_multi_uint32(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 5:
+            retval = io_multi_int32(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 6:
+            retval = io_multi_float32(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 7:
+            retval = io_multi_float64(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 8:
+            retval = io_multi_cint16(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 9:
+            retval = io_multi_cint32(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 10:
+            retval = io_multi_cfloat32(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+        elif gdt == 11:
+            retval = io_multi_cfloat64(
+                            self._hds, 1, xoff, yoff, width, height,
+                            src, indexes_arr, indexes_count)
+
+        if retval in (1, 2, 3):
+            raise IOError("Read or write failed")
+        elif retval == 4:
+            raise ValueError("NULL band")
+
+    def write_band(self, bidx, src, window=None):
+        """Write the src array into the `bidx` band.
+
+        Band indexes begin with 1: read_band(1) returns the first band.
+
+        The optional `window` argument takes a tuple like:
+
+            ((row_start, row_stop), (col_start, col_stop))
+
+        specifying a raster subset to write into.
+        """
+        self.write(src, bidx, window=window)
+
+    def update_tags(self, bidx=0, ns=None, **kwargs):
+        """Updates the tags of a dataset or one of its bands.
+
+        Tags are pairs of key and value strings. Tags belong to
+        namespaces.  The standard namespaces are: default (None) and
+        'IMAGE_STRUCTURE'.  Applications can create their own additional
+        namespaces.
+
+        The optional bidx argument can be used to select the dataset
+        band. The optional ns argument can be used to select a namespace
+        other than the default.
+        """
+        cdef char *key_c = NULL
+        cdef char *value_c = NULL
+        cdef void *hobj = NULL
+        cdef const char *domain_c = NULL
+        cdef char **papszStrList = NULL
+        if self._hds == NULL:
+            raise ValueError("can't read closed raster file")
+        if bidx > 0:
+            if bidx not in self.indexes:
+                raise ValueError("Invalid band index")
+            hobj = _gdal.GDALGetRasterBand(self._hds, bidx)
+            if hobj == NULL:
+                raise ValueError("NULL band")
+        else:
+            hobj = self._hds
+        if ns:
+            domain_b = ns.encode('utf-8')
+            domain_c = domain_b
+        else:
+            domain_c = NULL
+        
+        papszStrList = _gdal.CSLDuplicate(
+            _gdal.GDALGetMetadata(hobj, domain_c))
+
+        for key, value in kwargs.items():
+            key_b = text_type(key).encode('utf-8')
+            value_b = text_type(value).encode('utf-8')
+            key_c = key_b
+            value_c = value_b
+            papszStrList = _gdal.CSLSetNameValue(
+                    papszStrList, key_c, value_c)
+
+        retval = _gdal.GDALSetMetadata(hobj, papszStrList, domain_c)
+        if papszStrList != NULL:
+            _gdal.CSLDestroy(papszStrList)
+
+        if retval == 2:
+            log.warn("Tags accepted but may not be persisted.")
+        elif retval == 3:
+            raise RuntimeError("Tag update failed.")
+
+    def write_colormap(self, bidx, colormap):
+        """Write a colormap for a band to the dataset."""
+        cdef void *hBand
+        cdef void *hTable
+        cdef _gdal.GDALColorEntry color
+        if self._hds == NULL:
+            raise ValueError("can't read closed raster file")
+        if bidx > 0:
+            if bidx not in self.indexes:
+                raise ValueError("Invalid band index")
+            hBand = _gdal.GDALGetRasterBand(self._hds, bidx)
+            if hBand == NULL:
+                raise ValueError("NULL band")
+        # RGB only for now. TODO: the other types.
+        # 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 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.GDALSetRasterColorTable(hBand, hTable)
+        _gdal.GDALDestroyColorTable(hTable)
+
+    def write_mask(self, src, window=None):
+        """Write the valid data mask src array into the dataset's band
+        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
+        cdef void *hmask
+        if self._hds == NULL:
+            raise ValueError("can't write closed raster file")
+        hband = _gdal.GDALGetRasterBand(self._hds, 1)
+        if hband == NULL:
+            raise ValueError("NULL band mask")
+        if _gdal.GDALCreateMaskBand(hband, 0x02) != 0:
+            raise RuntimeError("Failed to create mask")
+        hmask = _gdal.GDALGetMaskBand(hband)
+        if hmask == NULL:
+            raise ValueError("NULL band mask")
+        log.debug("Created mask band")
+        if window:
+            window = eval_window(window, self.height, self.width)
+            yoff = window[0][0]
+            xoff = window[1][0]
+            height = window[0][1] - yoff
+            width = window[1][1] - xoff
+        else:
+            xoff = yoff = 0
+            width = self.width
+            height = self.height
+        if src.dtype == np.bool:
+            array = 255 * src.astype(np.uint8)
+            retval = io_ubyte(
+                hmask, 1, xoff, yoff, width, height, array)
+        else:
+            retval = io_ubyte(
+                hmask, 1, xoff, yoff, width, height, src)
+
+
+cdef class InMemoryRaster:
+    """
+    Class that manages a single-band in memory GDAL raster dataset.  Data type
+    is determined from the data type of the input numpy 2D array (image), and
+    must be one of the data types supported by GDAL
+    (see rasterio.dtypes.dtype_rev).  Data are populated at create time from
+    the 2D array passed in.
+
+    Use the 'with' pattern to instantiate this class for automatic closing
+    of the memory dataset.
+
+    This class includes attributes that are intended to be passed into GDAL
+    functions:
+    self.dataset
+    self.band
+    self.band_ids  (single element array with band ID of this dataset's band)
+    self.transform (GDAL compatible transform array)
+
+    This class is only intended for internal use within rasterio to support
+    IO with GDAL.  Other memory based operations should use numpy arrays.
+    """
+
+    def __cinit__(self, image, transform=None):
+        """
+        Create in-memory raster dataset, and populate its initial values with
+        the values in image.
+
+        :param image: 2D numpy array.  Must be of supported data type
+        (see rasterio.dtypes.dtype_rev)
+        :param transform: GDAL compatible transform array
+        """
+
+        self._image = image
+        self.dataset = NULL
+
+        cdef void *memdriver = _gdal.GDALGetDriverByName("MEM")
+
+        # Several GDAL operations require the array of band IDs as input
+        self.band_ids[0] = 1
+
+        self.dataset = _gdal.GDALCreate(
+            memdriver,
+            "output",
+            image.shape[1],
+            image.shape[0],
+            1,
+            <_gdal.GDALDataType>dtypes.dtype_rev[image.dtype.name],
+            NULL
+        )
+
+        if self.dataset == NULL:
+            raise ValueError("NULL output datasource")
+
+        if transform is not None:
+            for i in range(6):
+                self.transform[i] = transform[i]
+            err = _gdal.GDALSetGeoTransform(self.dataset, self.transform)
+            if err:
+                raise ValueError("transform not set: %s" % transform)
+
+        self.band = _gdal.GDALGetRasterBand(self.dataset, 1)
+        if self.band == NULL:
+            raise ValueError("NULL output band: {0}".format(i))
+
+        self.write(image)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args, **kwargs):
+        self.close()
+
+    def close(self):
+        if self.dataset != NULL:
+            _gdal.GDALClose(self.dataset)
+            self.dataset = NULL
+
+    def read(self):
+        io_auto(self._image, self.band, False)
+        return self._image
+
+    def write(self, image):
+        io_auto(image, self.band, True)
+
+
+cdef class IndirectRasterUpdater(RasterUpdater):
+
+    def __repr__(self):
+        return "<%s IndirectRasterUpdater name='%s' mode='%s'>" % (
+            self.closed and 'closed' or 'open', 
+            self.name,
+            self.mode)
+
+    def start(self):
+        cdef const char *drv_name = NULL
+        cdef void *drv = NULL
+        cdef void *memdrv = NULL
+        cdef void *hband = NULL
+        cdef void *temp = NULL
+        cdef int success
+        name_b = self.name.encode('utf-8')
+        cdef const char *fname = name_b
+
+        memdrv = _gdal.GDALGetDriverByName("MEM")
+
+        # Is there not a driver manager already?
+        if driver_count() == 0 and not self.env:
+            # create a local manager and enter
+            self.env = GDALEnv(True)
+        else:
+            self.env = GDALEnv(False)
+        self.env.start()
+        
+        if self.mode == 'w':
+            # Find the equivalent GDAL data type or raise an exception
+            # We've mapped numpy scalar types to GDAL types so see
+            # if we can crosswalk those.
+            if hasattr(self._init_dtype, 'type'):
+                tp = self._init_dtype.type
+                if tp not in dtypes.dtype_rev:
+                    raise ValueError(
+                        "Unsupported dtype: %s" % self._init_dtype)
+                else:
+                    gdal_dtype = dtypes.dtype_rev.get(tp)
+            else:
+                gdal_dtype = dtypes.dtype_rev.get(self._init_dtype)
+            self._hds = _gdal.GDALCreate(
+                memdrv, "temp", self.width, self.height, self._count,
+                gdal_dtype, NULL)
+            if self._hds == NULL:
+                raise ValueError("NULL dataset")
+            if self._init_nodata is not None:
+                for i in range(self._count):
+                    hband = _gdal.GDALGetRasterBand(self._hds, i+1)
+                    success = _gdal.GDALSetRasterNoDataValue(
+                                    hband, self._init_nodata)
+            if self._transform:
+                self.write_transform(self._transform)
+            if self._crs:
+                self.set_crs(self._crs)
+
+        elif self.mode == 'r+':
+            with cpl_errs:
+                temp = _gdal.GDALOpen(fname, 0)
+            if temp == NULL:
+                raise ValueError("Null dataset")
+            self._hds = _gdal.GDALCreateCopy(
+                                memdrv, "temp", temp, 1, NULL, NULL, NULL)
+            if self._hds == NULL:
+                raise ValueError("NULL dataset")
+            drv = _gdal.GDALGetDatasetDriver(temp)
+            drv_name = _gdal.GDALGetDriverShortName(drv)
+            self.driver = drv_name.decode('utf-8')
+            _gdal.GDALClose(temp)
+
+        self._count = _gdal.GDALGetRasterCount(self._hds)
+        self.width = _gdal.GDALGetRasterXSize(self._hds)
+        self.height = _gdal.GDALGetRasterYSize(self._hds)
+        self.shape = (self.height, self.width)
+
+        self._transform = self.read_transform()
+        self._crs = self.read_crs()
+        self._crs_wkt = self.read_crs_wkt()
+
+        # touch self.meta
+        _ = self.meta
+
+        self._closed = False
+
+    def close(self):
+        cdef const char *drv_name = NULL
+        cdef char **options = NULL
+        cdef char *key_c, *val_c = NULL
+        cdef void *drv = NULL
+        cdef void *temp = NULL
+        cdef int success
+        name_b = self.name.encode('utf-8')
+        cdef const char *fname = name_b
+
+        # Delete existing file, create.
+        if os.path.exists(self.name):
+            os.unlink(self.name)
+
+        driver_b = self.driver.encode('utf-8')
+        drv_name = driver_b
+        drv = _gdal.GDALGetDriverByName(drv_name)
+        if drv == NULL:
+            raise ValueError("NULL driver for %s", self.driver)
+
+        kwds = []
+        # Creation options
+        for k, v in self._options.items():
+            # Skip items that are definitely *not* valid driver options.
+            if k.lower() in ['affine']:
+                continue
+            kwds.append((k.lower(), v))
+            k, v = k.upper(), str(v).upper()
+            key_b = k.encode('utf-8')
+            val_b = v.encode('utf-8')
+            key_c = key_b
+            val_c = val_b
+            options = _gdal.CSLSetNameValue(options, key_c, val_c)
+            log.debug(
+                "Option: %r\n", 
+                (k, _gdal.CSLFetchNameValue(options, key_c)))
+        
+        #self.update_tags(ns='rio_creation_kwds', **kwds)
+        temp = _gdal.GDALCreateCopy(
+                    drv, fname, self._hds, 1, options, NULL, NULL)
+
+        if options != NULL:
+            _gdal.CSLDestroy(options)
+
+        if temp != NULL:
+            _gdal.GDALClose(temp)
+
+
+def writer(path, mode, **kwargs):
+    # Dispatch to direct or indirect writer/updater according to the
+    # format driver's capabilities.
+    cdef void *hds = NULL
+    cdef void *drv = NULL
+    cdef char *drv_name = NULL
+    cdef const char *fname = NULL
+
+    if mode == 'w' and 'driver' in kwargs:
+        if kwargs['driver'] == 'GTiff':
+            return RasterUpdater(path, mode, **kwargs)
+        else:
+            return IndirectRasterUpdater(path, mode, **kwargs)
+    else:
+        # Peek into the dataset at path to determine it's format
+        # driver.
+        name_b = path.encode('utf-8')
+        fname = name_b
+        with cpl_errs:
+            hds = _gdal.GDALOpen(fname, 0)
+        if hds == NULL:
+            raise ValueError("NULL dataset")
+        drv = _gdal.GDALGetDatasetDriver(hds)
+        drv_name = _gdal.GDALGetDriverShortName(drv)
+        drv_name_b = drv_name
+        driver = drv_name_b.decode('utf-8')
+        _gdal.GDALClose(hds)
+        if driver == 'GTiff':
+            return RasterUpdater(path, mode)
+        else:
+            return IndirectRasterUpdater(path, mode)
diff --git a/rasterio/_ogr.pxd b/rasterio/_ogr.pxd
new file mode 100644
index 0000000..9fb3796
--- /dev/null
+++ b/rasterio/_ogr.pxd
@@ -0,0 +1,99 @@
+
+ctypedef int OGRErr
+ctypedef struct OGREnvelope:
+    double MinX
+    double MaxX
+    double MinY
+    double MaxY
+
+cdef extern from "ogr_core.h":
+    char *  OGRGeometryTypeToName(int)
+
+cdef extern from "ogr_api.h":
+    char *  OGR_Dr_GetName (void *driver)
+    void *  OGR_Dr_CreateDataSource (void *driver, char *path, char **options)
+    int     OGR_Dr_DeleteDataSource (void *driver, char *)
+    int     OGR_DS_DeleteLayer (void *datasource, int n)
+    void *  OGR_DS_CreateLayer (void *datasource, char *name, void *crs, int geomType, char **options)
+    void *  OGR_DS_ExecuteSQL (void *datasource, char *name, void *filter, char *dialext)
+    void    OGR_DS_Destroy (void *datasource)
+    void *  OGR_DS_GetDriver (void *layer_defn)
+    void *  OGR_DS_GetLayerByName (void *datasource, char *name)
+    int     OGR_DS_GetLayerCount (void *datasource)
+    void *  OGR_DS_GetLayer (void *datasource, int n)
+    void    OGR_DS_ReleaseResultSet (void *datasource, void *results)
+    int     OGR_DS_SyncToDisk (void *datasource)
+    void *  OGR_F_Create (void *featuredefn)
+    void    OGR_F_Destroy (void *feature)
+    long    OGR_F_GetFID (void *feature)
+    int     OGR_F_IsFieldSet (void *feature, int n)
+    int     OGR_F_GetFieldAsDateTime (void *feature, int n, int *y, int *m, int *d, int *h, int *m, int *s, int *z)
+    double  OGR_F_GetFieldAsDouble (void *feature, int n)
+    int     OGR_F_GetFieldAsInteger (void *feature, int n)
+    char *  OGR_F_GetFieldAsString (void *feature, int n)
+    int     OGR_F_GetFieldCount (void *feature)
+    void *  OGR_F_GetFieldDefnRef (void *feature, int n)
+    int     OGR_F_GetFieldIndex (void *feature, char *name)
+    void *  OGR_F_GetGeometryRef (void *feature)
+    void    OGR_F_SetFieldDateTime (void *feature, int n, int y, int m, int d, int hh, int mm, int ss, int tz)
+    void    OGR_F_SetFieldDouble (void *feature, int n, double value)
+    void    OGR_F_SetFieldInteger (void *feature, int n, int value)
+    void    OGR_F_SetFieldString (void *feature, int n, char *value)
+    int     OGR_F_SetGeometryDirectly (void *feature, void *geometry)
+    void *  OGR_FD_Create (char *name)
+    int     OGR_FD_GetFieldCount (void *featuredefn)
+    void *  OGR_FD_GetFieldDefn (void *featuredefn, int n)
+    int     OGR_FD_GetGeomType (void *featuredefn)
+    char *  OGR_FD_GetName (void *featuredefn)
+    void *  OGR_Fld_Create (char *name, int fieldtype)
+    void    OGR_Fld_Destroy (void *fielddefn)
+    char *  OGR_Fld_GetNameRef (void *fielddefn)
+    int     OGR_Fld_GetPrecision (void *fielddefn)
+    int     OGR_Fld_GetType (void *fielddefn)
+    int     OGR_Fld_GetWidth (void *fielddefn)
+    void    OGR_Fld_Set (void *fielddefn, char *name, int fieldtype, int width, int precision, int justification)
+    void    OGR_Fld_SetPrecision (void *fielddefn, int n)
+    void    OGR_Fld_SetWidth (void *fielddefn, int n)
+    OGRErr  OGR_G_AddGeometryDirectly (void *geometry, void *part)
+    void    OGR_G_AddPoint (void *geometry, double x, double y, double z)
+    void    OGR_G_AddPoint_2D (void *geometry, double x, double y)
+    void    OGR_G_CloseRings (void *geometry)
+    void *  OGR_G_CreateGeometry (int wkbtypecode)
+    void *  OGR_G_CreateGeometryFromJson(char *json)
+    void    OGR_G_DestroyGeometry (void *geometry)
+    unsigned char *  OGR_G_ExportToJson (void *geometry)
+    void    OGR_G_ExportToWkb (void *geometry, int endianness, char *buffer)
+    int     OGR_G_GetCoordinateDimension (void *geometry)
+    int     OGR_G_GetGeometryCount (void *geometry)
+    unsigned char *  OGR_G_GetGeometryName (void *geometry)
+    int     OGR_G_GetGeometryType (void *geometry)
+    void *  OGR_G_GetGeometryRef (void *geometry, int n)
+    int     OGR_G_GetPointCount (void *geometry)
+    double  OGR_G_GetX (void *geometry, int n)
+    double  OGR_G_GetY (void *geometry, int n)
+    double  OGR_G_GetZ (void *geometry, int n)
+    void    OGR_G_ImportFromWkb (void *geometry, unsigned char *bytes, int nbytes)
+    int     OGR_G_WkbSize (void *geometry)
+    OGRErr  OGR_L_CreateFeature (void *layer, void *feature)
+    int     OGR_L_CreateField (void *layer, void *fielddefn, int flexible)
+    OGRErr  OGR_L_GetExtent (void *layer, void *extent, int force)
+    void *  OGR_L_GetFeature (void *layer, int n)
+    int     OGR_L_GetFeatureCount (void *layer, int m)
+    void *  OGR_L_GetLayerDefn (void *layer)
+    char *  OGR_L_GetName (void *layer)
+    void *  OGR_L_GetNextFeature (void *layer)
+    void *  OGR_L_GetSpatialFilter (void *layer)
+    void *  OGR_L_GetSpatialRef (void *layer)
+    void    OGR_L_ResetReading (void *layer)
+    void    OGR_L_SetSpatialFilter (void *layer, void *geometry)
+    void    OGR_L_SetSpatialFilterRect (
+                void *layer, double minx, double miny, double maxx, double maxy
+                )
+    int     OGR_L_TestCapability (void *layer, char *name)
+    void *  OGRGetDriverByName (char *)
+    void *  OGROpen (char *path, int mode, void *x)
+    void *  OGROpenShared (char *path, int mode, void *x)
+    int     OGRReleaseDataSource (void *datasource)
+    void    OGRRegisterAll()
+    void    OGRCleanupAll()
+
diff --git a/rasterio/_warp.pyx b/rasterio/_warp.pyx
new file mode 100644
index 0000000..3137bf2
--- /dev/null
+++ b/rasterio/_warp.pyx
@@ -0,0 +1,532 @@
+# distutils: language = c++
+# cython: profile=True
+#
+
+from collections import namedtuple
+import logging
+
+import numpy as np
+cimport numpy as np
+
+from rasterio cimport _gdal, _ogr, _io, _features
+from rasterio import dtypes
+
+
+cdef extern from "gdalwarper.h" nogil:
+
+    ctypedef struct GDALWarpOptions
+
+    cdef cppclass GDALWarpOperation:
+        GDALWarpOperation() except +
+        int Initialize(const GDALWarpOptions *psNewOptions)
+        const GDALWarpOptions *GetOptions()
+        int ChunkAndWarpImage( 
+            int nDstXOff, int nDstYOff, int nDstXSize, int nDstYSize )
+        int ChunkAndWarpMulti( 
+            int nDstXOff, int nDstYOff, int nDstXSize, int nDstYSize )
+        int WarpRegion( int nDstXOff, int nDstYOff, 
+                        int nDstXSize, int nDstYSize,
+                        int nSrcXOff=0, int nSrcYOff=0,
+                        int nSrcXSize=0, int nSrcYSize=0,
+                        double dfProgressBase=0.0, double dfProgressScale=1.0)
+        int WarpRegionToBuffer( int nDstXOff, int nDstYOff, 
+                                int nDstXSize, int nDstYSize, 
+                                void *pDataBuf, 
+                                int eBufDataType,
+                                int nSrcXOff=0, int nSrcYOff=0,
+                                int nSrcXSize=0, int nSrcYSize=0,
+                                double dfProgressBase=0.0, 
+                                double dfProgressScale=1.0)
+
+
+RESAMPLING = namedtuple('RESAMPLING', [
+                'nearest', 
+                'bilinear', 
+                'cubic', 
+                'cubic_spline', 
+                'lanczos', 
+                'average', 
+                'mode'] )(*list(range(7)))
+
+
+cdef extern from "ogr_geometry.h" nogil:
+
+    cdef cppclass OGRGeometry:
+        pass
+
+    cdef cppclass OGRGeometryFactory:
+        void * transformWithOptions(void *geom, void *ct, char **options)
+#            const OGRGeometry* poSrcGeom,
+#            OGRCoordinateTransformation *poCT,
+#            char** papszOptions
+
+
+cdef extern from "ogr_spatialref.h":
+
+    cdef cppclass OGRCoordinateTransformation:
+        pass
+
+
+log = logging.getLogger('rasterio')
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+log.addHandler(NullHandler())
+
+
+def tastes_like_gdal(t):
+    return t[2] == t[4] == 0.0 and t[1] > 0 and t[5] < 0
+
+
+cdef void *_osr_from_crs(object crs):
+    cdef char *proj_c = NULL
+    cdef void *osr
+    osr = _gdal.OSRNewSpatialReference(NULL)
+    params = []
+    # Normally, we expect a CRS dict.
+    if isinstance(crs, dict):
+        # EPSG is a special case.
+        init = crs.get('init')
+        if init:
+            auth, val = init.split(':')
+            if auth.upper() == 'EPSG':
+                _gdal.OSRImportFromEPSG(osr, int(val))
+        else:
+            crs['wktext'] = True
+            for k, v in crs.items():
+                if v is True or (k in ('no_defs', 'wktext') and v):
+                    params.append("+%s" % k)
+                else:
+                    params.append("+%s=%s" % (k, v))
+            proj = " ".join(params)
+            log.debug("PROJ.4 to be imported: %r", proj)
+            proj_b = proj.encode('utf-8')
+            proj_c = proj_b
+            _gdal.OSRImportFromProj4(osr, proj_c)
+    # Fall back for CRS strings like "EPSG:3857."
+    else:
+        proj_b = crs.encode('utf-8')
+        proj_c = proj_b
+        _gdal.OSRSetFromUserInput(osr, proj_c)
+    return osr
+
+
+def _transform(src_crs, dst_crs, xs, ys):
+    cdef double *x, *y
+    cdef char *proj_c = NULL
+    cdef void *src, *dst
+    cdef void *transform
+    cdef int i
+
+    assert len(xs) == len(ys)
+    
+    src = _osr_from_crs(src_crs)
+    dst = _osr_from_crs(dst_crs)
+
+    n = len(xs)
+    x = <double *>_gdal.CPLMalloc(n*sizeof(double))
+    y = <double *>_gdal.CPLMalloc(n*sizeof(double))
+    for i in range(n):
+        x[i] = xs[i]
+        y[i] = ys[i]
+
+    transform = _gdal.OCTNewCoordinateTransformation(src, dst)
+    res = _gdal.OCTTransform(transform, n, x, y, NULL)
+    #if res:
+    #    raise ValueError("Failed coordinate transformation")
+
+    res_xs = [0]*n
+    res_ys = [0]*n
+
+    for i in range(n):
+        res_xs[i] = x[i]
+        res_ys[i] = y[i]
+
+    _gdal.CPLFree(x)
+    _gdal.CPLFree(y)
+    _gdal.OCTDestroyCoordinateTransformation(transform)
+    _gdal.OSRDestroySpatialReference(src)
+    _gdal.OSRDestroySpatialReference(dst)
+    return res_xs, res_ys
+
+
+def _transform_geom(
+        src_crs, dst_crs, geom, antimeridian_cutting, antimeridian_offset,
+        precision):
+    """Return a transformed geometry."""
+    cdef char *proj_c = NULL
+    cdef char *key_c = NULL
+    cdef char *val_c = NULL
+    cdef char **options = NULL
+    cdef void *src, *dst
+    cdef void *transform
+    cdef OGRGeometryFactory *factory
+    cdef void *src_ogr_geom
+    cdef void *dst_ogr_geom
+    cdef int i
+
+    src = _osr_from_crs(src_crs)
+    dst = _osr_from_crs(dst_crs)
+    transform = _gdal.OCTNewCoordinateTransformation(src, dst)
+
+    # Transform options.
+    val_b = str(antimeridian_offset).encode('utf-8')
+    val_c = val_b
+    options = _gdal.CSLSetNameValue(
+                options, "DATELINEOFFSET", val_c)
+    if antimeridian_cutting:
+        options = _gdal.CSLSetNameValue(options, "WRAPDATELINE", "YES")
+
+    factory = new OGRGeometryFactory()
+    src_ogr_geom = _features.OGRGeomBuilder().build(geom)
+    dst_ogr_geom = factory.transformWithOptions(
+                    <const OGRGeometry *>src_ogr_geom,
+                    <OGRCoordinateTransformation *>transform,
+                    options)
+    g = _features.GeomBuilder().build(dst_ogr_geom)
+
+    _ogr.OGR_G_DestroyGeometry(dst_ogr_geom)
+    _ogr.OGR_G_DestroyGeometry(src_ogr_geom)
+    _gdal.OCTDestroyCoordinateTransformation(transform)
+    if options != NULL:
+        _gdal.CSLDestroy(options)
+    _gdal.OSRDestroySpatialReference(src)
+    _gdal.OSRDestroySpatialReference(dst)
+
+    if precision >= 0:
+        if g['type'] == 'Point':
+            x, y = g['coordinates']
+            x = round(x, precision)
+            y = round(y, precision)
+            new_coords = [x, y]
+        elif g['type'] in ['LineString', 'MultiPoint']:
+            xp, yp = zip(*g['coordinates'])
+            xp = [round(v, precision) for v in xp]
+            yp = [round(v, precision) for v in yp]
+            new_coords = list(zip(xp, yp))
+        elif g['type'] in ['Polygon', 'MultiLineString']:
+            new_coords = []
+            for piece in g['coordinates']:
+                xp, yp = zip(*piece)
+                xp = [round(v, precision) for v in xp]
+                yp = [round(v, precision) for v in yp]
+                new_coords.append(list(zip(xp, yp)))
+        elif g['type'] == 'MultiPolygon':
+            parts = g['coordinates']
+            new_coords = []
+            for part in parts:
+                inner_coords = []
+                for ring in part:
+                    xp, yp = zip(*ring)
+                    xp = [round(v, precision) for v in xp]
+                    yp = [round(v, precision) for v in yp]
+                    inner_coords.append(list(zip(xp, yp)))
+                new_coords.append(inner_coords)
+        g['coordinates'] = new_coords
+
+    return g
+
+
+def _reproject(
+        source, destination,
+        src_transform=None, src_crs=None, 
+        dst_transform=None, dst_crs=None,
+        resampling=RESAMPLING.nearest, 
+        **kwargs):
+    """Reproject a source raster to a destination.
+
+    If the source and destination are ndarrays, coordinate reference
+    system definitions and affine transformation parameters are required
+    for reprojection.
+
+    If the source and destination are rasterio Bands, shorthand for
+    bands of datasets on disk, the coordinate reference systems and
+    transforms will be read from the appropriate datasets.
+    """
+    cdef int retval, rows, cols
+    cdef void *hrdriver
+    cdef void *hdsin
+    cdef void *hdsout
+    cdef void *hbandin
+    cdef void *hbandout
+    cdef _io.RasterReader rdr
+    cdef _io.RasterUpdater udr
+    cdef _io.GDALAccess GA
+    cdef double gt[6]
+    cdef char *srcwkt = NULL
+    cdef char *dstwkt= NULL
+    cdef const char *proj_c
+    cdef void *osr
+    cdef char **warp_extras
+    cdef char *key_c
+    cdef char *val_c
+    cdef const char* pszWarpThreads
+
+    # If the source is an ndarray, we copy to a MEM dataset.
+    # We need a src_transform and src_dst in this case. These will
+    # be copied to the MEM dataset.
+    if isinstance(source, np.ndarray):
+        # Convert 2D single-band arrays to 3D multi-band.
+        if len(source.shape) == 2:
+            source = source.reshape(1, *source.shape)
+        src_count = source.shape[0]
+        rows = source.shape[1]
+        cols = source.shape[2]
+        dtype = np.dtype(source.dtype).name
+        hrdriver = _gdal.GDALGetDriverByName("MEM")
+        if hrdriver == NULL:
+            raise ValueError("NULL driver for 'MEM'")
+
+        hdsin = _gdal.GDALCreate(
+                    hrdriver, "input", cols, rows, 
+                    src_count, dtypes.dtype_rev[dtype], NULL)
+        if hdsin == NULL:
+            raise ValueError("NULL input datasource")
+        _gdal.GDALSetDescription(
+            hdsin, "Temporary source dataset for _reproject()")
+        log.debug("Created temp source dataset")
+        for i in range(6):
+            gt[i] = src_transform[i]
+        retval = _gdal.GDALSetGeoTransform(hdsin, gt)
+        log.debug("Set transform on temp source dataset: %d", retval)
+        osr = _gdal.OSRNewSpatialReference(NULL)
+        if osr == NULL:
+            raise ValueError("Null spatial reference")
+        params = []
+        for k, v in src_crs.items():
+            if v is True or (k == 'no_defs' and v):
+                params.append("+%s" % k)
+            else:
+                params.append("+%s=%s" % (k, v))
+        proj = " ".join(params)
+        proj_b = proj.encode()
+        proj_c = proj_b
+        _gdal.OSRImportFromProj4(osr, proj_c)
+        _gdal.OSRExportToWkt(osr, &srcwkt)
+        _gdal.GDALSetProjection(hdsin, srcwkt)
+        _gdal.CPLFree(srcwkt)
+        _gdal.OSRDestroySpatialReference(osr)
+        log.debug("Set CRS on temp source dataset: %s", srcwkt)
+        
+        # Copy arrays to the dataset.
+        #hbandin = _gdal.GDALGetRasterBand(hdsin, 1)
+        #if hbandin == NULL:
+        #    raise ValueError("NULL input band")
+        #log.debug("Got temp source band")
+        indexes = np.array(range(1, src_count+1))
+        if dtype == dtypes.ubyte:
+            retval = _io.io_multi_ubyte(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        elif dtype == dtypes.uint16:
+            retval = _io.io_multi_uint16(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        elif dtype == dtypes.int16:
+            retval = _io.io_multi_int16(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        elif dtype == dtypes.uint32:
+            retval = _io.io_multi_uint32(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        elif dtype == dtypes.int32:
+            retval = _io.io_multi_int32(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        elif dtype == dtypes.float32:
+            retval = _io.io_multi_float32(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        elif dtype == dtypes.float64:
+            retval = _io.io_multi_float64(
+                hdsin, 1, 0, 0, cols, rows, source, indexes, src_count)
+        else:
+            raise ValueError("Invalid dtype")
+        # TODO: handle errors (by retval).
+        log.debug("Wrote array to temp source dataset")
+    
+    # If the source is a rasterio Band, no copy necessary.
+    elif isinstance(source, tuple):
+        rdr = source.ds
+        hdsin = rdr._hds
+        src_count = 1
+    else:
+        raise ValueError("Invalid source")
+    
+    # Next, do the same for the destination raster.
+    if isinstance(destination, np.ndarray):
+        if len(destination.shape) == 2:
+            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'")
+        _, rows, cols = destination.shape
+        hdsout = _gdal.GDALCreate(
+                        hrdriver, "output", cols, rows, src_count, 
+                        dtypes.dtype_rev[np.dtype(destination.dtype).name], NULL)
+        if hdsout == NULL:
+            raise ValueError("NULL output datasource")
+        _gdal.GDALSetDescription(
+            hdsout, "Temporary destination dataset for _reproject()")
+        log.debug("Created temp destination dataset")
+        for i in range(6):
+            gt[i] = dst_transform[i]
+        retval = _gdal.GDALSetGeoTransform(hdsout, gt)
+        log.debug("Set transform on temp destination dataset: %d", retval)
+        osr = _gdal.OSRNewSpatialReference(NULL)
+        if osr == NULL:
+            raise ValueError("Null spatial reference")
+        params = []
+        for k, v in dst_crs.items():
+            if v is True or (k == 'no_defs' and v):
+                params.append("+%s" % k)
+            else:
+                params.append("+%s=%s" % (k, v))
+        proj = " ".join(params)
+        log.debug("Proj4 string: %s", proj)
+        proj_b = proj.encode()
+        proj_c = proj_b
+        _gdal.OSRImportFromProj4(osr, proj_c)
+        _gdal.OSRExportToWkt(osr, &dstwkt)
+        retval = _gdal.GDALSetProjection(hdsout, dstwkt)
+        log.debug("Setting Projection: %d", retval)
+        _gdal.CPLFree(dstwkt)
+        _gdal.OSRDestroySpatialReference(osr)
+        log.debug("Set CRS on temp destination dataset: %s", dstwkt)
+
+    elif isinstance(destination, tuple):
+        udr = destination.ds
+        hdsout = udr._hds
+    else:
+        raise ValueError("Invalid destination")
+    
+    cdef void *hTransformArg = NULL
+    cdef _gdal.GDALWarpOptions *psWOptions = NULL
+    cdef GDALWarpOperation *oWarper = new GDALWarpOperation()
+    reprojected = False
+
+    try:
+        hTransformArg = _gdal.GDALCreateGenImgProjTransformer(
+                                            hdsin, NULL, hdsout, NULL, 
+                                            1, 1000.0, 0)
+        if hTransformArg == NULL:
+            raise ValueError("NULL transformer")
+        log.debug("Created transformer")
+
+        psWOptions = _gdal.GDALCreateWarpOptions()
+        
+        warp_extras = psWOptions.papszWarpOptions
+        for k, v in kwargs.items():
+            k, v = k.upper(), str(v).upper()
+            key_b = k.encode('utf-8')
+            val_b = v.encode('utf-8')
+            key_c = key_b
+            val_c = val_b
+            warp_extras = _gdal.CSLSetNameValue(warp_extras, key_c, val_c)
+        
+        pszWarpThreads = _gdal.CSLFetchNameValue(warp_extras, "NUM_THREADS")
+        if pszWarpThreads == NULL:
+            pszWarpThreads = _gdal.CPLGetConfigOption(
+                                "GDAL_NUM_THREADS", "1")
+            warp_extras = _gdal.CSLSetNameValue(
+                warp_extras, "NUM_THREADS", pszWarpThreads)
+
+        log.debug("Created warp options")
+    
+        psWOptions.eResampleAlg = <_gdal.GDALResampleAlg>resampling
+        # TODO: Approximate transformations.
+        #if maxerror > 0.0:
+        #    psWOptions.pTransformerArg = _gdal.GDALCreateApproxTransformer(
+        #                                    _gdal.GDALGenImgProjTransform, 
+        #                                    hTransformArg, 
+        #                                    maxerror )
+        #    psWOptions.pfnTransformer = _gdal.GDALApproxTransform
+        #else:
+        psWOptions.pfnTransformer = _gdal.GDALGenImgProjTransform
+        psWOptions.pTransformerArg = hTransformArg
+        psWOptions.hSrcDS = hdsin
+        psWOptions.hDstDS = hdsout
+        psWOptions.nBandCount = src_count
+        psWOptions.panSrcBands = <int *>_gdal.CPLMalloc(src_count*sizeof(int))
+        psWOptions.panDstBands = <int *>_gdal.CPLMalloc(src_count*sizeof(int))
+        if isinstance(source, tuple):
+            psWOptions.panSrcBands[0] = source.bidx
+        else:
+            for i in range(src_count):
+                psWOptions.panSrcBands[i] = i+1
+        if isinstance(destination, tuple):
+            psWOptions.panDstBands[0] = destination.bidx
+        else:
+            for i in range(src_count):
+                psWOptions.panDstBands[i] = i+1
+        log.debug("Set transformer options")
+
+        # TODO: Src nodata and alpha band.
+
+        eErr = oWarper.Initialize(psWOptions)
+        if eErr == 0:
+            _, rows, cols = destination.shape
+            log.debug(
+                "Chunk and warp window: %d, %d, %d, %d",
+                0, 0, cols, rows)
+            with nogil:
+                eErr = oWarper.ChunkAndWarpMulti(0, 0, cols, rows)
+            log.debug("Chunked and warped: %d", retval)
+    
+    except Exception:
+        log.exception(
+            "Caught exception in warping. Source not reprojected.")
+    
+    else:
+        reprojected = True
+
+    finally:
+        if hTransformArg != NULL:
+            _gdal.GDALDestroyGenImgProjTransformer(hTransformArg)
+        #if maxerror > 0.0:
+        #    _gdal.GDALDestroyApproxTransformer(psWOptions.pTransformerArg)
+        if psWOptions != NULL:
+            _gdal.GDALDestroyWarpOptions(psWOptions)
+        if isinstance(source, np.ndarray):
+            if hdsin != NULL:
+                _gdal.GDALClose(hdsin)
+
+    if reprojected and isinstance(destination, np.ndarray):
+        try:
+            dtype = np.dtype(destination.dtype).name
+            _, rows, cols = destination.shape
+            indexes = np.array(range(1, src_count+1))
+            if dtype == dtypes.ubyte:
+                retval = _io.io_multi_ubyte(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            elif dtype == dtypes.uint16:
+                retval = _io.io_multi_uint16(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            elif dtype == dtypes.int16:
+                retval = _io.io_multi_int16(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            elif dtype == dtypes.uint32:
+                retval = _io.io_multi_uint32(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            elif dtype == dtypes.int32:
+                retval = _io.io_multi_int32(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            elif dtype == dtypes.float32:
+                retval = _io.io_multi_float32(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            elif dtype == dtypes.float64:
+                retval = _io.io_multi_float64(
+                    hdsout, 0, 0, 0, cols, rows,
+                    destination, indexes, src_count)
+            else:
+                raise ValueError("Invalid dtype")
+            # TODO: handle errors (by retval).
+        except Exception:
+            raise
+        finally:
+            if hdsout != NULL:
+                _gdal.GDALClose(hdsout)
+
diff --git a/rasterio/coords.py b/rasterio/coords.py
new file mode 100644
index 0000000..1d89156
--- /dev/null
+++ b/rasterio/coords.py
@@ -0,0 +1,5 @@
+
+from collections import namedtuple
+
+BoundingBox = namedtuple('BoundingBox', ('left', 'bottom', 'right', 'top'))
+
diff --git a/rasterio/crs.py b/rasterio/crs.py
new file mode 100644
index 0000000..eef744c
--- /dev/null
+++ b/rasterio/crs.py
@@ -0,0 +1,186 @@
+# Coordinate reference systems and functions.
+#
+# PROJ.4 is the law of this land: http://proj.osgeo.org/. But whereas PROJ.4
+# coordinate reference systems are described by strings of parameters such as
+#
+#   +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
+#
+# here we use mappings:
+#
+#   {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}
+#
+
+from rasterio.five import string_types
+
+def to_string(crs):
+    """Turn a parameter mapping into a more conventional PROJ.4 string.
+
+    Mapping keys are tested against the ``all_proj_keys`` list. Values of
+    ``True`` are omitted, leaving the key bare: {'no_defs': True} -> "+no_defs"
+    and items where the value is otherwise not a str, int, or float are
+    omitted.
+    """
+    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], string_types)),
+            crs.items() )):
+        items.append(
+            "+" + "=".join(
+                map(str, filter(
+                    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.
+    """
+    parts = [o.lstrip('+') for o in prjs.strip().split()]
+    def parse(v):
+        if v in ('True', 'true'):
+            return True
+        elif v in ('False', 'false'):
+            return False
+        else:
+            try:
+                return int(v)
+            except ValueError:
+                pass
+            try:
+                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)
+
+def from_epsg(code):
+    """Given an integer code, returns an EPSG-like mapping.
+
+    Note: the input code is not validated against an EPSG database.
+    """
+    if int(code) <= 0:
+        raise ValueError("EPSG codes are positive integers")
+    return {'init': "epsg:%s" % code, 'no_defs': True}
+
+
+# Below is the big list of PROJ4 parameters from
+# http://trac.osgeo.org/proj/wiki/GenParms.
+# It is parsed into a list of paramter keys ``all_proj_keys``.
+
+_param_data = """
++a         Semimajor radius of the ellipsoid axis
++alpha     ? Used with Oblique Mercator and possibly a few others
++axis      Axis orientation (new in 4.8.0)
++b         Semiminor radius of the ellipsoid axis
++datum     Datum name (see `proj -ld`)
++ellps     Ellipsoid name (see `proj -le`)
++init      Initialize from a named CRS
++k         Scaling factor (old name)
++k_0       Scaling factor (new name)
++lat_0     Latitude of origin
++lat_1     Latitude of first standard parallel
++lat_2     Latitude of second standard parallel
++lat_ts    Latitude of true scale
++lon_0     Central meridian
++lonc      ? Longitude used with Oblique Mercator and possibly a few others
++lon_wrap  Center longitude to use for wrapping (see below)
++nadgrids  Filename of NTv2 grid file to use for datum transforms (see below)
++no_defs   Don't use the /usr/share/proj/proj_def.dat defaults file
++over      Allow longitude output outside -180 to 180 range, disables wrapping (see below)
++pm        Alternate prime meridian (typically a city name, see below)
++proj      Projection name (see `proj -l`)
++south     Denotes southern hemisphere UTM zone
++to_meter  Multiplier to convert map units to 1.0m
++towgs84   3 or 7 term datum transform parameters (see below)
++units     meters, US survey feet, etc.
++vto_meter vertical conversion to meters.
++vunits    vertical units.
++x_0       False easting
++y_0       False northing
++zone      UTM zone
++a         Semimajor radius of the ellipsoid axis
++alpha     ? Used with Oblique Mercator and possibly a few others
++azi
++b         Semiminor radius of the ellipsoid axis
++belgium
++beta
++czech
++e         Eccentricity of the ellipsoid = sqrt(1 - b^2/a^2) = sqrt( f*(2-f) )
++ellps     Ellipsoid name (see `proj -le`)
++es        Eccentricity of the ellipsoid squared
++f         Flattening of the ellipsoid (often presented as an inverse, e.g. 1/298)
++gamma
++geoc
++guam
++h
++k         Scaling factor (old name)
++K
++k_0       Scaling factor (new name)
++lat_0     Latitude of origin
++lat_1     Latitude of first standard parallel
++lat_2     Latitude of second standard parallel
++lat_b
++lat_t
++lat_ts    Latitude of true scale
++lon_0     Central meridian
++lon_1
++lon_2
++lonc      ? Longitude used with Oblique Mercator and possibly a few others
++lsat
++m
++M
++n
++no_cut
++no_off
++no_rot
++ns
++o_alpha
++o_lat_1
++o_lat_2
++o_lat_c
++o_lat_p
++o_lon_1
++o_lon_2
++o_lon_c
++o_lon_p
++o_proj
++over
++p
++path
++proj      Projection name (see `proj -l`)
++q
++R
++R_a
++R_A       Compute radius such that the area of the sphere is the same as the area of the ellipsoid
++rf        Reciprocal of the ellipsoid flattening term (e.g. 298)
++R_g
++R_h
++R_lat_a
++R_lat_g
++rot
++R_V
++s
++south     Denotes southern hemisphere UTM zone
++sym
++t
++theta
++tilt
++to_meter  Multiplier to convert map units to 1.0m
++units     meters, US survey feet, etc.
++vopt
++W
++westo
++x_0       False easting
++y_0       False northing
++zone      UTM zone
+"""
+
+_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) 
+    ) + ['no_mayo']
diff --git a/rasterio/dtypes.py b/rasterio/dtypes.py
new file mode 100644
index 0000000..e08f14e
--- /dev/null
+++ b/rasterio/dtypes.py
@@ -0,0 +1,98 @@
+# Mapping of GDAL to Numpy data types.
+#
+# Since 0.13 we are not importing numpy here and data types are strings.
+# Happily strings can be used throughout Numpy and so existing code will
+# break.
+#
+# Within Rasterio, to test data types, we use Numpy's dtype() factory to 
+# do something like this:
+#
+#   if np.dtype(destination.dtype) == np.dtype(rasterio.uint8): ...
+#
+
+bool_ = 'bool'
+ubyte = uint8 = 'uint8'
+uint16 = 'uint16'
+int16 = 'int16'
+uint32 = 'uint32'
+int32 = 'int32'
+float32 = 'float32'
+float64 = 'float64'
+complex_ = 'complex'
+complex64 = 'complex64'
+complex128 = 'complex128'
+
+# Not supported:
+#  GDT_CInt16 = 8, GDT_CInt32 = 9, GDT_CFloat32 = 10, GDT_CFloat64 = 11
+
+dtype_fwd = {
+    0: None,            # GDT_Unknown
+    1: ubyte,           # GDT_Byte
+    2: uint16,          # GDT_UInt16
+    3: int16,           # GDT_Int16
+    4: uint32,          # GDT_UInt32
+    5: int32,           # GDT_Int32
+    6: float32,         # GDT_Float32
+    7: float64,         # GDT_Float64
+    8: complex_,        # GDT_CInt16
+    9: complex_,        # GDT_CInt32
+    10: complex64,      # GDT_CFloat32
+    11: complex128 }    # GDT_CFloat64
+
+dtype_rev = dict((v, k) for k, v in dtype_fwd.items())
+dtype_rev['uint8'] = 1
+
+typename_fwd = {
+    0: 'Unknown',
+    1: 'Byte',
+    2: 'UInt16',
+    3: 'Int16',
+    4: 'UInt32',
+    5: 'Int32',
+    6: 'Float32',
+    7: 'Float64',
+    8: 'CInt16',
+    9: 'CInt32',
+    10: 'CFloat32',
+    11: 'CFloat64' }
+
+typename_rev = dict((v, k) for k, v in typename_fwd.items())
+
+def _gdal_typename(dt):
+    try:
+        return typename_fwd[dtype_rev[dt]]
+    except KeyError:
+        return typename_fwd[dtype_rev[dt().dtype.name]]
+
+def check_dtype(dt):
+    if dt not in dtype_rev:
+        try:
+            return dt().dtype.name in dtype_rev
+        except:
+            return False
+    return True
+
+
+def get_minimum_int_dtype(values):
+    """
+    Uses range checking to determine the minimum integer data type required
+    to represent values.
+
+    :param values: numpy array
+    :return: named data type that can be later used to create a numpy dtype
+    """
+
+    min_value = values.min()
+    max_value = values.max()
+    
+    if min_value >= 0:
+        if max_value <= 255:
+            return uint8
+        elif max_value <= 65535:
+            return uint16
+        elif max_value <= 4294967295:
+            return uint32
+    elif min_value >= -32768 and max_value <= 32767:
+        return int16
+    elif min_value >= -2147483648 and max_value <= 2147483647:
+        return int32
diff --git a/rasterio/enums.py b/rasterio/enums.py
new file mode 100644
index 0000000..3926680
--- /dev/null
+++ b/rasterio/enums.py
@@ -0,0 +1,19 @@
+
+from enum import IntEnum
+
+class ColorInterp(IntEnum):
+    undefined=0
+    grey=1
+    gray=1
+    palette=2
+    red=3
+    green=4
+    blue=5
+    alpha=6
+    hue=7
+    saturation=8
+    lightness=9
+    cyan=10
+    magenta=11
+    yellow=12
+    black=13
\ No newline at end of file
diff --git a/rasterio/features.py b/rasterio/features.py
new file mode 100644
index 0000000..9ea14db
--- /dev/null
+++ b/rasterio/features.py
@@ -0,0 +1,318 @@
+"""Functions for working with features in a raster dataset."""
+
+import json
+import logging
+import time
+import warnings
+
+import numpy as np
+
+import rasterio
+from rasterio._features import _shapes, _sieve, _rasterize
+from rasterio.transform import IDENTITY, guard_transform
+from rasterio.dtypes import get_minimum_int_dtype
+
+
+log = logging.getLogger('rasterio')
+
+
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+log.addHandler(NullHandler())
+
+
+def shapes(image, mask=None, connectivity=4, transform=IDENTITY):
+    """
+    Return a generator of (polygon, value) for each each set of adjacent pixels
+    of the same value.
+
+    Parameters
+    ----------
+    image : numpy ndarray or rasterio Band object
+        (RasterReader, bidx namedtuple).
+        Data type must be one of rasterio.int16, rasterio.int32,
+        rasterio.uint8, rasterio.uint16, or rasterio.float32.
+    mask : numpy ndarray or rasterio Band object, optional
+        Values of False will be excluded from feature generation
+        Must be of type rasterio.bool_
+    connectivity : int, optional
+        Use 4 or 8 pixel connectivity for grouping pixels into features
+    transform : Affine transformation, optional
+        If not provided, feature coordinates will be generated based on pixel
+        coordinates
+
+    Returns
+    -------
+    Generator of (polygon, value)
+        Yields a pair of (polygon, value) for each feature found in the image.
+        Polygons are GeoJSON-like dicts and the values are the associated value
+        from the image, in the data type of the image.
+        Note: due to floating point precision issues, values returned from a
+        floating point image may not exactly match the original values.
+
+    Notes
+    -----
+    The amount of memory used by this algorithm is proportional to the number
+    and complexity of polygons produced.  This algorithm is most appropriate
+    for simple thematic data.  Data with high pixel-to-pixel variability, such
+    as imagery, may produce one polygon per pixel and consume large amounts of
+    memory.
+
+    """
+
+    valid_dtypes = ('int16', 'int32', 'uint8', 'uint16', 'float32')
+
+    if np.dtype(image.dtype).name not in valid_dtypes:
+        raise ValueError('image dtype must be one of: %s'
+                         % (', '.join(valid_dtypes)))
+
+    if mask is not None and np.dtype(mask.dtype) != np.dtype(rasterio.bool_):
+        raise ValueError("Mask must be dtype rasterio.bool_")
+
+    if connectivity not in (4, 8):
+        raise ValueError("Connectivity Option must be 4 or 8")
+
+    transform = guard_transform(transform)
+
+    with rasterio.drivers():
+        for s, v in _shapes(image, mask, connectivity, transform.to_gdal()):
+            yield s, v
+
+
+def sieve(image, size, out=None, output=None, mask=None, connectivity=4):
+    """
+    Replaces small polygons in `image` with the value of their largest
+    neighbor.  Polygons are found for each set of neighboring pixels of the
+    same value.
+
+    Parameters
+    ----------
+    image : numpy ndarray or rasterio Band object
+        (RasterReader, bidx namedtuple)
+        Must be of type rasterio.int16, rasterio.int32, rasterio.uint8,
+        rasterio.uint16, or rasterio.float32
+    size : int
+        minimum polygon size (number of pixels) to retain.
+    out : numpy ndarray, optional
+        Array of same shape and data type as `image` in which to store results.
+    output : older alias for `out`, will be removed before 1.0.
+    output : numpy ndarray, optional
+    mask : numpy ndarray or rasterio Band object, optional
+        Values of False will be excluded from feature generation
+        Must be of type rasterio.bool_
+    connectivity : int, optional
+        Use 4 or 8 pixel connectivity for grouping pixels into features
+
+    Returns
+    -------
+    out : numpy ndarray
+        Result
+
+    Notes
+    -----
+    GDAL only supports values that can be cast to 32-bit integers for this
+    operation.
+
+    The amount of memory used by this algorithm is proportional to the number
+    and complexity of polygons found in the image.  This algorithm is most
+    appropriate for simple thematic data.  Data with high pixel-to-pixel
+    variability, such as imagery, may produce one polygon per pixel and consume
+    large amounts of memory.
+
+    """
+
+    valid_dtypes = ('int16', 'int32', 'uint8', 'uint16')
+
+    if np.dtype(image.dtype).name not in valid_dtypes:
+        valid_types_str = ', '.join(('rasterio.{0}'.format(t) for t
+                                     in valid_dtypes))
+        raise ValueError('image dtype must be one of: %' % valid_types_str)
+
+    if size <= 0:
+        raise ValueError('size must be greater than 0')
+    elif type(size) == float:
+        raise ValueError('size must be an integer number of pixels')
+    elif size > (image.shape[0] * image.shape[1]):
+        raise ValueError('size must be smaller than size of image')
+
+    if connectivity not in (4, 8):
+        raise ValueError('connectivity must be 4 or 8')
+
+    if mask is not None:
+        if np.dtype(mask.dtype) != np.dtype(rasterio.bool_):
+            raise ValueError('Mask must be dtype rasterio.bool_')
+        elif mask.shape != image.shape:
+            raise ValueError('mask shape must be same as image shape')
+
+    # Start moving users over to 'out'.
+    if output is not None:
+        warnings.warn(
+            "The 'output' keyword arg has been superceded by 'out' "
+            "and will be removed before Rasterio 1.0.",
+            FutureWarning,
+            stacklevel=2)
+    
+    out = out if out is not None else output
+    if out is None:
+        out = np.zeros_like(image)
+    else:
+        if np.dtype(image.dtype).name != np.dtype(out.dtype).name:
+            raise ValueError('output must match dtype of image')
+        elif out.shape != image.shape:
+            raise ValueError('mask shape must be same as image shape')
+
+    with rasterio.drivers():
+        _sieve(image, size, out, mask, connectivity)
+        return out
+
+
+def rasterize(
+        shapes,
+        out_shape=None,
+        fill=0,
+        out=None,
+        output=None,
+        transform=IDENTITY,
+        all_touched=False,
+        default_value=1,
+        dtype=None):
+    """
+    Returns an image array with input geometries burned in.
+
+    Parameters
+    ----------
+    shapes : iterable of (geometry, value) pairs or iterable over geometries
+        `geometry` can either be an object that implements the geo interface or
+        GeoJSON-like object.
+    out_shape : tuple or list
+        Shape of output numpy ndarray
+    fill : int or float, optional
+        Used as fill value for all areas not covered by input geometries
+    out : numpy ndarray, optional
+        Array of same shape and data type as `image` in which to store results.
+    output : older alias for `out`, will be removed before 1.0.
+    transform : Affine transformation object, optional
+        transformation applied to shape geometries into pixel coordinates
+    all_touched : boolean, optional
+        If True, all pixels touched by geometries will be burned in.
+        If false, only pixels whose center is within the polygon or that are
+        selected by brezenhams line algorithm will be burned in.
+    default_value : int or float, optional
+        Used as value for all geometries, if not provided in `shapes`
+    dtype : rasterio or numpy data type, optional
+        Used as data type for results, if `output` is not provided
+
+    Returns
+    -------
+    out : numpy ndarray
+        Results
+
+    Notes
+    -----
+    Valid data types for `fill`, `default_value`, `out`, `dtype` and
+    shape values are rasterio.int16, rasterio.int32, rasterio.uint8,
+    rasterio.uint16, rasterio.uint32, rasterio.float32, rasterio.float64
+
+    """
+
+    valid_dtypes = ('int16', 'int32', 'uint8', 'uint16', 'uint32', 'float32',
+                    'float64')
+
+    def get_valid_dtype(values):
+        values_dtype = values.dtype
+        if values_dtype.kind == 'i':
+            values_dtype = np.dtype(get_minimum_int_dtype(values))
+        if values_dtype.name in valid_dtypes:
+            return values_dtype
+        return None
+
+    def can_cast_dtype(values, dtype):
+        if values.dtype.name == np.dtype(dtype).name:
+            return True
+        elif values.dtype.kind == 'f':
+            return np.allclose(values, values.astype(dtype))
+        else:
+            return np.array_equal(values, values.astype(dtype))
+
+    if fill != 0:
+        fill_array = np.array([fill])
+        if get_valid_dtype(fill_array) is None:
+            raise ValueError('fill must be one of these types: %s'
+                             % (', '.join(valid_dtypes)))
+        elif dtype is not None and not can_cast_dtype(fill_array, dtype):
+            raise ValueError('fill value cannot be cast to specified dtype')
+
+    if default_value != 1:
+        default_value_array = np.array([default_value])
+        if get_valid_dtype(default_value_array) is None:
+            raise ValueError('default_value must be one of these types: %s'
+                             % (', '.join(valid_dtypes)))
+        elif dtype is not None and not can_cast_dtype(default_value_array,
+                                                      dtype):
+            raise ValueError('default_value cannot be cast to specified dtype')
+
+    valid_shapes = []
+    shape_values = []
+    for index, item in enumerate(shapes):
+        try:
+            if isinstance(item, (tuple, list)):
+                geom, value = item
+            else:
+                geom = item
+                value = default_value
+            geom = getattr(geom, '__geo_interface__', None) or geom
+            if (not isinstance(geom, dict) or
+                'type' not in geom or 'coordinates' not in geom):
+                raise ValueError(
+                    'Object %r at index %d is not a geometry object' %
+                    (geom, index))
+            valid_shapes.append((geom, value))
+            shape_values.append(value)
+        except Exception:
+            log.exception('Exception caught, skipping shape %d', index)
+
+    if not valid_shapes:
+        raise ValueError('No valid shapes found for rasterize.  Shapes must be '
+                         'valid geometry objects')
+
+    shape_values = np.array(shape_values)
+    values_dtype = get_valid_dtype(shape_values)
+    if values_dtype is None:
+        raise ValueError('shape values must be one of these dtypes: %s' %
+                         (', '.join(valid_dtypes)))
+
+    if dtype is None:
+        dtype = values_dtype
+    elif np.dtype(dtype).name not in valid_dtypes:
+        raise ValueError('dtype must be one of: %s' % (', '.join(valid_dtypes)))
+    elif not can_cast_dtype(shape_values, dtype):
+        raise ValueError('shape values could not be cast to specified dtype')
+
+    if output is not None:
+        warnings.warn(
+            "The 'output' keyword arg has been superceded by 'out' "
+            "and will be removed before Rasterio 1.0.",
+            FutureWarning,
+            stacklevel=2)
+    out = out if out is not None else output
+    if out is not None:
+        if np.dtype(output.dtype).name not in valid_dtypes:
+            raise ValueError('Output image dtype must be one of: %s'
+                             % (', '.join(valid_dtypes)))
+        if not can_cast_dtype(shape_values, output.dtype):
+            raise ValueError('shape values cannot be cast to dtype of output '
+                             'image')
+
+    elif out_shape is not None:
+        out = np.empty(out_shape, dtype=dtype)
+        out.fill(fill)
+    else:
+        raise ValueError('Either an output shape or image must be provided')
+
+    transform = guard_transform(transform)
+
+    with rasterio.drivers():
+        _rasterize(valid_shapes, out, transform.to_gdal(), all_touched)
+
+    return out
diff --git a/rasterio/five.py b/rasterio/five.py
new file mode 100644
index 0000000..ec38a12
--- /dev/null
+++ b/rasterio/five.py
@@ -0,0 +1,15 @@
+# Python 2-3 compatibility
+
+import itertools
+import sys
+
+if sys.version_info[0] >= 3:
+    string_types = str,
+    text_type = str
+    integer_types = int,
+    zip_longest = itertools.zip_longest
+else:
+    string_types = basestring,
+    text_type = unicode
+    integer_types = int, long
+    zip_longest = itertools.izip_longest
diff --git a/rasterio/rio/__init__.py b/rasterio/rio/__init__.py
new file mode 100644
index 0000000..e736ca1
--- /dev/null
+++ b/rasterio/rio/__init__.py
@@ -0,0 +1 @@
+# module of CLI commands.
diff --git a/rasterio/rio/bands.py b/rasterio/rio/bands.py
new file mode 100644
index 0000000..cab2c7b
--- /dev/null
+++ b/rasterio/rio/bands.py
@@ -0,0 +1,129 @@
+import logging
+import os.path
+import sys
+
+import click
+
+import rasterio
+
+from rasterio.five import zip_longest
+from rasterio.rio.cli import cli
+
+
+PHOTOMETRIC_CHOICES = [val.lower() for val in [
+    'MINISBLACK',
+    'MINISWHITE',
+    'RGB',
+    'CMYK',
+    'YCBCR',
+    'CIELAB',
+    'ICCLAB',
+    'ITULAB']]
+
+
+# Stack command.
+ at cli.command(short_help="Stack a number of bands into a multiband dataset.")
+ at click.argument('input', nargs=-1,
+                type=click.Path(exists=True, resolve_path=True), required=True)
+ at click.option('--bidx', multiple=True,
+              help="Indexes of input file bands.")
+ at click.option('--photometric', default=None,
+              type=click.Choice(PHOTOMETRIC_CHOICES),
+              help="Photometric interpretation")
+ at click.option('-o','--output',
+              type=click.Path(exists=False, resolve_path=True), required=True,
+              help="Path to output file.")
+ at click.option('-f', '--format', '--driver', default='GTiff',
+              help="Output format driver")
+ at click.pass_context
+def stack(ctx, input, bidx, photometric, output, driver):
+    """Stack a number of bands from one or more input files into a
+    multiband dataset.
+
+    Input datasets must be of a kind: same data type, dimensions, etc. The
+    output is cloned from the first input.
+
+    By default, rio-stack will take all bands from each input and write them
+    in same order to the output. Optionally, bands for each input may be
+    specified using a simple syntax:
+
+      --bidx N takes the Nth band from the input (first band is 1).
+
+      --bidx M,N,0 takes bands M, N, and O.
+
+      --bidx M..O takes bands M-O, inclusive.
+
+      --bidx ..N takes all bands up to and including N.
+
+      --bidx N.. takes all bands from N to the end.
+
+    Examples, using the Rasterio testing dataset, which produce a copy.
+
+      rio stack RGB.byte.tif -o stacked.tif
+
+      rio stack RGB.byte.tif --bidx 1,2,3 -o stacked.tif
+
+      rio stack RGB.byte.tif --bidx 1..3 -o stacked.tif
+
+      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')
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            output_count = 0
+            indexes = []
+            for path, item in zip_longest(input, bidx, fillvalue=None):
+                with rasterio.open(path) as src:
+                    src_indexes = src.indexes
+                if item is None:
+                    indexes.append(src_indexes)
+                    output_count += len(src_indexes)
+                elif '..' in item:
+                    start, stop = map(
+                        lambda x: int(x) if x else None, item.split('..'))
+                    if start is None:
+                        start = 1
+                    indexes.append(src_indexes[slice(start-1, stop)])
+                    output_count += len(src_indexes[slice(start-1, stop)])
+                else:
+                    parts = list(map(int, item.split(',')))
+                    if len(parts) == 1:
+                        indexes.append(parts[0])
+                        output_count += 1
+                    else:
+                        parts = list(parts)
+                        indexes.append(parts)
+                        output_count += len(parts)
+
+            with rasterio.open(input[0]) as first:
+                kwargs = first.meta
+                kwargs['transform'] = kwargs.pop('affine')
+
+            kwargs.update(
+                driver=driver,
+                count=output_count)
+
+            if photometric:
+                kwargs['photometric'] = photometric
+
+            with rasterio.open(output, 'w', **kwargs) as dst:
+                dst_idx = 1
+                for path, index in zip(input, indexes):
+                    with rasterio.open(path) as src:
+                        if isinstance(index, int):
+                            data = src.read(index)
+                            dst.write(data, dst_idx)
+                            dst_idx += 1
+                        elif isinstance(index, list):
+                            data = src.read(index)
+                            dst.write(data, range(dst_idx, dst_idx+len(index)))
+                            dst_idx += len(index)
+
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
diff --git a/rasterio/rio/cli.py b/rasterio/rio/cli.py
new file mode 100644
index 0000000..cf54fda
--- /dev/null
+++ b/rasterio/rio/cli.py
@@ -0,0 +1,83 @@
+import json
+import logging
+import sys
+
+import click
+
+import rasterio
+from rasterio.rio import options
+
+def configure_logging(verbosity):
+    log_level = max(10, 30 - 10*verbosity)
+    logging.basicConfig(stream=sys.stderr, level=log_level)
+
+
+# The CLI command group.
+ at click.group(help="Rasterio command line interface.")
+ at options.verbose
+ at options.quiet
+ at options.version
+ at click.pass_context
+def cli(ctx, verbose, quiet):
+    verbosity = verbose - quiet
+    configure_logging(verbosity)
+    ctx.obj = {}
+    ctx.obj['verbosity'] = verbosity
+
+
+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(file, collection,
+        agg_mode='obj', expression='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 agg_mode == 'seq':
+        for feat in collection():
+            xs, ys = zip(*coords(feat))
+            bbox = (min(xs), min(ys), max(xs), max(ys))
+            if use_rs:
+                file.write(u'\u001e')
+            if expression == 'feature':
+                file.write(json.dumps(feat, **dump_kwds))
+            elif expression == 'bbox':
+                file.write(json.dumps(bbox, **dump_kwds))
+            else:
+                file.write(
+                    json.dumps({
+                        'type': 'FeatureCollection',
+                        'bbox': bbox,
+                        'features': [feat]}, **dump_kwds))
+            file.write('\n')
+    # Aggregate all features into a single object expressed as 
+    # bbox or collection.
+    else:
+        features = list(collection())
+        if expression == 'bbox':
+            file.write(json.dumps(collection.bbox, **dump_kwds))
+        elif expression == 'feature':
+            file.write(json.dumps(features[0], **dump_kwds))
+        else:
+            file.write(json.dumps({
+                'bbox': collection.bbox,
+                'type': 'FeatureCollection', 
+                'features': features},
+                **dump_kwds))
+        file.write('\n')
diff --git a/rasterio/rio/features.py b/rasterio/rio/features.py
new file mode 100644
index 0000000..ede426f
--- /dev/null
+++ b/rasterio/rio/features.py
@@ -0,0 +1,156 @@
+import functools
+import json
+import logging
+import sys
+import warnings
+
+import click
+
+import rasterio
+from rasterio.transform import Affine
+from rasterio.rio.cli import cli, coords, write_features
+
+
+warnings.simplefilter('default')
+
+
+# Shapes command.
+ at cli.command(short_help="Write the shapes of features.")
+ at click.argument('input', type=click.Path(exists=True))
+# Coordinate precision option.
+ at click.option('--precision', type=int, default=-1,
+              help="Decimal precision of coordinates.")
+# JSON formatting options.
+ at click.option('--indent', default=None, type=int,
+              help="Indentation level for JSON output")
+ at click.option('--compact/--no-compact', default=False,
+              help="Use compact separators (',', ':').")
+# Geographic (default) or Mercator switch.
+ at click.option('--geographic', 'projected', flag_value='geographic',
+              default=True,
+              help="Output in geographic coordinates (the default).")
+ at click.option('--projected', 'projected', flag_value='projected',
+              help="Output in projected coordinates.")
+# JSON object (default) or sequence (experimental) switch.
+ at click.option('--json-obj', 'json_mode', flag_value='obj', default=True,
+        help="Write a single JSON object (the default).")
+ at click.option('--x-json-seq', 'json_mode', flag_value='seq',
+        help="Write a JSON sequence. Experimental.")
+# Use ASCII RS control code to signal a sequence item (False is default).
+# See http://tools.ietf.org/html/draft-ietf-json-text-sequence-05.
+# Experimental.
+ at click.option('--x-json-seq-rs/--x-json-seq-no-rs', default=False,
+        help="Use RS as text separator. Experimental.")
+# GeoJSON feature (default), bbox, or collection switch. Meaningful only
+# when --x-json-seq is used.
+ at click.option('--collection', 'output_mode', flag_value='collection',
+              default=True,
+              help="Output as a GeoJSON feature collection (the default).")
+ at click.option('--feature', 'output_mode', flag_value='feature',
+              help="Output as sequence of GeoJSON features.")
+ at click.option('--bbox', 'output_mode', flag_value='bbox',
+              help="Output as a GeoJSON bounding box array.")
+ at click.option('--bands/--mask', default=True,
+              help="Extract shapes from one of the dataset bands or from "
+                   "its nodata mask")
+ at click.option('--bidx', type=int, default=1,
+              help="Index of the source band")
+ at click.option('--sampling', type=int, default=1,
+              help="Inverse of the sampling fraction")
+ at click.option('--with-nodata/--without-nodata', default=False,
+              help="Include or do not include (the default) nodata regions.")
+ at click.pass_context
+def shapes(
+        ctx, input, precision, indent, compact, projected, json_mode,
+        x_json_seq_rs, output_mode, bands, bidx, sampling, with_nodata):
+    """Writes features of a dataset out as GeoJSON. It's intended for
+    use with single-band rasters and reads from the first band.
+    """
+    # These import numpy, which we don't want to do unless its needed.
+    import numpy
+    import rasterio.features
+    import rasterio.warp
+
+    verbosity = ctx.obj['verbosity']
+    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):
+            with rasterio.open(input) as src:
+                if bands:
+                    if sampling == 1:
+                        img = src.read_band(bidx)
+                        transform = src.transform
+                    # Decimate the band.
+                    else:
+                        img = numpy.zeros(
+                            (src.height//sampling, src.width//sampling),
+                            dtype=src.dtypes[src.indexes.index(bidx)])
+                        img = src.read_band(bidx, img)
+                        transform = src.affine * Affine.scale(float(sampling))
+                else:
+                    if sampling == 1:
+                        img = src.read_mask()
+                        transform = src.transform
+                    # Decimate the mask.
+                    else:
+                        img = numpy.zeros(
+                            (src.height//sampling, src.width//sampling),
+                            dtype=numpy.uint8)
+                        img = src.read_mask(img)
+                        transform = src.affine * Affine.scale(float(sampling))
+
+                bounds = src.bounds
+                xs = [bounds[0], bounds[2]]
+                ys = [bounds[1], bounds[3]]
+                if projected == '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]
+                self._xs = xs
+                self._ys = ys
+
+                kwargs = {'transform': transform}
+                if not bands and not with_nodata:
+                    kwargs['mask'] = (img==255)
+                for g, i in rasterio.features.shapes(img, **kwargs):
+                    if projected == 'geographic':
+                        g = rasterio.warp.transform_geom(
+                            src.crs, 'EPSG:4326', g,
+                            antimeridian_cutting=True, precision=precision)
+                    xs, ys = zip(*coords(g))
+                    yield {
+                        'type': 'Feature',
+                        'id': str(i),
+                        'properties': {'val': i},
+                        'bbox': [min(xs), min(ys), max(xs), max(ys)],
+                        'geometry': g }
+
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            write_features(
+                stdout, Collection(), agg_mode=json_mode,
+                expression=output_mode, use_rs=x_json_seq_rs,
+                **dump_kwds)
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
new file mode 100644
index 0000000..a3e79e0
--- /dev/null
+++ b/rasterio/rio/info.py
@@ -0,0 +1,100 @@
+# Info command.
+
+import json
+import logging
+import os.path
+import pprint
+import sys
+
+import click
+
+import rasterio
+import rasterio.crs
+from rasterio.rio.cli import cli
+
+
+ at cli.command(short_help="Print information about the rio environment.")
+ at click.option('--formats', 'key', flag_value='formats', default=True,
+              help="Enumerate the available formats.")
+ at click.pass_context
+def env(ctx, key):
+    """Print information about the Rasterio environment: available
+    formats, etc.
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    stdout = click.get_text_stream('stdout')
+    with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
+        if key == 'formats':
+            for k, v in sorted(env.drivers().items()):
+                stdout.write("%s: %s\n" % (k, v))
+            stdout.write('\n')
+
+
+ at cli.command(short_help="Print information about a data file.")
+ at click.argument('input', type=click.Path(exists=True))
+ at click.option('--meta', 'aspect', flag_value='meta', default=True,
+              help="Show data file structure (default).")
+ at click.option('--tags', 'aspect', flag_value='tags',
+              help="Show data file tags.")
+ at click.option('--namespace', help="Select a tag namespace.")
+ at click.option('--indent', default=None, type=int,
+              help="Indentation level for pretty printed output")
+# Options to pick out a single metadata item and print it as
+# a string.
+ at click.option('--count', 'meta_member', flag_value='count',
+              help="Print the count of bands.")
+ at click.option('--dtype', 'meta_member', flag_value='dtype',
+              help="Print the dtype name.")
+ at click.option('--nodata', 'meta_member', flag_value='nodata',
+              help="Print the nodata value.")
+ at click.option('-f', '--format', '--driver', 'meta_member', flag_value='driver',
+              help="Print the format driver.")
+ at click.option('--shape', 'meta_member', flag_value='shape',
+              help="Print the (height, width) shape.")
+ at click.option('--height', 'meta_member', flag_value='height',
+              help="Print the height (number of rows).")
+ at click.option('--width', 'meta_member', flag_value='width',
+              help="Print the width (number of columns).")
+ at click.option('--crs', 'meta_member', flag_value='crs',
+              help="Print the CRS as a PROJ.4 string.")
+ at click.option('--bounds', 'meta_member', flag_value='bounds',
+              help="Print the nodata value.")
+ at click.pass_context
+def info(ctx, input, aspect, indent, namespace, meta_member):
+    """Print metadata about the dataset as JSON.
+
+    Optionally print a single metadata item as a string.
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    stdout = click.get_text_stream('stdout')
+    try:
+        with rasterio.drivers(CPL_DEBUG=(verbosity > 2)):
+            with rasterio.open(input, 'r-') as src:
+                info = src.meta
+                del info['affine']
+                del info['transform']
+                info['shape'] = info['height'], info['width']
+                info['bounds'] = src.bounds
+                proj4 = rasterio.crs.to_string(src.crs)
+                if proj4.startswith('+init=epsg'):
+                    proj4 = proj4.split('=')[1].upper()
+                info['crs'] = proj4
+                if aspect == 'meta':
+                    if meta_member:
+                        if isinstance(info[meta_member], (list, tuple)):
+                            print(" ".join(map(str, info[meta_member])))
+                        else:
+                            print(info[meta_member])
+                    else:
+                        stdout.write(json.dumps(info, indent=indent))
+                        stdout.write("\n")
+                elif aspect == 'tags':
+                    stdout.write(json.dumps(src.tags(ns=namespace), 
+                                            indent=indent))
+                    stdout.write("\n")
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
diff --git a/rasterio/rio/main.py b/rasterio/rio/main.py
new file mode 100644
index 0000000..1bccb93
--- /dev/null
+++ b/rasterio/rio/main.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+
+from rasterio.rio.cli import cli
+from rasterio.rio.bands import stack
+from rasterio.rio.features import shapes
+from rasterio.rio.info import env, info
+from rasterio.rio.merge import merge
+from rasterio.rio.rio import bounds, insp, transform
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
new file mode 100644
index 0000000..a2a92f5
--- /dev/null
+++ b/rasterio/rio/merge.py
@@ -0,0 +1,75 @@
+# Merge command.
+
+import logging
+import os.path
+import sys
+
+import click
+
+import rasterio
+
+from rasterio.rio.cli import cli
+
+
+ at cli.command(short_help="Merge a stack of raster datasets.")
+ at click.argument('input', nargs=-1,
+                type=click.Path(exists=True, resolve_path=True),
+                required=True)
+ at click.option('-o','--output',
+              type=click.Path(exists=False, resolve_path=True),
+              required=True,
+              help="Path to output file.")
+ at click.option('-f', '--format', '--driver', default='GTiff',
+              help="Output format driver")
+ at click.pass_context
+def merge(ctx, input, output, driver):
+    """Copy valid pixels from input files to the output file.
+
+    All files must have the same shape, number of bands, and data type.
+
+    Input files are merged in their listed order using a reverse
+    painter's algorithm.
+    """
+    import numpy as np
+
+    verbosity = ctx.obj['verbosity']
+    logger = logging.getLogger('rio')
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity>2):
+
+            with rasterio.open(input[0]) as first:
+                kwargs = first.meta
+                kwargs['transform'] = kwargs.pop('affine')
+                dest = np.empty((3,) + first.shape, dtype=first.dtypes[0])
+
+            if os.path.exists(output):
+                dst = rasterio.open(output, 'r+')
+                nodataval = dst.nodatavals[0]
+            else:
+                kwargs['driver'] == driver
+                dst = rasterio.open(output, 'w', **kwargs)
+                nodataval = first.nodatavals[0]
+
+            dest.fill(nodataval)
+
+            for fname in reversed(input):
+                with rasterio.open(fname) as src:
+                    data = src.read()
+                    np.copyto(dest, data,
+                        where=np.logical_and(
+                        dest==nodataval, data.mask==False))
+
+            if dst.mode == 'r+':
+                data = dst.read()
+                np.copyto(dest, data,
+                    where=np.logical_and(
+                    dest==nodataval, data.mask==False))
+
+            dst.write(dest)
+            dst.close()
+
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
+
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
new file mode 100644
index 0000000..7f8340f
--- /dev/null
+++ b/rasterio/rio/options.py
@@ -0,0 +1,21 @@
+import click
+
+from rasterio import __version__ as rio_version
+
+
+def print_version(ctx, param, value):
+    if not value or ctx.resilient_parsing:
+        return
+    click.echo(rio_version)
+    ctx.exit()
+
+
+verbose = click.option('--verbose', '-v', count=True,
+                       help="Increase verbosity.")
+
+quiet = click.option('--quiet', '-q', count=True,
+                     help="Decrease verbosity.")
+
+version = click.option('--version', is_flag=True, callback=print_version,
+                       expose_value=False, is_eager=True,
+                       help="Print Rasterio version.")
diff --git a/rasterio/rio/rio.py b/rasterio/rio/rio.py
new file mode 100644
index 0000000..10df923
--- /dev/null
+++ b/rasterio/rio/rio.py
@@ -0,0 +1,220 @@
+"""Rasterio command line interface"""
+
+import functools
+import json
+import logging
+import os.path
+import pprint
+import sys
+import warnings
+
+import click
+
+import rasterio
+
+from rasterio.rio.cli import cli, write_features
+
+
+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 click.argument('input', type=click.Path(exists=True))
+ at click.option('--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)
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
+
+
+# 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))
+# Coordinate precision option.
+ at click.option('--precision', type=int, default=-1,
+              help="Decimal precision of coordinates.")
+# JSON formatting options.
+ at click.option('--indent', default=None, type=int,
+              help="Indentation level for JSON output")
+ at click.option('--compact/--no-compact', default=False,
+              help="Use compact separators (',', ':').")
+# Geographic (default) or Mercator switch.
+ at click.option('--geographic', 'projected', flag_value='geographic',
+              default=True,
+              help="Output in geographic coordinates (the default).")
+ at click.option('--projected', 'projected', flag_value='projected',
+              help="Output in projected coordinates.")
+ at click.option('--mercator', 'projected', flag_value='mercator',
+              help="Output in Web Mercator coordinates.")
+# JSON object (default) or sequence (experimental) switch.
+ at click.option('--json-obj', 'json_mode', flag_value='obj', default=True,
+        help="Write a single JSON object (the default).")
+ at click.option('--x-json-seq', 'json_mode', flag_value='seq',
+        help="Write a JSON sequence. Experimental.")
+# Use ASCII RS control code to signal a sequence item (False is default).
+# See http://tools.ietf.org/html/draft-ietf-json-text-sequence-05.
+# Experimental.
+ at click.option('--x-json-seq-rs/--x-json-seq-no-rs', default=False,
+        help="Use RS as text separator. Experimental.")
+# GeoJSON feature (default), bbox, or collection switch. Meaningful only
+# when --x-json-seq is used.
+ at click.option('--collection', 'output_mode', flag_value='collection',
+              default=True,
+              help="Output as a GeoJSON feature collection (the default).")
+ at click.option('--feature', 'output_mode', flag_value='feature',
+              help="Output as sequence of GeoJSON features.")
+ at click.option('--bbox', 'output_mode', flag_value='bbox',
+              help="Output as a GeoJSON bounding box array.")
+ at click.pass_context
+def bounds(ctx, input, precision, indent, compact, projected, json_mode,
+        x_json_seq_rs, output_mode):
+    """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 projected == 'geographic':
+                        xs, ys = rasterio.warp.transform(
+                            src.crs, {'init': 'epsg:4326'}, xs, ys)
+                    if projected == '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} }
+
+                self._xs.extend(bbox[::2])
+                self._ys.extend(bbox[1::2])
+
+    collection = 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, collection, agg_mode=json_mode,
+                expression=output_mode, use_rs=x_json_seq_rs,
+                **dump_kwds)
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
+
+
+# Transform command.
+ at cli.command(short_help="Transform coordinates.")
+ at click.argument('input', default='-', required=False)
+ at click.option('--src_crs', default='EPSG:4326', help="Source CRS.")
+ at click.option('--dst_crs', default='EPSG:4326', help="Destination CRS.")
+ at click.option('--precision', type=int, default=-1,
+              help="Decimal precision of coordinates.")
+ 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))
+
+        sys.exit(0)
+    except Exception:
+        logger.exception("Failed. Exception caught")
+        sys.exit(1)
diff --git a/rasterio/tool.py b/rasterio/tool.py
new file mode 100644
index 0000000..d0f368a
--- /dev/null
+++ b/rasterio/tool.py
@@ -0,0 +1,53 @@
+
+import code
+import collections
+import logging
+import sys
+
+try:
+    import matplotlib.pyplot as plt
+except ImportError:
+    plt = None
+
+import numpy
+
+import rasterio
+
+
+logger = logging.getLogger('rasterio')
+
+Stats = collections.namedtuple('Stats', ['min', 'max', 'mean'])
+
+def main(banner, dataset):
+
+    def show(source, cmap='gray'):
+        """Show a raster using matplotlib.
+
+        The raster may be either an ndarray or a (dataset, bidx)
+        tuple.
+        """
+        if isinstance(source, tuple):
+            arr = source[0].read_band(source[1])
+        else:
+            arr = source
+        if plt is not None:
+            plt.imshow(arr, cmap=cmap)
+            plt.show()
+        else:
+            raise ImportError("matplotlib could not be imported")
+
+    def stats(source):
+        """Return a tuple with raster min, max, and mean.
+        """
+        if isinstance(source, tuple):
+            arr = source[0].read_band(source[1])
+        else:
+            arr = source
+        return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
+
+    code.interact(
+        banner, 
+        local=dict(
+            locals(), src=dataset, np=numpy, rio=rasterio, plt=plt))
+
+    return 0
diff --git a/rasterio/transform.py b/rasterio/transform.py
new file mode 100644
index 0000000..4b3ed93
--- /dev/null
+++ b/rasterio/transform.py
@@ -0,0 +1,29 @@
+
+import warnings
+
+from affine import Affine
+
+IDENTITY = Affine.identity()
+
+def tastes_like_gdal(t):
+    return t[2] == t[4] == 0.0 and t[1] > 0 and t[5] < 0
+
+def guard_transform(transform):
+    """Return an Affine transformation instance"""
+    if not isinstance(transform, Affine):
+        if tastes_like_gdal(transform):
+            warnings.warn(
+                "GDAL-style transforms are deprecated and will not "
+                "be supported in Rasterio 1.0.",
+                FutureWarning,
+                stacklevel=2)
+            transform = Affine.from_gdal(*transform)
+        else:
+            transform = Affine(*transform)
+    a, e = transform.a, transform.e
+    if a == 0.0 or e == 0.0:
+        raise ValueError(
+            "Transform has invalid coefficients a, e: (%f, %f)" % (
+                transform.a, transform.e))
+    return transform
+
diff --git a/rasterio/warp.py b/rasterio/warp.py
new file mode 100644
index 0000000..b01f841
--- /dev/null
+++ b/rasterio/warp.py
@@ -0,0 +1,46 @@
+"""Raster warping and reprojection"""
+
+from rasterio._warp import _reproject, _transform, _transform_geom, RESAMPLING
+from rasterio.transform import guard_transform
+
+
+def transform(src_crs, dst_crs, xs, ys):
+    """Return transformed vectors of x and y."""
+    return _transform(src_crs, dst_crs, xs, ys)
+
+
+def transform_geom(
+        src_crs, dst_crs, geom,
+        antimeridian_cutting=False, antimeridian_offset=10.0, precision=-1):
+    """Return transformed geometry."""
+    return _transform_geom(
+        src_crs, dst_crs, geom,
+        antimeridian_cutting, antimeridian_offset, precision)
+
+
+def reproject(
+        source, destination,
+        src_transform=None, src_crs=None,
+        dst_transform=None, dst_crs=None,
+        resampling=RESAMPLING.nearest,
+        **kwargs):
+    """Reproject a source raster to a destination.
+
+    If the source and destination are ndarrays, coordinate reference
+    system definitions and affine transformation parameters are required
+    for reprojection.
+
+    If the source and destination are rasterio Bands, shorthand for
+    bands of datasets on disk, the coordinate reference systems and
+    transforms will be read from the appropriate datasets.
+    """
+    if src_transform:
+        src_transform = guard_transform(src_transform).to_gdal()
+    if dst_transform:
+        dst_transform = guard_transform(dst_transform).to_gdal()
+
+    _reproject(
+        source, destination,
+        src_transform, src_crs,
+        dst_transform, dst_crs,
+        resampling, **kwargs)
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..41a8d9f
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,7 @@
+git+https://github.com/sgillies/affine.git#egg=affine
+Cython>=0.20
+Numpy>=1.8.0
+pytest
+coveralls>=0.4
+setuptools
+six
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e095453
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+affine>=1.0
+click
+enum34
+numpy>=1.8
+setuptools
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..5eea52d
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[nosetests]
+tests=rasterio/tests
+nocapture=True
+verbosity=3
+logging-filter=rasterio
+logging-level=DEBUG
+with-coverage=1
+cover-package=rasterio
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..98fbc4c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+import logging
+import os
+import subprocess
+import sys
+from setuptools import setup
+
+from distutils.extension import Extension
+
+logging.basicConfig()
+log = logging.getLogger()
+
+# Parse the version from the fiona module.
+with open('rasterio/__init__.py') as f:
+    for line in f:
+        if line.find("__version__") >= 0:
+            version = line.split("=")[1].strip()
+            version = version.strip('"')
+            version = version.strip("'")
+            continue
+
+with open('VERSION.txt', 'w') as f:
+    f.write(version)
+
+# Use Cython if available.
+try:
+    from Cython.Build import cythonize
+except ImportError:
+    cythonize = None
+
+# By default we'll try to get options via gdal-config. On systems without,
+# options will need to be set in setup.cfg or on the setup command line.
+include_dirs = []
+library_dirs = []
+libraries = []
+extra_link_args = []
+
+try:
+    import numpy
+    include_dirs.append(numpy.get_include())
+except ImportError:
+    log.critical("Numpy and its headers are required to run setup(). Exiting.")
+    sys.exit(1)
+
+try:
+    gdal_config = "gdal-config"
+    with open("gdal-config.txt", "w") as gcfg:
+        subprocess.call([gdal_config, "--cflags"], stdout=gcfg)
+        subprocess.call([gdal_config, "--libs"], stdout=gcfg)
+    with open("gdal-config.txt", "r") as gcfg:
+        cflags = gcfg.readline().strip()
+        libs = gcfg.readline().strip()
+    for item in cflags.split():
+        if item.startswith("-I"):
+            include_dirs.extend(item[2:].split(":"))
+    for item in libs.split():
+        if item.startswith("-L"):
+            library_dirs.extend(item[2:].split(":"))
+        elif item.startswith("-l"):
+            libraries.append(item[2:])
+        else:
+            # e.g. -framework GDAL
+            extra_link_args.append(item)
+except Exception as e:
+    log.warning("Failed to get options via gdal-config: %s", str(e))
+
+ext_options = dict(
+    include_dirs=include_dirs,
+    library_dirs=library_dirs,
+    libraries=libraries,
+    extra_link_args=extra_link_args)
+
+# When building from a repo, Cython is required.
+if os.path.exists("MANIFEST.in") and "clean" not in sys.argv:
+    log.info("MANIFEST.in found, presume a repo, cythonizing...")
+    if not cythonize:
+        log.critical(
+            "Cython.Build.cythonize not found. "
+            "Cython is required to build from a repo.")
+        sys.exit(1)
+    ext_modules = cythonize([
+        Extension(
+            'rasterio._base', ['rasterio/_base.pyx'], **ext_options),
+        Extension(
+            'rasterio._io', ['rasterio/_io.pyx'], **ext_options),
+        Extension(
+            'rasterio._copy', ['rasterio/_copy.pyx'], **ext_options),
+        Extension(
+            'rasterio._features', ['rasterio/_features.pyx'], **ext_options),
+        Extension(
+            'rasterio._drivers', ['rasterio/_drivers.pyx'], **ext_options),
+        Extension(
+            'rasterio._warp', ['rasterio/_warp.pyx'], **ext_options),
+        Extension(
+            'rasterio._err', ['rasterio/_err.pyx'], **ext_options),
+        Extension(
+            'rasterio._example', ['rasterio/_example.pyx'], **ext_options),
+            ])
+
+# If there's no manifest template, as in an sdist, we just specify .c files.
+else:
+    ext_modules = [
+        Extension(
+            'rasterio._base', ['rasterio/_base.c'], **ext_options),
+        Extension(
+            'rasterio._io', ['rasterio/_io.c'], **ext_options),
+        Extension(
+            'rasterio._copy', ['rasterio/_copy.c'], **ext_options),
+        Extension(
+            'rasterio._features', ['rasterio/_features.c'], **ext_options),
+        Extension(
+            'rasterio._drivers', ['rasterio/_drivers.c'], **ext_options),
+        Extension(
+            'rasterio._warp', ['rasterio/_warp.cpp'], **ext_options),
+        Extension(
+            'rasterio._err', ['rasterio/_err.c'], **ext_options),
+        Extension(
+            'rasterio._example', ['rasterio/_example.c'], **ext_options),
+            ]
+
+with open('README.rst') as f:
+    readme = f.read()
+
+# Runtime requirements.
+inst_reqs = [
+    'affine>=1.0',
+    'click',
+    'Numpy>=1.7',
+    'setuptools' ] 
+
+if sys.version_info < (3, 4):
+    inst_reqs.append('enum34')
+
+setup(name='rasterio',
+      version=version,
+      description=(
+          "Fast and direct raster I/O for Python programmers who use Numpy"),
+      long_description=readme,
+      classifiers=[
+          'Development Status :: 4 - Beta',
+          'Intended Audience :: Developers',
+          'Intended Audience :: Information Technology',
+          'Intended Audience :: Science/Research',
+          'License :: OSI Approved :: BSD License',
+          'Programming Language :: C',
+          'Programming Language :: Python :: 2.6',
+          'Programming Language :: Python :: 2.7',
+          'Programming Language :: Python :: 3.3',
+          'Programming Language :: Python :: 3.4',
+          'Topic :: Multimedia :: Graphics :: Graphics Conversion',
+          'Topic :: Scientific/Engineering :: GIS'
+          ],
+      keywords='raster gdal',
+      author='Sean Gillies',
+      author_email='sean at mapbox.com',
+      url='https://github.com/mapbox/rasterio',
+      license='BSD',
+      package_dir={'': '.'},
+      packages=['rasterio', 'rasterio.rio'],
+      entry_points='''
+        [console_scripts]
+        rio=rasterio.rio.main:cli
+      ''',
+      include_package_data=True,
+      ext_modules=ext_modules,
+      zip_safe=False,
+      install_requires=inst_reqs )
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..e55a25e
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,21 @@
+import functools
+import operator
+import os
+import sys
+
+import pytest
+
+if sys.version_info > (3,):
+    reduce = functools.reduce
+
+test_files = [os.path.join(os.path.dirname(__file__), p) for p in [
+    'data/RGB.byte.tif', 'data/float.tif', 'data/float_nan.tif', 'data/shade.tif']]
+
+def pytest_cmdline_main(config):
+    # Bail if the test raster data is not present. Test data is not 
+    # distributed with sdists since 0.12.
+    if reduce(operator.and_, map(os.path.exists, test_files)):
+        print("Test data present.")
+    else:
+        print("Test data not present. See download directions in tests/README.txt")
+        sys.exit(1)
diff --git a/tests/data/README.rst b/tests/data/README.rst
new file mode 100644
index 0000000..9c85140
--- /dev/null
+++ b/tests/data/README.rst
@@ -0,0 +1,8 @@
+Testing
+=======
+
+Rasterio's tests require several raster data files. Grab them from
+
+https://github.com/mapbox/rasterio/tree/master/tests/data
+
+and copy them to this directory.
diff --git a/tests/data/RGB.byte.tif b/tests/data/RGB.byte.tif
new file mode 100644
index 0000000..1efaf4a
Binary files /dev/null and b/tests/data/RGB.byte.tif differ
diff --git a/tests/data/float.tif b/tests/data/float.tif
new file mode 100644
index 0000000..96c67bc
Binary files /dev/null and b/tests/data/float.tif differ
diff --git a/tests/data/float_nan.tif b/tests/data/float_nan.tif
new file mode 100644
index 0000000..dec973f
Binary files /dev/null and b/tests/data/float_nan.tif differ
diff --git a/tests/data/shade.tif b/tests/data/shade.tif
new file mode 100644
index 0000000..6a5a226
Binary files /dev/null and b/tests/data/shade.tif differ
diff --git a/tests/test_band.py b/tests/test_band.py
new file mode 100644
index 0000000..71fb830
--- /dev/null
+++ b/tests/test_band.py
@@ -0,0 +1,10 @@
+import rasterio
+
+def test_band():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        b = rasterio.band(src, 1)
+        assert b.ds == src
+        assert b.bidx == 1
+        assert b.dtype in src.dtypes
+        assert b.shape == src.shape
+
diff --git a/tests/test_blocks.py b/tests/test_blocks.py
new file mode 100644
index 0000000..912baf5
--- /dev/null
+++ b/tests/test_blocks.py
@@ -0,0 +1,123 @@
+import logging
+import os.path
+import unittest
+import shutil
+import subprocess
+import sys
+import tempfile
+
+import numpy
+
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+class WindowTest(unittest.TestCase):
+    def test_window_shape_errors(self):
+        # Positive height and width are needed when stop is None.
+        self.assertRaises(
+            ValueError,
+            rasterio.window_shape, 
+            (((10, 20),(10, None)),) )
+        self.assertRaises(
+            ValueError,
+            rasterio.window_shape, 
+            (((None, 10),(10, 20)),) )
+    def test_window_shape_None_start(self):
+        self.assertEqual(
+            rasterio.window_shape(((None,4),(None,102))),
+            (4, 102))
+    def test_window_shape_None_stop(self):
+        self.assertEqual(
+            rasterio.window_shape(((10, None),(10, None)), 100, 90),
+            (90, 80))
+    def test_window_shape_positive(self):
+        self.assertEqual(
+            rasterio.window_shape(((0,4),(1,102))),
+            (4, 101))
+    def test_window_shape_negative(self):
+        self.assertEqual(
+            rasterio.window_shape(((-10, None),(-10, None)), 100, 90),
+            (10, 10))
+        self.assertEqual(
+            rasterio.window_shape(((~0, None),(~0, None)), 100, 90),
+            (1, 1))
+        self.assertEqual(
+            rasterio.window_shape(((None, ~0),(None, ~0)), 100, 90),
+            (99, 89))
+    def test_eval(self):
+        self.assertEqual(
+            rasterio.eval_window(((-10, None), (-10, None)), 100, 90),
+            ((90, 100), (80, 90)))
+        self.assertEqual(
+            rasterio.eval_window(((None, -10), (None, -10)), 100, 90),
+            ((0, 90), (0, 80)))
+
+def test_window_index():
+    idx = rasterio.window_index(((0,4),(1,12)))
+    assert len(idx) == 2
+    r, c = idx
+    assert r.start == 0
+    assert r.stop == 4
+    assert c.start == 1
+    assert c.stop == 12
+    arr = numpy.ones((20,20))
+    assert arr[idx].shape == (4, 11)
+
+class RasterBlocksTest(unittest.TestCase):
+    def test_blocks(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            self.assertEqual(len(s.block_shapes), 3)
+            self.assertEqual(s.block_shapes, [(3, 791), (3, 791), (3, 791)])
+            windows = s.block_windows(1)
+            (j,i), first = next(windows)
+            self.assertEqual((j,i), (0, 0))
+            self.assertEqual(first, ((0, 3), (0, 791)))
+            windows = s.block_windows()
+            (j,i), first = next(windows)
+            self.assertEqual((j,i), (0, 0))
+            self.assertEqual(first, ((0, 3), (0, 791)))
+            (j, i), second = next(windows)
+            self.assertEqual((j,i), (1, 0))
+            self.assertEqual(second, ((3, 6), (0, 791)))
+            (j, i), last = list(windows)[~0]
+            self.assertEqual((j,i), (239, 0))
+            self.assertEqual(last, ((717, 718), (0, 791)))
+    def test_block_coverage(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            self.assertEqual(
+                s.width*s.height,
+                sum((w[0][1]-w[0][0])*(w[1][1]-w[1][0]) 
+                    for ji, w in s.block_windows(1)))
+
+class WindowReadTest(unittest.TestCase):
+    def test_read_window(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            windows = s.block_windows(1)
+            ji, first_window = next(windows)
+            first_block = s.read_band(1, window=first_window)
+            self.assertEqual(first_block.dtype, rasterio.ubyte)
+            self.assertEqual(
+                first_block.shape, 
+                rasterio.window_shape(first_window))
+
+class WindowWriteTest(unittest.TestCase):
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+    def test_write_window(self):
+        name = os.path.join(self.tempdir, "test_write_window.tif")
+        a = numpy.ones((50, 50), dtype=rasterio.ubyte) * 127
+        with rasterio.open(
+                name, 'w', 
+                driver='GTiff', width=100, height=100, count=1, 
+                dtype=a.dtype) as s:
+            s.write_band(1, a, window=((30, 80), (10, 60)))
+        # subprocess.call(["open", name])
+        info = subprocess.check_output(["gdalinfo", "-stats", name])
+        self.assert_(
+            "Minimum=0.000, Maximum=127.000, "
+            "Mean=31.750, StdDev=54.993" in info.decode('utf-8'),
+            info)
+
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..35cbabb
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,115 @@
+import subprocess
+
+
+def test_cli_bounds_obj_bbox():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --bbox --precision 6',
+        shell=True)
+    assert result.decode('utf-8').strip() == '[-78.898133, 23.564991, -76.599438, 25.550874]'
+
+
+def test_cli_bounds_obj_bbox_mercator():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --bbox --mercator --precision 3',
+        shell=True)
+    assert result.decode('utf-8').strip() == '[-8782900.033, 2700489.278, -8527010.472, 2943560.235]'
+
+
+def test_cli_bounds_obj_feature():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --feature --precision 6',
+        shell=True)
+    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}'
+
+
+def test_cli_bounds_obj_collection():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --precision 6',
+        shell=True)
+    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
+
+
+def test_cli_bounds_seq_feature_rs():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --feature --precision 6',
+        shell=True)
+    assert result.decode('utf-8').startswith(u'\x1e')
+    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}'
+
+
+def test_cli_bounds_seq_collection():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --precision 6',
+        shell=True)
+    assert result.decode('utf-8').startswith(u'\x1e')
+    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
+
+
+def test_cli_bounds_seq_bbox():
+    result = subprocess.check_output(
+        'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --bbox --precision 6',
+        shell=True)
+    assert result.decode('utf-8').startswith(u'\x1e')
+    assert result.decode('utf-8').strip() == '[-78.898133, 23.564991, -76.599438, 25.550874]'
+
+
+def test_cli_bounds_seq_collection_multi(tmpdir):
+    filename = str(tmpdir.join("test.json"))
+    tmp = open(filename, 'w')
+
+    subprocess.check_call(
+        'rio bounds tests/data/RGB.byte.tif tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --precision 6',
+        stdout=tmp,
+        shell=True)
+
+    tmp.close()
+    tmp = open(filename, 'r')
+    json_texts = []
+    text = ""
+    for line in tmp:
+        rs_idx = line.find(u'\x1e')
+        if rs_idx >= 0:
+            if text:
+                text += line[:rs_idx]
+                json_texts.append(text)
+            text = line[rs_idx+1:]
+        else:
+            text += line
+    else:
+        json_texts.append(text)
+
+    assert len(json_texts) == 2
+    assert json_texts[0].strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
+    assert json_texts[1].strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "1", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
+
+
+def test_cli_info_count():
+    result = subprocess.check_output(
+        'if [ `rio info tests/data/RGB.byte.tif --count` -eq 3 ]; '
+        'then echo "True"; fi',
+        shell=True)
+    assert result.decode('utf-8').strip() == 'True'
+
+
+def test_cli_info_nodata():
+    result = subprocess.check_output(
+        'if [ `rio info tests/data/RGB.byte.tif --nodata` = "0.0" ]; '
+        'then echo "True"; fi',
+        shell=True)
+    assert result.decode('utf-8').strip() == 'True'
+
+
+def test_cli_info_dtype():
+    result = subprocess.check_output(
+        'if [ `rio info tests/data/RGB.byte.tif --dtype` = "uint8" ]; '
+        'then echo "True"; fi',
+        shell=True)
+    assert result.decode('utf-8').strip() == 'True'
+
+
+def test_cli_info_shape():
+    result = subprocess.check_output(
+        'if [[ `rio info tests/data/RGB.byte.tif --shape` == "718 791" ]]; '
+        'then echo "True"; fi',
+        shell=True, executable='/bin/bash')
+    assert result.decode('utf-8').strip() == 'True'
diff --git a/tests/test_colorinterp.py b/tests/test_colorinterp.py
new file mode 100644
index 0000000..d05e8a2
--- /dev/null
+++ b/tests/test_colorinterp.py
@@ -0,0 +1,25 @@
+
+import rasterio
+from rasterio.enums import ColorInterp
+
+
+def test_colorinterp(tmpdir):
+    
+    with rasterio.drivers():
+
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            assert src.colorinterp(1) == ColorInterp.red
+            assert src.colorinterp(2) == ColorInterp.green
+            assert src.colorinterp(3) == ColorInterp.blue
+            
+        tiffname = str(tmpdir.join('foo.tif'))
+        
+        meta = src.meta
+        meta['photometric'] = 'CMYK'
+        meta['count'] = 4
+        with rasterio.open(tiffname, 'w', **meta) as dst:
+            assert dst.colorinterp(1) == ColorInterp.cyan
+            assert dst.colorinterp(2) == ColorInterp.magenta
+            assert dst.colorinterp(3) == ColorInterp.yellow
+            assert dst.colorinterp(4) == ColorInterp.black
+
diff --git a/tests/test_colormap.py b/tests/test_colormap.py
new file mode 100644
index 0000000..e5899fb
--- /dev/null
+++ b/tests/test_colormap.py
@@ -0,0 +1,33 @@
+import logging
+import pytest
+import subprocess
+import sys
+
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+def test_write_colormap(tmpdir):
+
+    with rasterio.drivers():
+
+        with rasterio.open('tests/data/shade.tif') as src:
+            shade = src.read_band(1)
+            meta = src.meta
+
+        tiffname = str(tmpdir.join('foo.tif'))
+        
+        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)})
+            cmap = dst.colormap(1)
+            assert cmap[0] == (255, 0, 0, 255)
+            assert cmap[255] == (0, 0, 255, 255)
+
+        with rasterio.open(tiffname) as src:
+            cmap = src.colormap(1)
+            assert cmap[0] == (255, 0, 0, 255)
+            assert cmap[255] == (0, 0, 255, 255)
+
+    # subprocess.call(['open', tiffname])
+
diff --git a/tests/test_coords.py b/tests/test_coords.py
new file mode 100644
index 0000000..69eafcf
--- /dev/null
+++ b/tests/test_coords.py
@@ -0,0 +1,34 @@
+
+import rasterio
+
+def test_bounds():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert src.bounds == (101985.0, 2611485.0, 339315.0, 2826915.0)
+
+def test_ul():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert src.ul(0, 0) == (101985.0, 2826915.0)
+        assert src.ul(1, 0) == (101985.0, 2826614.95821727)
+        assert src.ul(src.height, src.width) == (339315.0, 2611485.0)
+        assert tuple(
+            round(v, 6) for v in src.ul(~0, ~0)
+            ) == (339014.962073, 2611785.041783)
+
+def test_res():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert tuple(round(v, 6) for v in src.res) == (300.037927, 300.041783)
+
+def test_index():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert src.index(101985.0, 2826915.0) == (0, 0)
+        assert src.index(101985.0+400.0, 2826915.0) == (0, 1)
+        assert src.index(101985.0+400.0, 2826915.0-700.0) == (2, 1)
+
+def test_window():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        left, bottom, right, top = src.bounds
+        assert src.window(left, bottom, right, top) == ((0, src.height), 
+                                                        (0, src.width))
+        assert src.window(left, top-400, left+400, top) == ((0, 1), (0, 1))
+        assert src.window(left, top-500, left+500, top) == ((0, 2), (0, 2))
+
diff --git a/tests/test_copy.py b/tests/test_copy.py
new file mode 100644
index 0000000..4d2b4dc
--- /dev/null
+++ b/tests/test_copy.py
@@ -0,0 +1,26 @@
+import logging
+import os.path
+import unittest
+import shutil
+import subprocess
+import sys
+import tempfile
+
+import numpy
+
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+class CopyTest(unittest.TestCase):
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+    def test_copy(self):
+        name = os.path.join(self.tempdir, 'test_copy.tif')
+        rasterio.copy(
+            'tests/data/RGB.byte.tif', 
+            name)
+        info = subprocess.check_output(["gdalinfo", name])
+        self.assert_("GTiff" in info.decode('utf-8'))
diff --git a/tests/test_crs.py b/tests/test_crs.py
new file mode 100644
index 0000000..4bb549b
--- /dev/null
+++ b/tests/test_crs.py
@@ -0,0 +1,66 @@
+import logging
+import pytest
+import subprocess
+import sys
+
+import rasterio
+from rasterio import 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():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            assert src.crs == {'init': 'epsg:32618'}
+
+def test_read_epsg3857(tmpdir):
+    tiffname = str(tmpdir.join('lol.tif'))
+    subprocess.call([
+        'gdalwarp', '-t_srs', 'EPSG:3857', 
+        'tests/data/RGB.byte.tif', tiffname])
+    with rasterio.drivers():
+        with rasterio.open(tiffname) as src:
+            assert src.crs == {'init': 'epsg:3857'}
+
+# Ensure that CRS sticks when we write a file.
+def test_write_3857(tmpdir):
+    src_path = str(tmpdir.join('lol.tif'))
+    subprocess.call([
+        'gdalwarp', '-t_srs', 'EPSG:3857', 
+        'tests/data/RGB.byte.tif', src_path])
+    dst_path = str(tmpdir.join('wut.tif'))
+    with rasterio.drivers():
+        with rasterio.open(src_path) as src:
+            with rasterio.open(dst_path, 'w', **src.meta) as dst:
+                assert dst.crs == {'init': 'epsg:3857'}
+    info = subprocess.check_output([
+        'gdalinfo', dst_path])
+    assert """PROJCS["WGS 84 / Pseudo-Mercator",
+    GEOGCS["WGS 84",
+        DATUM["WGS_1984",
+            SPHEROID["WGS 84",6378137,298.257223563,
+                AUTHORITY["EPSG","7030"]],
+            AUTHORITY["EPSG","6326"]],
+        PRIMEM["Greenwich",0],
+        UNIT["degree",0.0174532925199433],
+        AUTHORITY["EPSG","4326"]],
+    PROJECTION["Mercator_1SP"],
+    PARAMETER["central_meridian",0],
+    PARAMETER["scale_factor",1],
+    PARAMETER["false_easting",0],
+    PARAMETER["false_northing",0],
+    UNIT["metre",1,
+        AUTHORITY["EPSG","9001"]],
+    EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext  +no_defs"],
+    AUTHORITY["EPSG","3857"]]""" in info.decode('utf-8')
+
+
+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,
+    which makes presents bare parameters as key=<bool>."""
+
+    # Example produced by pyproj
+    crs_dict = 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')
+    assert crs_dict.get('no_defs', False) is True
diff --git a/tests/test_driver_management.py b/tests/test_driver_management.py
new file mode 100644
index 0000000..7368ed3
--- /dev/null
+++ b/tests/test_driver_management.py
@@ -0,0 +1,47 @@
+import logging
+import sys
+
+import rasterio
+from rasterio._drivers import driver_count, GDALEnv
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+def test_drivers():
+    with rasterio.drivers() as m:
+        assert driver_count() > 0
+        assert type(m) == GDALEnv
+        
+        n = rasterio.drivers()
+        assert driver_count() > 0
+        assert type(n) == GDALEnv
+
+def test_options(tmpdir):
+    """Test that setting CPL_DEBUG=True results in GDAL debug messages.
+    """
+    logger = logging.getLogger('GDAL')
+    logger.setLevel(logging.DEBUG)
+    logfile1 = str(tmpdir.join('test_options1.log'))
+    fh = logging.FileHandler(logfile1)
+    logger.addHandler(fh)
+    
+    # With CPL_DEBUG=True, expect debug messages from GDAL in
+    # logfile1
+    with rasterio.drivers(CPL_DEBUG=True):
+        with rasterio.open("tests/data/RGB.byte.tif") as src:
+            pass
+
+    log = open(logfile1).read()
+    assert "GDAL: GDALOpen(tests/data/RGB.byte.tif" in log
+    
+    # The GDAL env above having exited, CPL_DEBUG should be OFF.
+    logfile2 = str(tmpdir.join('test_options2.log'))
+    fh = logging.FileHandler(logfile2)
+    logger.addHandler(fh)
+
+    with rasterio.open("tests/data/RGB.byte.tif") as src:
+        pass
+    
+    # Expect no debug messages from GDAL.
+    log = open(logfile2).read()
+    assert "GDAL: GDALOpen(tests/data/RGB.byte.tif" not in log
+
diff --git a/tests/test_dtypes.py b/tests/test_dtypes.py
new file mode 100644
index 0000000..7a41fc7
--- /dev/null
+++ b/tests/test_dtypes.py
@@ -0,0 +1,14 @@
+import numpy as np
+
+import rasterio.dtypes
+
+def test_np_dt_uint8():
+    assert rasterio.dtypes.check_dtype(np.uint8)
+
+def test_dt_ubyte():
+    assert rasterio.dtypes.check_dtype(rasterio.ubyte)
+
+def test_gdal_name():
+    assert rasterio.dtypes._gdal_typename(rasterio.ubyte) == 'Byte'
+    assert rasterio.dtypes._gdal_typename(np.uint8) == 'Byte'
+    assert rasterio.dtypes._gdal_typename(np.uint16) == 'UInt16'
diff --git a/tests/test_features_rasterize.py b/tests/test_features_rasterize.py
new file mode 100644
index 0000000..2d57da7
--- /dev/null
+++ b/tests/test_features_rasterize.py
@@ -0,0 +1,157 @@
+import logging
+import sys
+import numpy
+import pytest
+
+import rasterio
+from rasterio.features import shapes, rasterize
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_rasterize_geometries():
+    """
+    Make sure that geometries are correctly rasterized according to parameters
+    """
+
+    rows = cols = 10
+    transform = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
+    geometry = {
+        'type': 'Polygon',
+        'coordinates': [[(2, 2), (2, 4.25), (4.25, 4.25), (4.25, 2), (2, 2)]]
+    }
+
+    with rasterio.drivers():
+        # we expect a subset of the pixels using default mode
+        result = rasterize([geometry], out_shape=(rows, cols))
+        truth = numpy.zeros((rows, cols))
+        truth[2:4, 2:4] = 1
+        assert numpy.array_equal(result, truth)
+
+        # we expect all touched pixels
+        result = rasterize(
+            [geometry], out_shape=(rows, cols), all_touched=True
+        )
+        truth = numpy.zeros((rows, cols))
+        truth[2:5, 2:5] = 1
+        assert numpy.array_equal(result, truth)
+
+        # we expect the pixel value to match the one we pass in
+        value = 5
+        result = rasterize([(geometry, value)], out_shape=(rows, cols))
+        truth = numpy.zeros((rows, cols))
+        truth[2:4, 2:4] = value
+        assert numpy.array_equal(result, truth)
+
+        # Check the fill and default transform.
+        # we expect the pixel value to match the one we pass in
+        value = 5
+        result = rasterize(
+            [(geometry, value)],
+            out_shape=(rows, cols),
+            fill=1
+        )
+        truth = numpy.ones((rows, cols))
+        truth[2:4, 2:4] = value
+        assert numpy.array_equal(result, truth)
+
+
+def test_rasterize_dtype():
+    """Make sure that data types are handled correctly"""
+
+    rows = cols = 10
+    transform = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
+    geometry = {
+        'type': 'Polygon',
+        'coordinates': [[(2, 2), (2, 4.25), (4.25, 4.25), (4.25, 2), (2, 2)]]
+    }
+
+    with rasterio.drivers():
+        # Supported types should all work properly
+        supported_types = (
+            ('int16', -32768),
+            ('int32', -2147483648),
+            ('uint8', 255),
+            ('uint16', 65535),
+            ('uint32', 4294967295),
+            ('float32', 1.434532),
+            ('float64', -98332.133422114)
+        )
+
+        for dtype, default_value in supported_types:
+            truth = numpy.zeros((rows, cols), dtype=dtype)
+            truth[2:4, 2:4] = default_value
+
+            result = rasterize(
+                [geometry],
+                out_shape=(rows, cols),
+                default_value=default_value,
+                dtype=dtype
+            )
+            assert numpy.array_equal(result, truth)
+            assert numpy.dtype(result.dtype) == numpy.dtype(truth.dtype)
+
+            result = rasterize(
+                [(geometry, default_value)],
+                out_shape=(rows, cols)
+            )
+            if numpy.dtype(dtype).kind == 'f':
+                assert numpy.allclose(result, truth)
+            else:
+                assert numpy.array_equal(result, truth)
+            # Since dtype is auto-detected, it may not match due to upcasting
+
+        # Unsupported types should all raise exceptions
+        unsupported_types = (
+            ('int8', -127),
+            ('int64', 20439845334323),
+            ('float16', -9343.232)
+        )
+
+        for dtype, default_value in unsupported_types:
+            with pytest.raises(ValueError):
+                rasterize(
+                    [geometry],
+                    out_shape=(rows, cols),
+                    default_value=default_value,
+                    dtype=dtype
+                )
+
+            with pytest.raises(ValueError):
+                rasterize(
+                    [(geometry, default_value)],
+                    out_shape=(rows, cols),
+                    dtype=dtype
+                )
+
+        # Mismatched values and dtypes should raise exceptions
+        mismatched_types = (('uint8', 3.2423), ('uint8', -2147483648))
+        for dtype, default_value in mismatched_types:
+            with pytest.raises(ValueError):
+                rasterize(
+                    [geometry],
+                    out_shape=(rows, cols),
+                    default_value=default_value,
+                    dtype=dtype
+                )
+
+            with pytest.raises(ValueError):
+                rasterize(
+                    [(geometry, default_value)],
+                    out_shape=(rows, cols),
+                    dtype=dtype
+                )
+
+
+def test_rasterize_geometries_symmetric():
+    """Make sure that rasterize is symmetric with shapes"""
+
+    rows = cols = 10
+    transform = (1.0, 0.0, 0.0, 0.0, -1.0, 0.0)
+    truth = numpy.zeros((rows, cols), dtype=rasterio.ubyte)
+    truth[2:5, 2:5] = 1
+    with rasterio.drivers():
+        s = shapes(truth, transform=transform)
+        result = rasterize(s, out_shape=(rows, cols), transform=transform)
+        assert numpy.array_equal(result, truth)
diff --git a/tests/test_features_shapes.py b/tests/test_features_shapes.py
new file mode 100644
index 0000000..4ab9d78
--- /dev/null
+++ b/tests/test_features_shapes.py
@@ -0,0 +1,110 @@
+import logging
+import sys
+import numpy
+import pytest
+
+import rasterio
+import rasterio.features as ftrz
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_shapes():
+    """Test creation of shapes from pixel values"""
+
+    image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
+    image[5:15, 5:15] = 127
+    with rasterio.drivers():
+        shapes = ftrz.shapes(image)
+        shape, val = next(shapes)
+        assert shape['type'] == 'Polygon'
+        assert len(shape['coordinates']) == 2  # exterior and hole
+        assert val == 0
+        shape, val = next(shapes)
+        assert shape['type'] == 'Polygon'
+        assert len(shape['coordinates']) == 1  # no hole
+        assert val == 127
+        try:
+            shape, val = next(shapes)
+        except StopIteration:
+            assert True
+        else:
+            assert False
+
+
+def test_shapes_band_shortcut():
+    """Test rasterio bands as input to shapes"""
+
+    with rasterio.drivers():
+        with rasterio.open('tests/data/shade.tif') as src:
+            shapes = ftrz.shapes(rasterio.band(src, 1))
+            shape, val = next(shapes)
+            assert shape['type'] == 'Polygon'
+            assert len(shape['coordinates']) == 1
+            assert val == 255
+
+
+def test_shapes_internal_driver_manager():
+    """Make sure this works if driver is managed outside this test"""
+
+    image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
+    image[5:15, 5:15] = 127
+    shapes = ftrz.shapes(image)
+    shape, val = next(shapes)
+    assert shape['type'] == 'Polygon'
+
+
+def test_shapes_connectivity():
+    """Test connectivity options"""
+
+    image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
+    image[5:11, 5:11] = 1
+    image[11, 11] = 1
+
+    shapes = ftrz.shapes(image, connectivity=8)
+    shape, val = next(shapes)
+    assert len(shape['coordinates'][0]) == 9
+    # Note: geometry is not technically valid at this point, it has a self
+    # intersection at 11,11
+
+
+def test_shapes_dtype():
+    """Test image data type handling"""
+
+    rows = cols = 10
+    with rasterio.drivers():
+        supported_types = (
+            ('int16', -32768),
+            ('int32', -2147483648),
+            ('uint8', 255),
+            ('uint16', 65535),
+            ('float32', 1.434532)
+        )
+
+        for dtype, test_value in supported_types:
+            image = numpy.zeros((rows, cols), dtype=dtype)
+            image[2:5, 2:5] = test_value
+
+            shapes = ftrz.shapes(image)
+            shape, value = next(shapes)
+            if dtype == 'float32':
+                assert round(value, 6) == round(test_value, 6)
+            else:
+                assert value == test_value
+
+        # Unsupported types should all raise exceptions
+        unsupported_types = (
+            ('int8', -127),
+            ('uint32', 4294967295),
+            ('int64', 20439845334323),
+            ('float16', -9343.232),
+            ('float64', -98332.133422114)
+        )
+
+        for dtype, test_value in unsupported_types:
+            with pytest.raises(ValueError):
+                image = numpy.zeros((rows, cols), dtype=dtype)
+                image[2:5, 2:5] = test_value
+                shapes = ftrz.shapes(image)
+                shape, value = next(shapes)
diff --git a/tests/test_features_sieve.py b/tests/test_features_sieve.py
new file mode 100644
index 0000000..baea580
--- /dev/null
+++ b/tests/test_features_sieve.py
@@ -0,0 +1,141 @@
+import logging
+import sys
+import numpy
+import pytest
+
+import rasterio
+import rasterio.features as ftrz
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_sieve():
+    """Test sieving a 10x10 feature from an ndarray."""
+
+    image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
+    image[5:15, 5:15] = 1
+
+    # An attempt to sieve out features smaller than 100 should not change the
+    # image.
+    with rasterio.drivers():
+        sieved_image = ftrz.sieve(image, 100)
+        assert numpy.array_equal(sieved_image, image)
+
+    # Setting the size to 100 should leave us an empty, False image.
+    with rasterio.drivers():
+        sieved_image = ftrz.sieve(image, 101)
+        assert not sieved_image.any()
+
+
+def test_sieve_connectivity():
+    """Test proper behavior of connectivity"""
+
+    image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
+    image[5:15:2, 5:15] = 1
+    image[6, 4] = 1
+    image[8, 15] = 1
+    image[10, 4] = 1
+    image[12, 15] = 1
+
+    # Diagonals not connected, all become small features that will be removed
+    sieved_image = ftrz.sieve(image, 54, connectivity=4)
+    assert not sieved_image.any()
+
+    # Diagonals connected, everything is retained
+    sieved_image = ftrz.sieve(image, 54, connectivity=8)
+    assert numpy.array_equal(sieved_image, image)
+
+
+def test_sieve_output():
+    """Test proper behavior of output image, if passed into sieve"""
+
+    with rasterio.drivers():
+        shape = (20, 20)
+        image = numpy.zeros(shape, dtype=rasterio.ubyte)
+        image[5:15, 5:15] = 1
+
+        # Output should match returned array
+        output = numpy.zeros_like(image)
+        output[1:3, 1:3] = 5
+        sieved_image = ftrz.sieve(image, 100, output=output)
+        assert numpy.array_equal(output, sieved_image)
+
+        # Output of different dtype should fail
+        output = numpy.zeros(shape, dtype=rasterio.int32)
+        with pytest.raises(ValueError):
+            ftrz.sieve(image, 100, output)
+
+
+def test_sieve_mask():
+    """Test proper behavior of mask image, if passed int sieve"""
+
+    with rasterio.drivers():
+        shape = (20, 20)
+        image = numpy.zeros(shape, dtype=rasterio.ubyte)
+        image[5:15, 5:15] = 1
+        image[1:3, 1:3] = 2
+
+        # Blank mask has no effect, only areas smaller than size will be
+        # removed
+        mask = numpy.ones(shape, dtype=rasterio.bool_)
+        sieved_image = ftrz.sieve(image, 100, mask=mask)
+        truth = numpy.zeros_like(image)
+        truth[5:15, 5:15] = 1
+        assert numpy.array_equal(sieved_image, truth)
+
+        # Only areas within the overlap of the mask and values will be kept
+        mask = numpy.ones(shape, dtype=rasterio.bool_)
+        mask[7:10, 7:10] = False
+        sieved_image = ftrz.sieve(image, 100, mask=mask)
+        truth = numpy.zeros_like(image)
+        truth[7:10, 7:10] = 1
+        assert numpy.array_equal(sieved_image, truth)
+
+        # mask of other type than rasterio.bool_ should fail
+        mask = numpy.zeros(shape, dtype=rasterio.uint8)
+        with pytest.raises(ValueError):
+            ftrz.sieve(image, 100, mask=mask)
+
+
+def test_dtypes():
+    """Test data type support for sieve"""
+
+    rows = cols = 10
+    with rasterio.drivers():
+        supported_types = (
+            ('int16', -32768),
+            ('int32', -2147483648),
+            ('uint8', 255),
+            ('uint16', 65535)
+        )
+
+        for dtype, test_value in supported_types:
+            image = numpy.zeros((rows, cols), dtype=dtype)
+            image[2:5, 2:5] = test_value
+
+            # Sieve should return the original image
+            sieved_image = ftrz.sieve(image, 2)
+            assert numpy.array_equal(image, sieved_image)
+            assert numpy.dtype(sieved_image.dtype).name == dtype
+
+            # Sieve should return a blank image
+            sieved_image = ftrz.sieve(image, 10)
+            assert numpy.array_equal(numpy.zeros_like(image), sieved_image)
+            assert numpy.dtype(sieved_image.dtype).name == dtype
+
+        # Unsupported types should all raise exceptions
+        unsupported_types = (
+            ('int8', -127),
+            ('uint32', 4294967295),
+            ('int64', 20439845334323),
+            ('float16', -9343.232),
+            ('float32', 1.434532),
+            ('float64', -98332.133422114)
+        )
+
+        for dtype, test_value in unsupported_types:
+            with pytest.raises(ValueError):
+                image = numpy.zeros((rows, cols), dtype=dtype)
+                image[2:5, 2:5] = test_value
+                sieved_image = ftrz.sieve(image, 2)
diff --git a/tests/test_indexing.py b/tests/test_indexing.py
new file mode 100644
index 0000000..fbc57b4
--- /dev/null
+++ b/tests/test_indexing.py
@@ -0,0 +1,25 @@
+
+import rasterio
+
+def test_index():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        left, bottom, right, top = src.bounds
+        assert src.index(left, top) == (0, 0)
+        assert src.index(right, top) == (0, src.width)
+        assert src.index(right, bottom) == (src.height, src.width)
+        assert src.index(left, bottom) == (src.height, 0)
+
+def test_full_window():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert src.window(*src.bounds) == tuple(zip((0, 0), src.shape))
+
+def test_window_exception():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        left, bottom, right, top = src.bounds
+        left -= 1000.0
+        try:
+            _ = src.window(left, bottom, right, top)
+            assert False
+        except ValueError:
+            assert True
+
diff --git a/tests/test_nodata.py b/tests/test_nodata.py
new file mode 100644
index 0000000..1fee860
--- /dev/null
+++ b/tests/test_nodata.py
@@ -0,0 +1,46 @@
+import logging
+import pytest
+import re
+import subprocess
+import sys
+
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+def test_nodata(tmpdir):
+    dst_path = str(tmpdir.join('lol.tif'))
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            with rasterio.open(dst_path, 'w', **src.meta) as dst:
+                assert dst.meta['nodata'] == 0.0
+                assert dst.nodatavals == [0.0, 0.0, 0.0]
+    info = subprocess.check_output([
+        'gdalinfo', dst_path])
+    pattern = b'Band 1.*?NoData Value=0'
+    assert re.search(pattern, info, re.DOTALL) is not None
+    pattern = b'Band 2.*?NoData Value=0'
+    assert re.search(pattern, info, re.DOTALL) is not None
+    pattern = b'Band 2.*?NoData Value=0'
+    assert re.search(pattern, info, re.DOTALL) is not None
+
+def test_set_nodata(tmpdir):
+    dst_path = str(tmpdir.join('lol.tif'))
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            meta = src.meta
+            meta['nodata'] = 42
+            with rasterio.open(dst_path, 'w', **meta) as dst:
+                assert dst.meta['nodata'] == 42
+                assert dst.nodatavals == [42, 42, 42]
+    info = subprocess.check_output([
+        'gdalinfo', dst_path])
+    pattern = b'Band 1.*?NoData Value=42'
+    assert re.search(pattern, info, re.DOTALL) is not None
+    pattern = b'Band 2.*?NoData Value=42'
+    assert re.search(pattern, info, re.DOTALL) is not None
+    pattern = b'Band 2.*?NoData Value=42'
+    assert re.search(pattern, info, re.DOTALL) is not None
+
+
+
diff --git a/tests/test_pad.py b/tests/test_pad.py
new file mode 100644
index 0000000..8cd894c
--- /dev/null
+++ b/tests/test_pad.py
@@ -0,0 +1,15 @@
+
+import affine
+import numpy
+
+import rasterio
+
+
+def test_pad():
+    arr = numpy.ones((10, 10))
+    trans = affine.Affine(1.0, 0.0, 0.0, 0.0, -1.0, 10.0)
+    arr2, trans2 = rasterio.pad(arr, trans, 2, 'edge')
+    assert arr2.shape == (14, 14)
+    assert trans2.xoff ==  -2.0
+    assert trans2.yoff ==  12.0
+
diff --git a/tests/test_png.py b/tests/test_png.py
new file mode 100644
index 0000000..c9324a0
--- /dev/null
+++ b/tests/test_png.py
@@ -0,0 +1,20 @@
+import logging
+import subprocess
+import sys
+import re
+import numpy
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_write_ubyte(tmpdir):
+    name = str(tmpdir.mkdir("sub").join("test_write_ubyte.png"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    with rasterio.open(
+            name, 'w', 
+            driver='PNG', width=100, height=100, count=1, 
+            dtype=a.dtype) as s:
+        s.write_band(1, a)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
diff --git a/tests/test_read.py b/tests/test_read.py
new file mode 100644
index 0000000..f962138
--- /dev/null
+++ b/tests/test_read.py
@@ -0,0 +1,242 @@
+import unittest
+
+import numpy
+from hashlib import md5
+
+import rasterio
+
+
+class ReaderContextTest(unittest.TestCase):
+
+    def test_context(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            self.assertEqual(s.name, 'tests/data/RGB.byte.tif')
+            self.assertEqual(s.driver, 'GTiff')
+            self.assertEqual(s.closed, False)
+            self.assertEqual(s.count, 3)
+            self.assertEqual(s.width, 791)
+            self.assertEqual(s.height, 718)
+            self.assertEqual(s.shape, (718, 791))
+            self.assertEqual(s.dtypes, [rasterio.ubyte]*3)
+            self.assertEqual(s.nodatavals, [0]*3)
+            self.assertEqual(s.indexes, [1,2,3])
+            self.assertEqual(s.crs['init'], 'epsg:32618')
+            self.assert_(s.crs_wkt.startswith('PROJCS'), s.crs_wkt)
+            for i, v in enumerate((101985.0, 2611485.0, 339315.0, 2826915.0)):
+                self.assertAlmostEqual(s.bounds[i], v)
+            self.assertEqual(
+                s.affine, 
+                (300.0379266750948, 0.0, 101985.0,
+                 0.0, -300.041782729805, 2826915.0,
+                 0, 0, 1.0))
+            self.assertEqual(s.meta['crs'], s.crs)
+            self.assertEqual(
+                repr(s), 
+                "<open RasterReader name='tests/data/RGB.byte.tif' "
+                "mode='r'>")
+        self.assertEqual(s.closed, True)
+        self.assertEqual(s.count, 3)
+        self.assertEqual(s.width, 791)
+        self.assertEqual(s.height, 718)
+        self.assertEqual(s.shape, (718, 791))
+        self.assertEqual(s.dtypes, [rasterio.ubyte]*3)
+        self.assertEqual(s.nodatavals, [0]*3)
+        self.assertEqual(s.crs['init'], 'epsg:32618')
+        self.assertEqual(
+            s.affine, 
+            (300.0379266750948, 0.0, 101985.0,
+             0.0, -300.041782729805, 2826915.0,
+             0, 0, 1.0))
+        self.assertEqual(
+            repr(s),
+            "<closed RasterReader name='tests/data/RGB.byte.tif' "
+            "mode='r'>")
+
+    def test_derived_spatial(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            self.assert_(s.crs_wkt.startswith('PROJCS'), s.crs_wkt)
+            for i, v in enumerate((101985.0, 2611485.0, 339315.0, 2826915.0)):
+                self.assertAlmostEqual(s.bounds[i], v)
+            for a, b in zip(s.ul(0, 0), (101985.0, 2826915.0)):
+                self.assertAlmostEqual(a, b)
+
+    def test_read_ubyte(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            a = s.read_band(1)
+            self.assertEqual(a.dtype, rasterio.ubyte)
+
+    def test_read_ubyte_bad_index(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            self.assertRaises(IndexError, s.read_band, 0)
+
+    def test_read_ubyte_out(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            a = numpy.zeros((718, 791), dtype=rasterio.ubyte)
+            a = s.read_band(1, a)
+            self.assertEqual(a.dtype, rasterio.ubyte)
+
+    def test_read_out_dtype_fail(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            a = numpy.zeros((718, 791), dtype=rasterio.float32)
+            try:
+                s.read_band(1, a)
+            except ValueError as e:
+                assert "the array's dtype 'float32' does not match the file's dtype" in str(e)
+            except:
+                assert "failed to catch exception" is False
+
+    def test_read_out_shape_resample(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            a = numpy.zeros((7, 8), dtype=rasterio.ubyte)
+            s.read_band(1, a)
+            self.assert_(
+                repr(a) == """array([[  0,   8,   5,   7,   0,   0,   0,   0],
+       [  0,   6,  61,  15,  27,  15,  24, 128],
+       [  0,  20, 152,  23,  15,  19,  28,   0],
+       [  0,  17, 255,  25, 255,  22,  32,   0],
+       [  9,   7,  14,  16,  19,  18,  36,   0],
+       [  6,  27,  43, 207,  38,  31,  73,   0],
+       [  0,   0,   0,   0,  74,  23,   0,   0]], dtype=uint8)""", a)
+
+    def test_read_basic(self):
+        with rasterio.open('tests/data/shade.tif') as s:
+            a = s.read()  # Gray
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (1, 1024, 1024))
+            self.assertTrue(hasattr(a, 'mask'))
+            self.assertEqual(a.fill_value, 255)
+            self.assertEqual(list(set(s.nodatavals)), [255])
+            self.assertEqual(a.dtype, rasterio.ubyte)
+            self.assertEqual(a.sum((1, 2)).tolist(), [0])
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            a = s.read()  # RGB
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (3, 718, 791))
+            self.assertTrue(hasattr(a, 'mask'))
+            self.assertEqual(a.fill_value, 0)
+            self.assertEqual(list(set(s.nodatavals)), [0])
+            self.assertEqual(a.dtype, rasterio.ubyte)
+            a = s.read(masked=False)  # no mask
+            self.assertFalse(hasattr(a, 'mask'))
+            self.assertEqual(list(set(s.nodatavals)), [0])
+            self.assertEqual(a.dtype, rasterio.ubyte)
+        with rasterio.open('tests/data/float.tif') as s:
+            a = s.read()  # floating point values
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (1, 2, 3))
+            self.assertFalse(hasattr(a, 'mask'))
+            self.assertEqual(list(set(s.nodatavals)), [None])
+            self.assertEqual(a.dtype, rasterio.float64)
+
+    def test_read_indexes(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            a = s.read()  # RGB
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (3, 718, 791))
+            self.assertEqual(a.sum((1, 2)).tolist(),
+                             [17008452, 25282412, 27325233])
+            # read last index as 2D array
+            a = s.read(s.indexes[-1])  # B
+            self.assertEqual(a.ndim, 2)
+            self.assertEqual(a.shape, (718, 791))
+            self.assertEqual(a.sum(), 27325233)
+            # read last index as 2D array
+            a = s.read(s.indexes[-1:])  # [B]
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (1, 718, 791))
+            self.assertEqual(a.sum((1, 2)).tolist(), [27325233])
+            # out of range indexes
+            self.assertRaises(IndexError, s.read, 0)
+            self.assertRaises(IndexError, s.read, [3, 4])
+            # read slice
+            a = s.read(s.indexes[0:2])  # [RG]
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (2, 718, 791))
+            self.assertEqual(a.sum((1, 2)).tolist(), [17008452, 25282412])
+            # read stride
+            a = s.read(s.indexes[::2])  # [RB]
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (2, 718, 791))
+            self.assertEqual(a.sum((1, 2)).tolist(), [17008452, 27325233])
+            # read zero-length slice
+            try:
+                a = s.read(s.indexes[1:1])
+            except ValueError:
+                pass
+
+    def test_read_window(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            # correct format
+            self.assertRaises(ValueError, s.read, window=(300, 320, 320, 330))
+            # window with 1 nodata on band 3
+            a = s.read(window=((300, 320), (320, 330)))
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (3, 20, 10))
+            self.assertTrue(hasattr(a, 'mask'))
+            self.assertEqual(a.mask.sum((1, 2)).tolist(), [0, 0, 1])
+            self.assertEqual([md5(x.tostring()).hexdigest() for x in a],
+                              ['1df719040daa9dfdb3de96d6748345e8',
+                               'ec8fb3659f40c4a209027231bef12bdb',
+                               '5a9c12aebc126ec6f27604babd67a4e2'])
+            # window without any missing data, but still is masked result
+            a = s.read(window=((310, 330), (320, 330)))
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (3, 20, 10))
+            self.assertTrue(hasattr(a, 'mask'))
+            self.assertEqual([md5(x.tostring()).hexdigest() for x in a[:]],
+                              ['9e3000d60b4b6fb956f10dc57c4dc9b9',
+                               '6a675416a32fcb70fbcf601d01aeb6ee',
+                               '94fd2733b534376c273a894f36ad4e0b'])
+
+    def test_read_out(self):
+        with rasterio.open('tests/data/RGB.byte.tif') as s:
+            # regular array, without mask
+            a = numpy.empty((3, 718, 791), numpy.ubyte)
+            b = s.read(out=a)
+            self.assertEqual(id(a), id(b))
+            self.assertFalse(hasattr(a, 'mask'))
+            self.assertFalse(hasattr(b, 'mask'))
+            # with masked array
+            a = numpy.ma.empty((3, 718, 791), numpy.ubyte)
+            b = s.read(out=a)
+            self.assertEqual(id(a.data), id(b.data))
+            # TODO: is there a way to id(a.mask)?
+            self.assertTrue(hasattr(a, 'mask'))
+            self.assertTrue(hasattr(b, 'mask'))
+            # use all parameters
+            a = numpy.empty((1, 20, 10), numpy.ubyte)
+            b = s.read([2], a, ((310, 330), (320, 330)), False)
+            self.assertEqual(id(a), id(b))
+            # pass 2D array with index
+            a = numpy.empty((20, 10), numpy.ubyte)
+            b = s.read(2, a, ((310, 330), (320, 330)), False)
+            self.assertEqual(id(a), id(b))
+            self.assertEqual(a.ndim, 2)
+            # different data types
+            a = numpy.empty((3, 718, 791), numpy.float64)
+            self.assertRaises(ValueError, s.read, out=a)
+            # different number of array dimensions
+            a = numpy.empty((20, 10), numpy.ubyte)
+            self.assertRaises(ValueError, s.read, [2], out=a)
+            # different number of array shape in 3D
+            a = numpy.empty((2, 20, 10), numpy.ubyte)
+            self.assertRaises(ValueError, s.read, [2], out=a)
+
+    def test_read_nan_nodata(self):
+        with rasterio.open('tests/data/float_nan.tif') as s:
+            a = s.read()
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (1, 2, 3))
+            self.assertTrue(hasattr(a, 'mask'))
+            self.assertNotEqual(a.fill_value, numpy.nan)
+            self.assertEqual(str(list(set(s.nodatavals))), str([numpy.nan]))
+            self.assertEqual(a.dtype, rasterio.float32)
+            self.assertFalse(numpy.isnan(a).any())
+            a = s.read(masked=False)
+            self.assertFalse(hasattr(a, 'mask'))
+            self.assertTrue(numpy.isnan(a).any())
+            # Window does not contain a nodatavalue, result is still masked
+            a = s.read(window=((0, 2), (0, 2)))
+            self.assertEqual(a.ndim, 3)
+            self.assertEqual(a.shape, (1, 2, 2))
+            self.assertTrue(hasattr(a, 'mask'))
diff --git a/tests/test_revolvingdoor.py b/tests/test_revolvingdoor.py
new file mode 100644
index 0000000..40d0e95
--- /dev/null
+++ b/tests/test_revolvingdoor.py
@@ -0,0 +1,37 @@
+# Test of opening and closing and opening
+
+import logging
+import os.path
+import shutil
+import subprocess
+import sys
+import tempfile
+import unittest
+
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+log = logging.getLogger('rasterio.tests')
+
+class RevolvingDoorTest(unittest.TestCase):
+
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+    
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+
+    def test_write_colormap_revolving_door(self):
+
+        with rasterio.open('tests/data/shade.tif') as src:
+            shade = src.read_band(1)
+            meta = src.meta
+
+        tiffname = os.path.join(self.tempdir, 'foo.tif')
+        
+        with rasterio.open(tiffname, 'w', **meta) as dst:
+            dst.write_band(1, shade)
+
+        with rasterio.open(tiffname) as src:
+            pass
+
diff --git a/tests/test_rio_bands.py b/tests/test_rio_bands.py
new file mode 100644
index 0000000..b25b293
--- /dev/null
+++ b/tests/test_rio_bands.py
@@ -0,0 +1,79 @@
+import click
+from click.testing import CliRunner
+
+import rasterio
+from rasterio.rio import bands
+
+
+def test_photometic_choices():
+    assert len(bands.PHOTOMETRIC_CHOICES) == 8
+
+
+def test_stack(tmpdir):
+    outputname = str(tmpdir.join('stacked.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        bands.stack,
+        ['tests/data/RGB.byte.tif', '-o', outputname],
+        catch_exceptions=False)
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as out:
+        assert out.count == 3
+
+
+def test_stack_list(tmpdir):
+    outputname = str(tmpdir.join('stacked.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        bands.stack,
+        ['tests/data/RGB.byte.tif', '--bidx', '1,2,3', '-o', outputname])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as out:
+        assert out.count == 3
+
+
+def test_stack_slice(tmpdir):
+    outputname = str(tmpdir.join('stacked.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        bands.stack, 
+        [
+            'tests/data/RGB.byte.tif', '--bidx', '..2',
+            'tests/data/RGB.byte.tif', '--bidx', '3..',
+            '-o', outputname])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as out:
+        assert out.count == 3
+
+
+def test_stack_single_slice(tmpdir):
+    outputname = str(tmpdir.join('stacked.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        bands.stack, 
+        [
+            'tests/data/RGB.byte.tif', '--bidx', '1',
+            'tests/data/RGB.byte.tif', '--bidx', '2..',
+            '--photometric', 'rgb',
+            '-o', outputname])
+    assert result.exit_code == 0
+    with rasterio.open(outputname) as out:
+        assert out.count == 3
+
+
+def test_format_jpeg(tmpdir):
+    outputname = str(tmpdir.join('stacked.jpg'))
+    runner = CliRunner()
+    result = runner.invoke(
+        bands.stack,
+        ['tests/data/RGB.byte.tif', '-o', outputname, '--format', 'JPEG'])
+    assert result.exit_code == 0
+
+
+def test_error(tmpdir):
+    outputname = str(tmpdir.join('stacked.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        bands.stack,
+        ['tests/data/RGB.byte.tif', '-o', outputname, '--driver', 'BOGUS'])
+    assert result.exit_code == 1
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
new file mode 100644
index 0000000..fd56e20
--- /dev/null
+++ b/tests/test_rio_info.py
@@ -0,0 +1,57 @@
+import click
+from click.testing import CliRunner
+
+
+import rasterio
+from rasterio.rio import info
+
+
+def test_env():
+    runner = CliRunner()
+    result = runner.invoke(info.env, ['--formats'])
+    assert result.exit_code == 0
+    assert 'GTiff' in result.output
+
+
+def test_info_err():
+    runner = CliRunner()
+    result = runner.invoke(
+        info.info,
+        ['tests'])
+    assert result.exit_code == 1
+
+
+def test_info():
+    runner = CliRunner()
+    result = runner.invoke(
+        info.info,
+        ['tests/data/RGB.byte.tif'])
+    assert result.exit_code == 0
+    assert '"count": 3' in result.output
+
+
+def test_info_count():
+    runner = CliRunner()
+    result = runner.invoke(
+        info.info,
+        ['tests/data/RGB.byte.tif', '--count'])
+    assert result.exit_code == 0
+    assert result.output == '3\n'
+
+
+def test_info_nodatavals():
+    runner = CliRunner()
+    result = runner.invoke(
+        info.info,
+        ['tests/data/RGB.byte.tif', '--bounds'])
+    assert result.exit_code == 0
+    assert result.output == '101985.0 2611485.0 339315.0 2826915.0\n'
+
+
+def test_info_tags():
+    runner = CliRunner()
+    result = runner.invoke(
+        info.info,
+        ['tests/data/RGB.byte.tif', '--tags'])
+    assert result.exit_code == 0
+    assert result.output == '{"AREA_OR_POINT": "Area"}\n'
diff --git a/tests/test_rio_options.py b/tests/test_rio_options.py
new file mode 100644
index 0000000..a4b3e31
--- /dev/null
+++ b/tests/test_rio_options.py
@@ -0,0 +1,15 @@
+import click
+from click.testing import CliRunner
+
+
+import rasterio
+from rasterio.rio import rio
+
+
+def test_insp():
+    runner = CliRunner()
+    result = runner.invoke(
+        rio.cli,
+        ['--version'])
+    assert result.exit_code == 0
+    assert result.output.strip() == rasterio.__version__
diff --git a/tests/test_rio_rio.py b/tests/test_rio_rio.py
new file mode 100644
index 0000000..4780e33
--- /dev/null
+++ b/tests/test_rio_rio.py
@@ -0,0 +1,170 @@
+import click
+from click.testing import CliRunner
+
+
+import rasterio
+from rasterio.rio import rio
+
+
+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_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', '--x-json-seq', '--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', '--x-json-seq', '--x-json-seq-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_bounds_obj_feature():
+    runner = CliRunner()
+    result = runner.invoke(
+        rio.bounds,
+        ['tests/data/RGB.byte.tif', '--feature', '--precision', '6'])
+    assert result.exit_code == 0
+    assert result.output.strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}'
+
+
+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_tags.py b/tests/test_tags.py
new file mode 100644
index 0000000..4431055
--- /dev/null
+++ b/tests/test_tags.py
@@ -0,0 +1,56 @@
+#-*- coding: utf-8 -*-
+import logging
+import sys
+
+import pytest
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+def test_tags_read():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert src.tags() == {'AREA_OR_POINT': 'Area'}
+        assert src.tags(ns='IMAGE_STRUCTURE') == {'INTERLEAVE': 'PIXEL'}
+        assert src.tags(ns='bogus') == {}
+        assert 'STATISTICS_MAXIMUM' in src.tags(1)
+        with pytest.raises(ValueError):
+            tags = src.tags(4)
+
+def test_tags_update(tmpdir):
+    tiffname = str(tmpdir.join('foo.tif'))
+    with rasterio.open(
+            tiffname, 
+            'w', 
+            driver='GTiff', 
+            count=1, 
+            dtype=rasterio.uint8, 
+            width=10, 
+            height=10) as dst:
+
+        dst.update_tags(a='1', b='2')
+        dst.update_tags(1, c=3)
+        with pytest.raises(ValueError):
+            dst.update_tags(4, d=4)
+
+        assert dst.tags() == {'a': '1', 'b': '2'}
+        assert dst.tags(1) == {'c': '3' }
+        
+        # Assert that unicode tags work.
+        # Russian text appropriated from pytest issue #319
+        # https://bitbucket.org/hpk42/pytest/issue/319/utf-8-output-in-assertion-error-converted
+        dst.update_tags(ns='rasterio_testing', rus=u'другая строка')
+        assert dst.tags(ns='rasterio_testing') == {'rus': u'другая строка'}
+
+    with rasterio.open(tiffname) as src:
+        assert src.tags() == {'a': '1', 'b': '2'}
+        assert src.tags(1) == {'c': '3'}
+        assert src.tags(ns='rasterio_testing') == {'rus': u'другая строка'}
+
+def test_tags_update_twice():
+    with rasterio.open(
+            'test.tif', 'w', 
+            'GTiff', 3, 4, 1, dtype=rasterio.ubyte) as dst:
+        dst.update_tags(a=1, b=2)
+        assert dst.tags() == {'a': '1', 'b': '2'}
+        dst.update_tags(c=3)
+        assert dst.tags() == {'a': '1', 'b': '2', 'c': '3'}
diff --git a/tests/test_transform.py b/tests/test_transform.py
new file mode 100644
index 0000000..b17feab
--- /dev/null
+++ b/tests/test_transform.py
@@ -0,0 +1,11 @@
+
+import rasterio
+
+def test_window():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        left, bottom, right, top = src.bounds
+        assert src.window(left, bottom, right, top) == ((0, src.height),
+                                                        (0, src.width))
+        assert src.window(left, top-src.res[1], left+src.res[0], top) == (
+            (0, 1), (0, 1))
+
diff --git a/tests/test_update.py b/tests/test_update.py
new file mode 100644
index 0000000..8d84748
--- /dev/null
+++ b/tests/test_update.py
@@ -0,0 +1,61 @@
+
+import shutil
+import subprocess
+import re
+
+import affine
+import numpy
+import pytest
+
+import rasterio
+
+def test_update_tags(tmpdir):
+    tiffname = str(tmpdir.join('foo.tif'))
+    shutil.copy('tests/data/RGB.byte.tif', tiffname)
+    with rasterio.open(tiffname, 'r+') as f:
+        f.update_tags(a='1', b='2')
+        f.update_tags(1, c=3)
+        with pytest.raises(ValueError):
+            f.update_tags(4, d=4)
+        assert f.tags() == {'AREA_OR_POINT': 'Area', 'a': '1', 'b': '2'}
+        assert ('c', '3') in f.tags(1).items()
+    info = subprocess.check_output(["gdalinfo", tiffname]).decode('utf-8')
+    assert re.search("Metadata:\W+a=1\W+AREA_OR_POINT=Area\W+b=2", info)
+
+def test_update_band(tmpdir):
+    tiffname = str(tmpdir.join('foo.tif'))
+    shutil.copy('tests/data/RGB.byte.tif', tiffname)
+    with rasterio.open(tiffname, 'r+') as f:
+        f.write_band(1, numpy.zeros(f.shape, dtype=f.dtypes[0]))
+    with rasterio.open(tiffname) as f:
+        assert not f.read_band(1).any()
+
+def test_update_spatial(tmpdir):
+    tiffname = str(tmpdir.join('foo.tif'))
+    shutil.copy('tests/data/RGB.byte.tif', tiffname)
+    with rasterio.open(tiffname, 'r+') as f:
+        f.transform = affine.Affine.from_gdal(1.0, 1.0, 0.0, 0.0, 0.0, -1.0)
+        f.crs = {'init': 'epsg:4326'}
+    with rasterio.open(tiffname) as f:
+        assert list(f.transform) == [1.0, 1.0, 0.0, 0.0, 0.0, -1.0]
+        assert list(f.affine.to_gdal()) == [1.0, 1.0, 0.0, 0.0, 0.0, -1.0]
+        assert f.crs == {'init': 'epsg:4326'}
+
+def test_update_spatial_epsg(tmpdir):
+    tiffname = str(tmpdir.join('foo.tif'))
+    shutil.copy('tests/data/RGB.byte.tif', tiffname)
+    with rasterio.open(tiffname, 'r+') as f:
+        f.transform = affine.Affine.from_gdal(1.0, 1.0, 0.0, 0.0, 0.0, -1.0)
+        f.crs = 'EPSG:4326'
+    with rasterio.open(tiffname) as f:
+        assert list(f.transform) == [1.0, 1.0, 0.0, 0.0, 0.0, -1.0]
+        assert list(f.affine.to_gdal()) == [1.0, 1.0, 0.0, 0.0, 0.0, -1.0]
+        assert f.crs == {'init': 'epsg:4326'}
+
+def test_update_nodatavals(tmpdir):
+    tiffname = str(tmpdir.join('foo.tif'))
+    shutil.copy('tests/data/RGB.byte.tif', tiffname)
+    with rasterio.open(tiffname, 'r+') as f:
+        f.nodatavals = [-1, -1, -1]
+    with rasterio.open(tiffname) as f:
+        assert f.nodatavals == [-1, -1, -1]
diff --git a/tests/test_warp.py b/tests/test_warp.py
new file mode 100644
index 0000000..d9fef2b
--- /dev/null
+++ b/tests/test_warp.py
@@ -0,0 +1,184 @@
+
+import logging
+import subprocess
+import sys
+
+import affine
+import numpy
+
+import rasterio
+from rasterio.warp import reproject, RESAMPLING, transform_geom
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+def test_reproject():
+    """Ndarry to ndarray"""
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            source = src.read_band(1)
+        dst_transform = affine.Affine.from_gdal(-8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
+        dst_crs = dict(
+                    proj='merc',
+                    a=6378137,
+                    b=6378137,
+                    lat_ts=0.0,
+                    lon_0=0.0,
+                    x_0=0.0,
+                    y_0=0,
+                    k=1.0,
+                    units='m',
+                    nadgrids='@null',
+                    wktext=True,
+                    no_defs=True)
+        destin = numpy.empty(src.shape, dtype=numpy.uint8)
+        reproject(
+            source, 
+            destin,
+            src_transform=src.transform,
+            src_crs=src.crs,
+            dst_transform=dst_transform, 
+            dst_crs=dst_crs,
+            resampling=RESAMPLING.nearest )
+    assert destin.any()
+    try:
+        import matplotlib.pyplot as plt
+        plt.imshow(destin)
+        plt.gray()
+        plt.savefig('test_reproject.png')
+    except:
+        pass
+
+def test_reproject_multi():
+    """Ndarry to ndarray"""
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            source = src.read()
+        dst_transform = affine.Affine.from_gdal(
+                            -8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
+        dst_crs = dict(
+                    proj='merc',
+                    a=6378137,
+                    b=6378137,
+                    lat_ts=0.0,
+                    lon_0=0.0,
+                    x_0=0.0,
+                    y_0=0,
+                    k=1.0,
+                    units='m',
+                    nadgrids='@null',
+                    wktext=True,
+                    no_defs=True)
+        destin = numpy.empty(source.shape, dtype=numpy.uint8)
+        reproject(
+            source, 
+            destin,
+            src_transform=src.transform,
+            src_crs=src.crs,
+            dst_transform=dst_transform, 
+            dst_crs=dst_crs,
+            resampling=RESAMPLING.nearest )
+    assert destin.any()
+    try:
+        import matplotlib.pyplot as plt
+        plt.imshow(destin)
+        plt.gray()
+        plt.savefig('test_reproject.png')
+    except:
+        pass
+
+def test_warp_from_file():
+    """File to ndarray"""
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        dst_transform = affine.Affine.from_gdal(-8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
+        dst_crs = dict(
+                    proj='merc',
+                    a=6378137,
+                    b=6378137,
+                    lat_ts=0.0,
+                    lon_0=0.0,
+                    x_0=0.0,
+                    y_0=0,
+                    k=1.0,
+                    units='m',
+                    nadgrids='@null',
+                    wktext=True,
+                    no_defs=True)
+        destin = numpy.empty(src.shape, dtype=numpy.uint8)
+        reproject(
+            rasterio.band(src, 1), 
+            destin, 
+            dst_transform=dst_transform, 
+            dst_crs=dst_crs)
+    assert destin.any()
+    try:
+        import matplotlib.pyplot as plt
+        plt.imshow(destin)
+        plt.gray()
+        plt.savefig('test_warp_from_filereproject.png')
+    except:
+        pass
+
+def test_warp_from_to_file(tmpdir):
+    """File to file"""
+    tiffname = str(tmpdir.join('foo.tif'))
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        dst_transform = affine.Affine.from_gdal(-8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
+        dst_crs = dict(
+                    proj='merc',
+                    a=6378137,
+                    b=6378137,
+                    lat_ts=0.0,
+                    lon_0=0.0,
+                    x_0=0.0,
+                    y_0=0,
+                    k=1.0,
+                    units='m',
+                    nadgrids='@null',
+                    wktext=True,
+                    no_defs=True)
+        kwargs = src.meta.copy()
+        kwargs.update(
+            transform=dst_transform,
+            crs=dst_crs)
+        with rasterio.open(tiffname, 'w', **kwargs) as dst:
+            for i in (1, 2, 3):
+                reproject(rasterio.band(src, i), rasterio.band(dst, i))
+    # subprocess.call(['open', tiffname])
+
+def test_warp_from_to_file_multi(tmpdir):
+    """File to file"""
+    tiffname = str(tmpdir.join('foo.tif'))
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        dst_transform = affine.Affine.from_gdal(-8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
+        dst_crs = dict(
+                    proj='merc',
+                    a=6378137,
+                    b=6378137,
+                    lat_ts=0.0,
+                    lon_0=0.0,
+                    x_0=0.0,
+                    y_0=0,
+                    k=1.0,
+                    units='m',
+                    nadgrids='@null',
+                    wktext=True,
+                    no_defs=True)
+        kwargs = src.meta.copy()
+        kwargs.update(
+            transform=dst_transform,
+            crs=dst_crs)
+        with rasterio.open(tiffname, 'w', **kwargs) as dst:
+            for i in (1, 2, 3):
+                reproject(
+                    rasterio.band(src, i), 
+                    rasterio.band(dst, i),
+                    num_threads=2)
+    # subprocess.call(['open', tiffname])
+
+def test_transform_geom_wrap():
+    geom = {'type': 'Polygon', 'coordinates': (((798842.3090855901, 6569056.500655151), (756688.2826828464, 6412397.888771972), (755571.0617232556, 6408461.009397383), (677605.2284582685, 6425600.39266733), (677605.2284582683, 6425600.392667332), (670873.3791649605, 6427248.603432341), (664882.1106069803, 6407585.48425362), (663675.8662823177, 6403676.990080649), (485120.71963574126, 6449787.167760638), (485065.55660851026, 6449802.826920689), (485957.03982722526, 6452708.625101285), (48 [...]
+    result = transform_geom(
+                'EPSG:3373', 'EPSG:4326', geom, antimeridian_cutting=True)
+    assert result['type'] == 'MultiPolygon'
+    assert len(result['coordinates']) == 2
diff --git a/tests/test_write.py b/tests/test_write.py
new file mode 100644
index 0000000..121e478
--- /dev/null
+++ b/tests/test_write.py
@@ -0,0 +1,244 @@
+import logging
+import subprocess
+import sys
+import re
+import numpy
+import rasterio
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_validate_dtype_None(tmpdir):
+    name = str(tmpdir.join("lol.tif"))
+    try:
+        ds = rasterio.open(
+            name, 'w', driver='GTiff', width=100, height=100, count=1,
+            # dtype=None
+            )
+    except TypeError:
+        pass
+
+def test_validate_dtype_str(tmpdir):
+    name = str(tmpdir.join("lol.tif"))
+    try:
+        ds = rasterio.open(
+            name, 'w', driver='GTiff', width=100, height=100, count=1,
+            dtype='Int16')
+    except TypeError:
+        pass
+
+def test_validate_count_None(tmpdir):
+    name = str(tmpdir.join("lol.tif"))
+    try:
+        ds = rasterio.open(
+            name, 'w', driver='GTiff', width=100, height=100, #count=None
+            dtype=rasterio.uint8)
+    except TypeError:
+        pass
+
+def test_no_crs(tmpdir):
+    # A dataset without crs is okay.
+    name = str(tmpdir.join("lol.tif"))
+    with rasterio.open(
+            name, 'w', driver='GTiff', width=100, height=100, count=1,
+            dtype=rasterio.uint8) as dst:
+        dst.write_band(1, numpy.ones((100, 100), dtype=rasterio.uint8))
+
+def test_context(tmpdir):
+    name = str(tmpdir.join("test_context.tif"))
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=1, 
+            dtype=rasterio.ubyte) as s:
+        assert s.name == name
+        assert s.driver == 'GTiff'
+        assert s.closed == False
+        assert s.count == 1
+        assert s.width == 100
+        assert s.height == 100
+        assert s.shape == (100, 100)
+        assert s.indexes == [1]
+        assert repr(s) == "<open RasterUpdater name='%s' mode='w'>" % name
+    assert s.closed == True
+    assert s.count == 1
+    assert s.width == 100
+    assert s.height == 100
+    assert s.shape == (100, 100)
+    assert repr(s) == "<closed RasterUpdater name='%s' mode='w'>" % name
+    info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
+    assert "GTiff" in info
+    assert "Size is 100, 100" in info
+    assert "Band 1 Block=100x81 Type=Byte, ColorInterp=Gray" in info
+    
+def test_write_ubyte(tmpdir):
+    name = str(tmpdir.mkdir("sub").join("test_write_ubyte.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=1, 
+            dtype=a.dtype) as s:
+        s.write_band(1, a)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
+def test_write_ubyte_multi(tmpdir):
+    name = str(tmpdir.mkdir("sub").join("test_write_ubyte_multi.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=1, 
+            dtype=a.dtype) as s:
+        s.write(a, 1)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
+def test_write_ubyte_multi_list(tmpdir):
+    name = str(tmpdir.mkdir("sub").join("test_write_ubyte_multi_list.tif"))
+    a = numpy.array([numpy.ones((100, 100), dtype=rasterio.ubyte) * 127])
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=1, 
+            dtype=a.dtype) as s:
+        s.write(a, [1])
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
+def test_write_ubyte_multi_3(tmpdir):
+    name = str(tmpdir.mkdir("sub").join("test_write_ubyte_multi_list.tif"))
+    arr = numpy.array(3*[numpy.ones((100, 100), dtype=rasterio.ubyte) * 127])
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=3, 
+            dtype=arr.dtype) as s:
+        s.write(arr)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
+
+def test_write_float(tmpdir):
+    name = str(tmpdir.join("test_write_float.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.float32) * 42.0
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=2,
+            dtype=rasterio.float32) as s:
+        assert s.dtypes == [rasterio.float32]*2
+        s.write_band(1, a)
+        s.write_band(2, a)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=42.000, Maximum=42.000, Mean=42.000, StdDev=0.000" in info
+    
+def test_write_crs_transform(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},
+            transform=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"))
+    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='EPSG:32618',
+            transform=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["WGS 84 / UTM zone 18N",' 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_3(tmpdir):
+    """Using WKT as CRS."""
+    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]
+    crs_wkt = 'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]'
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=1,
+            crs=crs_wkt,
+            transform=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_meta(tmpdir):
+    name = str(tmpdir.join("test_write_meta.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    meta = dict(driver='GTiff', width=100, height=100, count=1)
+    with rasterio.open(name, 'w', dtype=a.dtype, **meta) as s:
+        s.write_band(1, a)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "Minimum=127.000, Maximum=127.000, Mean=127.000, StdDev=0.000" in info
+    
+def test_write_nodata(tmpdir):
+    name = str(tmpdir.join("test_write_nodata.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', width=100, height=100, count=2, 
+            dtype=a.dtype, nodata=0) as s:
+        s.write_band(1, a)
+        s.write_band(2, a)
+    info = subprocess.check_output(["gdalinfo", "-stats", name]).decode('utf-8')
+    assert "NoData Value=0" in info
+
+def test_write_lzw(tmpdir):
+    name = str(tmpdir.join("test_write_lzw.tif"))
+    a = numpy.ones((100, 100), dtype=rasterio.ubyte) * 127
+    with rasterio.open(
+            name, 'w', 
+            driver='GTiff', 
+            width=100, height=100, count=1, 
+            dtype=a.dtype,
+            compress='LZW') as s:
+        assert ('compress', 'LZW') in s.kwds.items()
+        s.write_band(1, a)
+    info = subprocess.check_output(["gdalinfo", name]).decode('utf-8')
+    assert "LZW" in info
+
+def test_write_noncontiguous(tmpdir):
+    name = str(tmpdir.join("test_write_nodata.tif"))
+    ROWS = 4
+    COLS = 10
+    BANDS = 6
+    with rasterio.drivers():
+        # Create a 3-D random int array (rows, columns, bands)
+        total = ROWS * COLS * BANDS
+        arr = numpy.random.randint(
+            0, 10, size=total).reshape(
+                (ROWS, COLS, BANDS), order='F').astype(numpy.int32)
+    kwargs = {
+        'driver': 'GTiff',
+        'width': COLS,
+        'height': ROWS,
+        'count': BANDS,
+        'dtype': rasterio.int32
+    }
+    with rasterio.open(name, 'w', **kwargs) as dst:
+        for i in range(BANDS):
+            dst.write_band(i+1, arr[:,:,i])
+

-- 
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