[python-mapnik] 01/03: Imported Upstream version 0.0~20150619-e477887

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Fri Jun 26 17:33:33 UTC 2015


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

sebastic pushed a commit to branch master
in repository python-mapnik.

commit 5b38c932ba42f34bbb44e81b60317e4218e47b47
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Fri Jun 26 18:52:06 2015 +0200

    Imported Upstream version 0.0~20150619-e477887
---
 .gitignore                                         |   20 +
 .gitmodules                                        |    6 +
 .travis.yml                                        |   63 ++
 AUTHORS.md                                         |    5 +
 CHANGELOG.md                                       |    6 +
 COPYING                                            |  502 +++++++++
 README.md                                          |   52 +
 bootstrap.sh                                       |   70 ++
 build.py                                           |  120 ++
 mapnik/__init__.py                                 | 1073 ++++++++++++++++++
 mapnik/mapnik_settings.py                          |   13 +
 mapnik/printing.py                                 | 1027 +++++++++++++++++
 setup.cfg                                          |    2 +
 setup.py                                           |  226 ++++
 src/boost_std_shared_shim.hpp                      |   49 +
 src/mapnik_color.cpp                               |  130 +++
 src/mapnik_coord.cpp                               |   73 ++
 src/mapnik_datasource.cpp                          |  217 ++++
 src/mapnik_datasource_cache.cpp                    |  104 ++
 src/mapnik_enumeration.hpp                         |   88 ++
 src/mapnik_enumeration_wrapper_converter.hpp       |   45 +
 src/mapnik_envelope.cpp                            |  301 +++++
 src/mapnik_expression.cpp                          |  111 ++
 src/mapnik_feature.cpp                             |  237 ++++
 src/mapnik_featureset.cpp                          |   93 ++
 src/mapnik_font_engine.cpp                         |   60 +
 src/mapnik_fontset.cpp                             |   64 ++
 src/mapnik_gamma_method.cpp                        |   49 +
 src/mapnik_geometry.cpp                            |  290 +++++
 src/mapnik_grid.cpp                                |   95 ++
 src/mapnik_grid_view.cpp                           |   64 ++
 src/mapnik_image.cpp                               |  471 ++++++++
 src/mapnik_image_view.cpp                          |  128 +++
 src/mapnik_label_collision_detector.cpp            |  131 +++
 src/mapnik_layer.cpp                               |  388 +++++++
 src/mapnik_logger.cpp                              |   83 ++
 src/mapnik_map.cpp                                 |  543 +++++++++
 src/mapnik_palette.cpp                             |   70 ++
 src/mapnik_parameters.cpp                          |  246 ++++
 src/mapnik_proj_transform.cpp                      |  154 +++
 src/mapnik_projection.cpp                          |  125 +++
 src/mapnik_python.cpp                              | 1072 ++++++++++++++++++
 src/mapnik_query.cpp                               |  107 ++
 src/mapnik_raster_colorizer.cpp                    |  241 ++++
 src/mapnik_rule.cpp                                |  100 ++
 src/mapnik_scaling_method.cpp                      |   58 +
 src/mapnik_style.cpp                               |  118 ++
 src/mapnik_svg.hpp                                 |   56 +
 src/mapnik_svg_generator_grammar.cpp               |   27 +
 src/mapnik_symbolizer.cpp                          |  422 +++++++
 src/mapnik_text_placement.cpp                      |  587 ++++++++++
 src/mapnik_threads.hpp                             |  109 ++
 src/mapnik_value_converter.hpp                     |   90 ++
 src/mapnik_view_transform.cpp                      |   92 ++
 src/python_grid_utils.cpp                          |  405 +++++++
 src/python_grid_utils.hpp                          |   79 ++
 src/python_optional.hpp                            |  198 ++++
 src/python_to_value.hpp                            |  122 ++
 test/python_tests/__init__.py                      |    0
 .../agg_rasterizer_integer_overflow_test.py        |   71 ++
 test/python_tests/box2d_test.py                    |  176 +++
 test/python_tests/buffer_clear_test.py             |   61 +
 test/python_tests/cairo_test.py                    |  196 ++++
 test/python_tests/color_test.py                    |  115 ++
 test/python_tests/compare_test.py                  |  112 ++
 test/python_tests/compositing_test.py              |  258 +++++
 test/python_tests/copy_test.py                     |   93 ++
 test/python_tests/csv_test.py                      |  604 ++++++++++
 test/python_tests/datasource_test.py               |  168 +++
 test/python_tests/datasource_xml_template_test.py  |   23 +
 test/python_tests/extra_map_props_test.py          |   54 +
 test/python_tests/feature_id_test.py               |   66 ++
 test/python_tests/feature_test.py                  |  110 ++
 test/python_tests/filter_test.py                   |  451 ++++++++
 test/python_tests/fontset_test.py                  |   41 +
 test/python_tests/geojson_plugin_test.py           |  126 +++
 test/python_tests/geometry_io_test.py              |  273 +++++
 test/python_tests/grayscale_test.py                |   13 +
 test/python_tests/image_encoding_speed_test.py     |  124 +++
 test/python_tests/image_filters_test.py            |   68 ++
 test/python_tests/image_test.py                    |  346 ++++++
 test/python_tests/image_tiff_test.py               |  335 ++++++
 test/python_tests/images/actual.png                |  Bin 0 -> 899 bytes
 test/python_tests/images/composited/clear.png      |  Bin 0 -> 334 bytes
 test/python_tests/images/composited/color.png      |  Bin 0 -> 13905 bytes
 test/python_tests/images/composited/color_burn.png |  Bin 0 -> 14804 bytes
 .../python_tests/images/composited/color_dodge.png |  Bin 0 -> 14898 bytes
 test/python_tests/images/composited/contrast.png   |  Bin 0 -> 10630 bytes
 test/python_tests/images/composited/darken.png     |  Bin 0 -> 14551 bytes
 test/python_tests/images/composited/difference.png |  Bin 0 -> 14926 bytes
 test/python_tests/images/composited/divide.png     |  Bin 0 -> 10492 bytes
 test/python_tests/images/composited/dst.png        |  Bin 0 -> 7521 bytes
 test/python_tests/images/composited/dst_atop.png   |  Bin 0 -> 11764 bytes
 test/python_tests/images/composited/dst_in.png     |  Bin 0 -> 7563 bytes
 test/python_tests/images/composited/dst_out.png    |  Bin 0 -> 9501 bytes
 test/python_tests/images/composited/dst_over.png   |  Bin 0 -> 14402 bytes
 test/python_tests/images/composited/exclusion.png  |  Bin 0 -> 14219 bytes
 .../images/composited/grain_extract.png            |  Bin 0 -> 9149 bytes
 .../python_tests/images/composited/grain_merge.png |  Bin 0 -> 13368 bytes
 test/python_tests/images/composited/hard_light.png |  Bin 0 -> 15018 bytes
 test/python_tests/images/composited/hue.png        |  Bin 0 -> 13240 bytes
 test/python_tests/images/composited/invert.png     |  Bin 0 -> 14130 bytes
 test/python_tests/images/composited/invert_rgb.png |  Bin 0 -> 13952 bytes
 test/python_tests/images/composited/lighten.png    |  Bin 0 -> 14758 bytes
 .../python_tests/images/composited/linear_burn.png |  Bin 0 -> 10261 bytes
 .../images/composited/linear_dodge.png             |  Bin 0 -> 14279 bytes
 test/python_tests/images/composited/minus.png      |  Bin 0 -> 12486 bytes
 test/python_tests/images/composited/multiply.png   |  Bin 0 -> 14948 bytes
 test/python_tests/images/composited/overlay.png    |  Bin 0 -> 15167 bytes
 test/python_tests/images/composited/plus.png       |  Bin 0 -> 13667 bytes
 test/python_tests/images/composited/saturation.png |  Bin 0 -> 13561 bytes
 test/python_tests/images/composited/screen.png     |  Bin 0 -> 14839 bytes
 test/python_tests/images/composited/soft_light.png |  Bin 0 -> 15000 bytes
 test/python_tests/images/composited/src.png        |  Bin 0 -> 8085 bytes
 test/python_tests/images/composited/src_atop.png   |  Bin 0 -> 11651 bytes
 test/python_tests/images/composited/src_in.png     |  Bin 0 -> 8163 bytes
 test/python_tests/images/composited/src_out.png    |  Bin 0 -> 10273 bytes
 test/python_tests/images/composited/src_over.png   |  Bin 0 -> 14368 bytes
 test/python_tests/images/composited/value.png      |  Bin 0 -> 13720 bytes
 test/python_tests/images/composited/xor.png        |  Bin 0 -> 14733 bytes
 test/python_tests/images/expected.png              |  Bin 0 -> 1263 bytes
 .../pycairo/cairo-cairo-expected-reduced.png       |  Bin 0 -> 2117 bytes
 .../images/pycairo/cairo-cairo-expected.pdf        |  Bin 0 -> 4340 bytes
 .../images/pycairo/cairo-cairo-expected.png        |  Bin 0 -> 3624 bytes
 .../images/pycairo/cairo-cairo-expected.svg        |   47 +
 .../pycairo/cairo-surface-expected.building.pdf    |  Bin 0 -> 7467 bytes
 .../pycairo/cairo-surface-expected.building.svg    |  261 +++++
 .../pycairo/cairo-surface-expected.point.pdf       |  Bin 0 -> 29928 bytes
 .../pycairo/cairo-surface-expected.point.svg       |  413 +++++++
 .../pycairo/cairo-surface-expected.polygon.pdf     |  Bin 0 -> 5622 bytes
 .../pycairo/cairo-surface-expected.polygon.svg     |   35 +
 test/python_tests/images/style-comp-op/clear.png   |  Bin 0 -> 334 bytes
 test/python_tests/images/style-comp-op/color.png   |  Bin 0 -> 13762 bytes
 .../images/style-comp-op/color_burn.png            |  Bin 0 -> 14900 bytes
 .../images/style-comp-op/color_dodge.png           |  Bin 0 -> 14503 bytes
 .../python_tests/images/style-comp-op/contrast.png |  Bin 0 -> 14532 bytes
 test/python_tests/images/style-comp-op/darken.png  |  Bin 0 -> 14321 bytes
 .../images/style-comp-op/difference.png            |  Bin 0 -> 14680 bytes
 test/python_tests/images/style-comp-op/divide.png  |  Bin 0 -> 3626 bytes
 test/python_tests/images/style-comp-op/dst.png     |  Bin 0 -> 11358 bytes
 .../python_tests/images/style-comp-op/dst_atop.png |  Bin 0 -> 7660 bytes
 test/python_tests/images/style-comp-op/dst_in.png  |  Bin 0 -> 7660 bytes
 test/python_tests/images/style-comp-op/dst_out.png |  Bin 0 -> 14889 bytes
 .../python_tests/images/style-comp-op/dst_over.png |  Bin 0 -> 11358 bytes
 .../images/style-comp-op/exclusion.png             |  Bin 0 -> 14354 bytes
 .../images/style-comp-op/grain_extract.png         |  Bin 0 -> 7252 bytes
 .../images/style-comp-op/grain_merge.png           |  Bin 0 -> 14206 bytes
 .../images/style-comp-op/hard_light.png            |  Bin 0 -> 14839 bytes
 test/python_tests/images/style-comp-op/hue.png     |  Bin 0 -> 12920 bytes
 test/python_tests/images/style-comp-op/invert.png  |  Bin 0 -> 13521 bytes
 test/python_tests/images/style-comp-op/lighten.png |  Bin 0 -> 12953 bytes
 .../images/style-comp-op/linear_burn.png           |  Bin 0 -> 2362 bytes
 .../images/style-comp-op/linear_dodge.png          |  Bin 0 -> 14392 bytes
 test/python_tests/images/style-comp-op/minus.png   |  Bin 0 -> 14359 bytes
 .../python_tests/images/style-comp-op/multiply.png |  Bin 0 -> 14663 bytes
 test/python_tests/images/style-comp-op/overlay.png |  Bin 0 -> 14378 bytes
 test/python_tests/images/style-comp-op/plus.png    |  Bin 0 -> 14392 bytes
 .../images/style-comp-op/saturation.png            |  Bin 0 -> 13640 bytes
 test/python_tests/images/style-comp-op/screen.png  |  Bin 0 -> 14240 bytes
 .../images/style-comp-op/soft_light.png            |  Bin 0 -> 14104 bytes
 test/python_tests/images/style-comp-op/src.png     |  Bin 0 -> 4942 bytes
 .../python_tests/images/style-comp-op/src_atop.png |  Bin 0 -> 14778 bytes
 test/python_tests/images/style-comp-op/src_in.png  |  Bin 0 -> 4942 bytes
 test/python_tests/images/style-comp-op/src_out.png |  Bin 0 -> 334 bytes
 .../python_tests/images/style-comp-op/src_over.png |  Bin 0 -> 14767 bytes
 test/python_tests/images/style-comp-op/value.png   |  Bin 0 -> 14450 bytes
 test/python_tests/images/style-comp-op/xor.png     |  Bin 0 -> 14934 bytes
 .../images/style-image-filter/agg-stack-blur22.png |  Bin 0 -> 33700 bytes
 .../images/style-image-filter/blur.png             |  Bin 0 -> 27212 bytes
 .../images/style-image-filter/edge-detect.png      |  Bin 0 -> 22467 bytes
 .../images/style-image-filter/emboss.png           |  Bin 0 -> 24564 bytes
 .../images/style-image-filter/gray.png             |  Bin 0 -> 23594 bytes
 .../images/style-image-filter/invert.png           |  Bin 0 -> 24135 bytes
 .../images/style-image-filter/none.png             |  Bin 0 -> 24072 bytes
 .../images/style-image-filter/sharpen.png          |  Bin 0 -> 22765 bytes
 .../images/style-image-filter/sobel.png            |  Bin 0 -> 23934 bytes
 .../images/style-image-filter/x-gradient.png       |  Bin 0 -> 27052 bytes
 .../images/style-image-filter/y-gradient.png       |  Bin 0 -> 27276 bytes
 test/python_tests/images/support/a.png             |  Bin 0 -> 7543 bytes
 test/python_tests/images/support/b.png             |  Bin 0 -> 8084 bytes
 .../images/support/dataraster_coloring.png         |  Bin 0 -> 2879 bytes
 .../encoding-opts/aerial_rgba-png+e=miniz.png      |  Bin 0 -> 47214 bytes
 .../support/encoding-opts/aerial_rgba-png+t=0.png  |  Bin 0 -> 46310 bytes
 .../support/encoding-opts/aerial_rgba-png.png      |  Bin 0 -> 46310 bytes
 .../encoding-opts/aerial_rgba-png32+e=miniz.png    |  Bin 0 -> 160552 bytes
 .../encoding-opts/aerial_rgba-png32+t=0.png        |  Bin 0 -> 143537 bytes
 .../support/encoding-opts/aerial_rgba-png32.png    |  Bin 0 -> 160268 bytes
 .../encoding-opts/aerial_rgba-png8+e=miniz.png     |  Bin 0 -> 47293 bytes
 .../encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png |  Bin 0 -> 103 bytes
 .../encoding-opts/aerial_rgba-png8+m=h+c=1.png     |  Bin 0 -> 103 bytes
 .../encoding-opts/aerial_rgba-png8+m=h+t=0.png     |  Bin 0 -> 46506 bytes
 .../encoding-opts/aerial_rgba-png8+m=h+t=1.png     |  Bin 0 -> 46506 bytes
 .../encoding-opts/aerial_rgba-png8+m=h+t=2.png     |  Bin 0 -> 46506 bytes
 .../support/encoding-opts/aerial_rgba-png8+m=h.png |  Bin 0 -> 46506 bytes
 .../encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png |  Bin 0 -> 103 bytes
 .../encoding-opts/aerial_rgba-png8+m=o+c=1.png     |  Bin 0 -> 103 bytes
 .../encoding-opts/aerial_rgba-png8+m=o+t=0.png     |  Bin 0 -> 43267 bytes
 .../encoding-opts/aerial_rgba-png8+m=o+t=1.png     |  Bin 0 -> 43267 bytes
 .../encoding-opts/aerial_rgba-png8+m=o+t=2.png     |  Bin 0 -> 43267 bytes
 .../support/encoding-opts/aerial_rgba-png8+m=o.png |  Bin 0 -> 43267 bytes
 .../aerial_rgba-webp+alpha=false.webp              |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+alpha_compression=0.webp      |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+alpha_filtering=2.webp        |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+alpha_quality=50.webp         |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+autofilter=0.webp             |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+filter_sharpness=4.webp       |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+filter_strength=50.webp       |  Bin 0 -> 10544 bytes
 ...erial_rgba-webp+filter_type=1+autofilter=1.webp |  Bin 0 -> 10544 bytes
 .../encoding-opts/aerial_rgba-webp+method=0.webp   |  Bin 0 -> 11778 bytes
 .../encoding-opts/aerial_rgba-webp+method=6.webp   |  Bin 0 -> 10010 bytes
 .../aerial_rgba-webp+partition_limit=50.webp       |  Bin 0 -> 10572 bytes
 .../aerial_rgba-webp+partitions=3.webp             |  Bin 0 -> 10544 bytes
 .../encoding-opts/aerial_rgba-webp+pass=10.webp    |  Bin 0 -> 10526 bytes
 .../aerial_rgba-webp+preprocessing=1.webp          |  Bin 0 -> 10546 bytes
 .../encoding-opts/aerial_rgba-webp+quality=64.webp |  Bin 0 -> 9338 bytes
 .../encoding-opts/aerial_rgba-webp+segments=3.webp |  Bin 0 -> 10528 bytes
 .../aerial_rgba-webp+sns_strength=50.webp          |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+target_PSNR=.5.webp           |  Bin 0 -> 10544 bytes
 .../aerial_rgba-webp+target_size=100.webp          |  Bin 0 -> 10544 bytes
 .../support/encoding-opts/aerial_rgba-webp.webp    |  Bin 0 -> 10544 bytes
 .../support/encoding-opts/blank-png+e=miniz.png    |  Bin 0 -> 103 bytes
 .../images/support/encoding-opts/blank-png+t=0.png |  Bin 0 -> 103 bytes
 .../images/support/encoding-opts/blank-png.png     |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png32+e=miniz.png  |  Bin 0 -> 985 bytes
 .../support/encoding-opts/blank-png32+t=0.png      |  Bin 0 -> 851 bytes
 .../images/support/encoding-opts/blank-png32.png   |  Bin 0 -> 915 bytes
 .../support/encoding-opts/blank-png8+e=miniz.png   |  Bin 0 -> 103 bytes
 .../encoding-opts/blank-png8+m=h+c=1+t=0.png       |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=h+c=1.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=h+t=0.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=h+t=1.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=h+t=2.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=h.png       |  Bin 0 -> 103 bytes
 .../encoding-opts/blank-png8+m=o+c=1+t=0.png       |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=o+c=1.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=o+t=0.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=o+t=1.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=o+t=2.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/blank-png8+m=o.png       |  Bin 0 -> 103 bytes
 .../encoding-opts/blank-webp+alpha=false.webp      |  Bin 0 -> 180 bytes
 .../blank-webp+alpha_compression=0.webp            |  Bin 0 -> 65744 bytes
 .../blank-webp+alpha_filtering=2.webp              |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+alpha_quality=50.webp |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+autofilter=0.webp     |  Bin 0 -> 224 bytes
 .../blank-webp+filter_sharpness=4.webp             |  Bin 0 -> 224 bytes
 .../blank-webp+filter_strength=50.webp             |  Bin 0 -> 224 bytes
 .../blank-webp+filter_type=1+autofilter=1.webp     |  Bin 0 -> 220 bytes
 .../support/encoding-opts/blank-webp+method=0.webp |  Bin 0 -> 282 bytes
 .../support/encoding-opts/blank-webp+method=6.webp |  Bin 0 -> 240 bytes
 .../blank-webp+partition_limit=50.webp             |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+partitions=3.webp     |  Bin 0 -> 224 bytes
 .../support/encoding-opts/blank-webp+pass=10.webp  |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+preprocessing=1.webp  |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+quality=64.webp       |  Bin 0 -> 222 bytes
 .../encoding-opts/blank-webp+segments=3.webp       |  Bin 0 -> 222 bytes
 .../encoding-opts/blank-webp+sns_strength=50.webp  |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+target_PSNR=.5.webp   |  Bin 0 -> 224 bytes
 .../encoding-opts/blank-webp+target_size=100.webp  |  Bin 0 -> 224 bytes
 .../images/support/encoding-opts/blank-webp.webp   |  Bin 0 -> 224 bytes
 .../images/support/encoding-opts/png8-17cols.png   |  Bin 0 -> 192 bytes
 .../images/support/encoding-opts/png8-2px.A.png    |  Bin 0 -> 351 bytes
 .../images/support/encoding-opts/png8-2px.png      |  Bin 0 -> 351 bytes
 .../images/support/encoding-opts/png8-9cols.png    |  Bin 0 -> 171 bytes
 .../support/encoding-opts/solid-png+e=miniz.png    |  Bin 0 -> 116 bytes
 .../images/support/encoding-opts/solid-png+t=0.png |  Bin 0 -> 103 bytes
 .../images/support/encoding-opts/solid-png.png     |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png32+e=miniz.png  |  Bin 0 -> 334 bytes
 .../support/encoding-opts/solid-png32+t=0.png      |  Bin 0 -> 270 bytes
 .../images/support/encoding-opts/solid-png32.png   |  Bin 0 -> 334 bytes
 .../support/encoding-opts/solid-png8+e=miniz.png   |  Bin 0 -> 116 bytes
 .../encoding-opts/solid-png8+m=h+c=1+t=0.png       |  Bin 0 -> 103 bytes
 .../support/encoding-opts/solid-png8+m=h+c=1.png   |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png8+m=h+t=0.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/solid-png8+m=h+t=1.png   |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png8+m=h+t=2.png   |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png8+m=h.png       |  Bin 0 -> 116 bytes
 .../encoding-opts/solid-png8+m=o+c=1+t=0.png       |  Bin 0 -> 103 bytes
 .../support/encoding-opts/solid-png8+m=o+c=1.png   |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png8+m=o+t=0.png   |  Bin 0 -> 103 bytes
 .../support/encoding-opts/solid-png8+m=o+t=1.png   |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png8+m=o+t=2.png   |  Bin 0 -> 116 bytes
 .../support/encoding-opts/solid-png8+m=o.png       |  Bin 0 -> 116 bytes
 .../encoding-opts/solid-webp+alpha=false.webp      |  Bin 0 -> 200 bytes
 .../solid-webp+alpha_compression=0.webp            |  Bin 0 -> 200 bytes
 .../solid-webp+alpha_filtering=2.webp              |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+alpha_quality=50.webp |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+autofilter=0.webp     |  Bin 0 -> 200 bytes
 .../solid-webp+filter_sharpness=4.webp             |  Bin 0 -> 200 bytes
 .../solid-webp+filter_strength=50.webp             |  Bin 0 -> 200 bytes
 .../solid-webp+filter_type=1+autofilter=1.webp     |  Bin 0 -> 196 bytes
 .../support/encoding-opts/solid-webp+method=0.webp |  Bin 0 -> 258 bytes
 .../support/encoding-opts/solid-webp+method=6.webp |  Bin 0 -> 216 bytes
 .../solid-webp+partition_limit=50.webp             |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+partitions=3.webp     |  Bin 0 -> 200 bytes
 .../support/encoding-opts/solid-webp+pass=10.webp  |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+preprocessing=1.webp  |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+quality=64.webp       |  Bin 0 -> 196 bytes
 .../encoding-opts/solid-webp+segments=3.webp       |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+sns_strength=50.webp  |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+target_PSNR=.5.webp   |  Bin 0 -> 200 bytes
 .../encoding-opts/solid-webp+target_size=100.webp  |  Bin 0 -> 200 bytes
 .../images/support/encoding-opts/solid-webp.webp   |  Bin 0 -> 200 bytes
 .../images/support/mapnik-layer-buffer-size.png    |  Bin 0 -> 2461 bytes
 .../support/mapnik-marker-ellipse-render1.png      |  Bin 0 -> 15850 bytes
 .../support/mapnik-marker-ellipse-render2.png      |  Bin 0 -> 13992 bytes
 .../mapnik-merc2merc-reprojection-render1.png      |  Bin 0 -> 44731 bytes
 .../mapnik-merc2merc-reprojection-render2.png      |  Bin 0 -> 44643 bytes
 .../mapnik-merc2wgs84-reprojection-render.png      |  Bin 0 -> 40505 bytes
 .../images/support/mapnik-palette-test.png         |  Bin 0 -> 12129 bytes
 .../support/mapnik-python-circle-render1.png       |  Bin 0 -> 126206 bytes
 .../images/support/mapnik-python-point-render1.png |  Bin 0 -> 4165 bytes
 .../images/support/mapnik-style-level-opacity.png  |  Bin 0 -> 42459 bytes
 .../mapnik-wgs842merc-reprojection-render.png      |  Bin 0 -> 48074 bytes
 .../images/support/marker-in-center-not-placed.png |  Bin 0 -> 116 bytes
 .../images/support/marker-in-center.png            |  Bin 0 -> 250 bytes
 .../marker-text-line-scale-factor-0.005.png        |  Bin 0 -> 1877 bytes
 .../support/marker-text-line-scale-factor-0.1.png  |  Bin 0 -> 3851 bytes
 .../marker-text-line-scale-factor-0.899.png        |  Bin 0 -> 17229 bytes
 .../support/marker-text-line-scale-factor-1.5.png  |  Bin 0 -> 11502 bytes
 .../support/marker-text-line-scale-factor-1.png    |  Bin 0 -> 18310 bytes
 .../support/marker-text-line-scale-factor-10.png   |  Bin 0 -> 8348 bytes
 .../support/marker-text-line-scale-factor-100.png  |  Bin 0 -> 2696 bytes
 .../marker-text-line-scale-factor-1e-05.png        |  Bin 0 -> 1637 bytes
 .../support/marker-text-line-scale-factor-2.png    |  Bin 0 -> 11823 bytes
 .../support/marker-text-line-scale-factor-5.png    |  Bin 0 -> 13987 bytes
 ...data_subquery-data_16bsi_subquery-16BSI-135.png |  Bin 0 -> 87 bytes
 ...data_subquery-data_16bui_subquery-16BUI-126.png |  Bin 0 -> 87 bytes
 .../data_subquery-data_2bui_subquery-2BUI-3.png    |  Bin 0 -> 87 bytes
 .../data_subquery-data_32bf_subquery-32BF-450.png  |  Bin 0 -> 87 bytes
 ...data_subquery-data_32bsi_subquery-32BSI-264.png |  Bin 0 -> 87 bytes
 ...data_subquery-data_32bui_subquery-32BUI-255.png |  Bin 0 -> 87 bytes
 .../data_subquery-data_4bui_subquery-4BUI-15.png   |  Bin 0 -> 87 bytes
 .../data_subquery-data_64bf_subquery-64BF-3072.png |  Bin 0 -> 87 bytes
 .../data_subquery-data_8bsi_subquery-8BSI-69.png   |  Bin 0 -> 87 bytes
 .../data_subquery-data_8bui_subquery-8BUI-63.png   |  Bin 0 -> 87 bytes
 ...subquery-grayscale_16bsi_subquery-16BSI-144.png |  Bin 0 -> 96 bytes
 ...subquery-grayscale_16bui_subquery-16BUI-126.png |  Bin 0 -> 96 bytes
 ...ale_subquery-grayscale_2bui_subquery-2BUI-3.png |  Bin 0 -> 96 bytes
 ...subquery-grayscale_32bsi_subquery-32BSI-129.png |  Bin 0 -> 96 bytes
 ...subquery-grayscale_32bui_subquery-32BUI-255.png |  Bin 0 -> 92 bytes
 ...le_subquery-grayscale_4bui_subquery-4BUI-15.png |  Bin 0 -> 96 bytes
 ...le_subquery-grayscale_8bsi_subquery-8BSI-69.png |  Bin 0 -> 96 bytes
 ...le_subquery-grayscale_8bui_subquery-8BUI-63.png |  Bin 0 -> 95 bytes
 ...ui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png |  Bin 0 -> 124081 bytes
 ...ui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png |  Bin 0 -> 4378 bytes
 ...nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png |  Bin 0 -> 124277 bytes
 ...nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png |  Bin 0 -> 4378 bytes
 ...ui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png |  Bin 0 -> 124277 bytes
 ...ui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png |  Bin 0 -> 4378 bytes
 ..._8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png |  Bin 0 -> 124081 bytes
 ..._8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png |  Bin 0 -> 4378 bytes
 .../rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png      |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png      |  Bin 0 -> 3647 bytes
 .../rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png   |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png   |  Bin 0 -> 3647 bytes
 .../rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png      |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png      |  Bin 0 -> 3637 bytes
 .../rgba_8bui-rgba_8bui C O_2-2-0-box1.png         |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui C O_2-2-0-box2.png         |  Bin 0 -> 3637 bytes
 ...ba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png |  Bin 0 -> 12436 bytes
 ...ba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png |  Bin 0 -> 3650 bytes
 ...8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png |  Bin 0 -> 12436 bytes
 ...8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png |  Bin 0 -> 3650 bytes
 ...ba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png |  Bin 0 -> 12436 bytes
 ...ba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png |  Bin 0 -> 3638 bytes
 .../rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png |  Bin 0 -> 3638 bytes
 .../rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png        |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png        |  Bin 0 -> 3647 bytes
 .../rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png     |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png     |  Bin 0 -> 3647 bytes
 .../rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png        |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png        |  Bin 0 -> 3637 bytes
 .../pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png  |  Bin 0 -> 12436 bytes
 .../pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png  |  Bin 0 -> 3637 bytes
 ...rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png |  Bin 0 -> 12436 bytes
 ...rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png |  Bin 0 -> 3650 bytes
 ...a_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png |  Bin 0 -> 12436 bytes
 ...a_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png |  Bin 0 -> 3650 bytes
 ...rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png |  Bin 0 -> 12436 bytes
 ...rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png |  Bin 0 -> 3638 bytes
 .../rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png   |  Bin 0 -> 12436 bytes
 .../rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png   |  Bin 0 -> 3638 bytes
 ...rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png |  Bin 0 -> 93 bytes
 test/python_tests/images/support/raster-alpha.png  |  Bin 0 -> 4442 bytes
 .../python_tests/images/support/raster_warping.png |  Bin 0 -> 1464 bytes
 .../raster_warping_does_not_overclip_source.png    |  Bin 0 -> 1099 bytes
 test/python_tests/images/support/spacing.png       |  Bin 0 -> 48120 bytes
 .../images/support/transparency/aerial_rgb.png     |  Bin 0 -> 143537 bytes
 .../images/support/transparency/aerial_rgba.png    |  Bin 0 -> 160268 bytes
 .../images/support/transparency/white0.png         |  Bin 0 -> 242 bytes
 .../images/support/transparency/white0.webp        |  Bin 0 -> 738 bytes
 .../images/support/transparency/white1.png         |  Bin 0 -> 257 bytes
 .../images/support/transparency/white2.png         |  Bin 0 -> 258 bytes
 test/python_tests/introspection_test.py            |   61 +
 test/python_tests/json_feature_properties_test.py  |  102 ++
 test/python_tests/layer_buffer_size_test.py        |   35 +
 test/python_tests/layer_modification_test.py       |   75 ++
 test/python_tests/layer_test.py                    |   28 +
 test/python_tests/load_map_test.py                 |   82 ++
 test/python_tests/map_query_test.py                |  104 ++
 test/python_tests/mapnik_logger_test.py            |   18 +
 test/python_tests/mapnik_test_data_test.py         |   60 +
 .../python_tests/markers_complex_rendering_test.py |   43 +
 test/python_tests/memory_datasource_test.py        |   34 +
 test/python_tests/multi_tile_raster_test.py        |   68 ++
 test/python_tests/object_test.py                   |  569 ++++++++++
 test/python_tests/ogr_and_shape_geometries_test.py |   43 +
 test/python_tests/ogr_test.py                      |  157 +++
 test/python_tests/osm_test.py                      |   62 ++
 test/python_tests/palette_test.py                  |   54 +
 test/python_tests/parameters_test.py               |   61 +
 test/python_tests/pgraster_test.py                 |  763 +++++++++++++
 test/python_tests/pickling_test.py                 |   44 +
 test/python_tests/png_encoding_test.py             |  218 ++++
 test/python_tests/pngsuite_test.py                 |   35 +
 test/python_tests/postgis_test.py                  | 1177 ++++++++++++++++++++
 test/python_tests/projection_test.py               |  151 +++
 test/python_tests/python_plugin_test.py            |  160 +++
 test/python_tests/query_test.py                    |   37 +
 test/python_tests/query_tolerance_test.py          |   43 +
 test/python_tests/raster_colorizer_test.py         |   90 ++
 test/python_tests/raster_symbolizer_test.py        |  217 ++++
 test/python_tests/rasterlite_test.py               |   38 +
 test/python_tests/render_grid_test.py              |  356 ++++++
 test/python_tests/render_test.py                   |  241 ++++
 test/python_tests/reprojection_test.py             |   92 ++
 test/python_tests/save_map_test.py                 |   76 ++
 test/python_tests/shapefile_test.py                |  113 ++
 test/python_tests/shapeindex_test.py               |   51 +
 test/python_tests/sqlite_rtree_test.py             |  169 +++
 test/python_tests/sqlite_test.py                   |  501 +++++++++
 test/python_tests/style_test.py                    |   18 +
 test/python_tests/topojson_plugin_test.py          |   91 ++
 test/python_tests/utilities.py                     |  102 ++
 test/python_tests/webp_encoding_test.py            |  164 +++
 test/run_tests.py                                  |   91 ++
 test/visual.py                                     |  331 ++++++
 438 files changed, 23142 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..331398c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+.DS_Store
+*.gcov
+*.gcda
+*.gcno
+*~
+*.o
+*.pyc
+*.os
+*.so
+*.a
+*.swp
+*.dylib
+build/
+dist/
+mapnik/paths.py
+*.egg-info/
+.eggs/
+.mason/
+mason_packages/
+mapnik/plugins
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..cf5011a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "test/data-visual"]
+	path = test/data-visual
+	url = https://github.com/mapnik/test-data-visual.git
+[submodule "test/data"]
+	path = test/data
+	url = https://github.com/mapnik/test-data.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..9e32a79
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,63 @@
+language: cpp
+
+sudo: false
+
+git:
+  submodules: true
+  depth: 10
+
+matrix:
+  include:
+     - os: linux
+       compiler: clang
+     - os: osx
+       compiler: clang
+
+env:
+  global:
+    - secure: "CqhZDPctJcpXGPpmIPK5usD/O+2HYawW3434oDufVS9uG/+C7aHzKzi8cuZ7n/REHqJMzy7gJfp6DiyF2QowpnN1L2W0FSJ9VOgj4JQF2Wsupo6gJkq6/CW2Fa35PhQHsv29bfyqtIq+R5SBVAieBe/Lh2P144RwRliGRopGQ68="
+    - secure: "idk4fdU49i546Zs6Fxha14H05eRJ1G/D6NPRaie8M8o+xySnEqf+TyA9/HU8QH7cFvroSLuHJ1U7TmwnR+sXy4XBlIfHLi4u2MN+l/q014GG7T2E2xYcTauqjB4ldToRsDQwe5Dq0gZCMsHLPspWPjL9twfp+Ds7qgcFhTsct0s="
+
+addons:
+  postgresql: "9.4"
+  apt:
+    sources:
+     - ubuntu-toolchain-r-test
+     - llvm-toolchain-precise-3.5
+    packages:
+     - clang-3.5
+
+before_install:
+ - export COMMIT_MESSAGE=$(git show -s --format=%B $TRAVIS_COMMIT | tr -d '\n')
+ - export MASON_BUILD=true
+ - if [[ $(uname -s) == 'Linux' ]]; then
+     psql -U postgres -c 'create database template_postgis;' -U postgres;
+     psql -U postgres -c 'create extension postgis;' -d template_postgis -U postgres;
+     export CXX="clang++-3.5";
+     export CC="clang++-3.5";
+     export PYTHONPATH=$(pwd)/mason_packages/.link/lib/python2.7/site-packages;
+   else
+     export PYTHONPATH=$(pwd)/mason_packages/.link/lib/python/site-packages;
+   fi;
+ - PYTHONUSERBASE=$(pwd)/mason_packages/.link pip install --user nose
+ - PYTHONUSERBASE=$(pwd)/mason_packages/.link pip install --user wheel
+ - PYTHONUSERBASE=$(pwd)/mason_packages/.link pip install --user twine
+ - python --version
+
+install:
+ - python setup.py install --prefix $(pwd)/mason_packages/.link
+
+before_script:
+ - python test/run_tests.py -q
+
+script:
+ - python test/visual.py -q
+ - if [[ ${COMMIT_MESSAGE} =~ "[publish]" ]]; then
+    python setup.py bdist_wheel;
+    if [[ $(uname -s) == 'Linux' ]]; then
+        export PRE_DISTS='dist/*.whl';
+        rename 's/linux_x86_64/any/;' $PRE_DISTS;
+    fi;
+    export DISTS='dist/*';
+    $(pwd)/mason_packages/.link/bin/twine upload -u $PYPI_USER -p $PYPI_PASSWORD $DISTS ;
+   fi;
diff --git a/AUTHORS.md b/AUTHORS.md
new file mode 100644
index 0000000..794bf76
--- /dev/null
+++ b/AUTHORS.md
@@ -0,0 +1,5 @@
+## Mapnik Python Binding Contributors
+
+* Artem Pavlenko
+* Dane Springmeyer
+* Blake Thompson
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8727fb4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Mapnik Python
+
+# Version 0.1.0
+
+ - Intial python bindings seperate from those of the core mapnik code
+ - For changes previous to this please see the core mapnik changelog
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e5ab03e
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..502d8c5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+
+[![Build Status](https://travis-ci.org/mapnik/python-mapnik.svg)](https://travis-ci.org/mapnik/python-mapnik)
+
+Python bindings for Mapnik.
+
+## Installation
+
+Eventually we hope that many people will simply be able to `pip install mapnik` in order to get prebuilt binaries,
+this currently does not work though. So for now here are the instructions
+
+### Create a virtual environment
+
+It is highly suggested that you [a python virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) when developing
+on mapnik.
+
+### Building from Mason
+
+If you do not have mapnik built from source and simply wish to develop from the latest version in [mapnik master branch](https://github.com/mapnik/mapnik) you can setup your environment with a mason build. In order to trigger a mason build prior to building you must set the `MASON_BUILD` environment variable.
+
+```bash
+export MASON_BUILD=true
+```
+
+After this is done simply follow the directions as per a source build.
+
+### Building from Source
+
+Assuming that you built your own mapnik from source, and you have run `make install`. Set any compiler or linking environment variables as necessary so that your installation of mapnik is found. Next simply run one of the two methods:
+
+```
+python setup.py develop
+```
+
+If you wish to are currently developing on mapnik-python and wish to change the code in place and immediately have python changes reflected in your environment.
+
+```
+python setup.py install
+```
+
+If you wish to just install the package
+
+## Testing
+
+Once you have installed you can test the package by running:
+
+```
+git submodule update --init
+python setup.py test
+```
+
+The test data in `./test/data` and `./test/data-visual` are standalone modules. If you need to update them see https://github.com/mapnik/mapnik/blob/master/docs/contributing.markdown#testing
+
diff --git a/bootstrap.sh b/bootstrap.sh
new file mode 100755
index 0000000..806f3f5
--- /dev/null
+++ b/bootstrap.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+
+function setup_mason() {
+    if [[ ! -d ./.mason ]]; then
+        git clone --depth 1 https://github.com/mapbox/mason.git ./.mason
+    else
+        echo "Updating to latest mason"
+        (cd ./.mason && git pull)
+    fi
+    export MASON_DIR=$(pwd)/.mason
+    export PATH=$(pwd)/.mason:$PATH
+    export CXX=${CXX:-clang++}
+    export CC=${CXX:-clang++}
+}
+
+function install() {
+    MASON_PLATFORM_ID=$(mason env MASON_PLATFORM_ID)
+    if [[ ! -d ./mason_packages/${MASON_PLATFORM_ID}/${1}/ ]]; then
+        mason install $1 $2
+        mason link $1 $2
+    fi
+}
+
+function install_mason_deps() {
+    install mapnik 3.0.0-rc3
+    install protobuf 2.6.1
+    install freetype 2.5.4
+    install harfbuzz 2cd5323
+    install jpeg_turbo 1.4.0
+    install libxml2 2.9.2
+    install libpng 1.6.16
+    install webp 0.4.2
+    install icu 54.1
+    install proj 4.8.0
+    install libtiff 4.0.4beta
+    install boost 1.57.0
+    install boost_libsystem 1.57.0
+    install boost_libthread 1.57.0
+    install boost_libfilesystem 1.57.0
+    install boost_libprogram_options 1.57.0
+    install boost_libpython 1.57.0
+    install boost_libregex 1.57.0
+    install boost_libpython 1.57.0
+    install pixman 0.32.6
+    install cairo 1.12.18
+}
+
+function setup_runtime_settings() {
+    local MASON_LINKED_ABS=$(pwd)/mason_packages/.link
+    export PROJ_LIB=${MASON_LINKED_ABS}/share/proj
+    export ICU_DATA=${MASON_LINKED_ABS}/share/icu/54.1
+    export GDAL_DATA=${MASON_LINKED_ABS}/share/gdal
+    if [[ $(uname -s) == 'Darwin' ]]; then
+        export DYLD_LIBRARY_PATH=$(pwd)/mason_packages/.link/lib:${DYLD_LIBRARY_PATH}
+    else
+        export LD_LIBRARY_PATH=$(pwd)/mason_packages/.link/lib:${LD_LIBRARY_PATH}
+    fi
+    export PATH=$(pwd)/mason_packages/.link/bin:${PATH}
+}
+
+function main() {
+    setup_mason
+    install_mason_deps
+    setup_runtime_settings
+    echo "Ready, now run:"
+    echo ""
+    echo "    make test"
+}
+
+main
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..0f94826
--- /dev/null
+++ b/build.py
@@ -0,0 +1,120 @@
+import glob
+import os
+from subprocess import Popen, PIPE
+from distutils import sysconfig
+
+Import('env')
+
+def call(cmd, silent=True):
+    stdin, stderr = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+    if not stderr:
+        return stdin.strip()
+    elif not silent:
+        print stderr
+
+
+prefix = env['PREFIX']
+target_path = os.path.normpath(sysconfig.get_python_lib() + os.path.sep + env['MAPNIK_NAME'])
+
+py_env = env.Clone()
+
+py_env.Append(CPPPATH = sysconfig.get_python_inc())
+
+py_env.Append(CPPDEFINES = env['LIBMAPNIK_DEFINES'])
+
+py_env['LIBS'] = [env['MAPNIK_NAME'],'libboost_python']
+
+link_all_libs = env['LINKING'] == 'static' or env['RUNTIME_LINK'] == 'static'
+
+# even though boost_thread is no longer used in mapnik core
+# we need to link in for boost_python to avoid missing symbol: _ZN5boost6detail12get_tss_dataEPKv / boost::detail::get_tss_data
+py_env.AppendUnique(LIBS = 'boost_thread%s' % env['BOOST_APPEND'])
+
+if link_all_libs:
+    py_env.AppendUnique(LIBS=env['LIBMAPNIK_LIBS'])
+
+# note: on linux -lrt must be linked after thread to avoid: undefined symbol: clock_gettime
+if env['RUNTIME_LINK'] == 'static' and env['PLATFORM'] == 'Linux':
+    py_env.AppendUnique(LIBS='rt')
+
+# TODO - do solaris/fedora need direct linking too?
+python_link_flag = ''
+if env['PLATFORM'] == 'Darwin':
+    python_link_flag = '-undefined dynamic_lookup'
+
+paths = '''
+"""Configuration paths of Mapnik fonts and input plugins (auto-generated by SCons)."""
+
+from os.path import normpath,join,dirname
+
+mapniklibpath = '%s'
+mapniklibpath = normpath(join(dirname(__file__),mapniklibpath))
+'''
+
+paths += "inputpluginspath = join(mapniklibpath,'input')\n"
+
+if env['SYSTEM_FONTS']:
+    paths += "fontscollectionpath = normpath('%s')\n" % env['SYSTEM_FONTS']
+else:
+    paths += "fontscollectionpath = join(mapniklibpath,'fonts')\n"
+
+paths += "__all__ = [mapniklibpath,inputpluginspath,fontscollectionpath]\n"
+
+if not os.path.exists(env['MAPNIK_NAME']):
+    os.mkdir(env['MAPNIK_NAME'])
+
+file('mapnik/paths.py','w').write(paths % (env['MAPNIK_LIB_DIR']))
+
+# force open perms temporarily so that `sudo scons install`
+# does not later break simple non-install non-sudo rebuild
+try:
+    os.chmod('mapnik/paths.py',0666)
+except: pass
+
+# install the shared object beside the module directory
+sources = glob.glob('src/*.cpp')
+
+if 'install' in COMMAND_LINE_TARGETS:
+    # install the core mapnik python files, including '__init__.py'
+    init_files = glob.glob('mapnik/*.py')
+    if 'mapnik/paths.py' in init_files:
+        init_files.remove('mapnik/paths.py')
+    init_module = env.Install(target_path, init_files)
+    env.Alias(target='install', source=init_module)
+    # fix perms and install the custom generated 'paths.py'
+    targetp = os.path.join(target_path,'paths.py')
+    env.Alias("install", targetp)
+    # use env.Command rather than env.Install
+    # to enable setting proper perms on `paths.py`
+    env.Command( targetp, 'mapnik/paths.py',
+        [
+        Copy("$TARGET","$SOURCE"),
+        Chmod("$TARGET", 0644),
+        ])
+
+if 'uninstall' not in COMMAND_LINE_TARGETS:
+    if env['HAS_CAIRO']:
+        py_env.Append(CPPPATH = env['CAIRO_CPPPATHS'])
+        py_env.Append(CPPDEFINES = '-DHAVE_CAIRO')
+        if link_all_libs:
+            py_env.Append(LIBS=env['CAIRO_ALL_LIBS'])
+
+    if env['HAS_PYCAIRO']:
+        py_env.Append(CPPDEFINES = '-DHAVE_PYCAIRO')
+        py_env.Append(CPPPATH = env['PYCAIRO_PATHS'])
+
+py_env.Append(LINKFLAGS=python_link_flag)
+py_env.AppendUnique(LIBS='mapnik-json')
+py_env.AppendUnique(LIBS='mapnik-wkt')
+
+_mapnik = py_env.LoadableModule('mapnik/_mapnik', sources, LDMODULEPREFIX='', LDMODULESUFFIX='.so')
+
+Depends(_mapnik, env.subst('../../src/%s' % env['MAPNIK_LIB_NAME']))
+Depends(_mapnik, env.subst('../../src/json/libmapnik-json${LIBSUFFIX}'))
+Depends(_mapnik, env.subst('../../src/wkt/libmapnik-wkt${LIBSUFFIX}'))
+
+if 'uninstall' not in COMMAND_LINE_TARGETS:
+    pymapniklib = env.Install(target_path,_mapnik)
+    py_env.Alias(target='install',source=pymapniklib)
+
+env['create_uninstall_target'](env, target_path)
diff --git a/mapnik/__init__.py b/mapnik/__init__.py
new file mode 100644
index 0000000..3eef555
--- /dev/null
+++ b/mapnik/__init__.py
@@ -0,0 +1,1073 @@
+#
+# This file is part of Mapnik (C++/Python mapping toolkit)
+# Copyright (C) 2014 Artem Pavlenko
+#
+# Mapnik is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+
+"""Mapnik Python module.
+
+Boost Python bindings to the Mapnik C++ shared library.
+
+Several things happen when you do:
+
+    >>> import mapnik
+
+ 1) Mapnik C++ objects are imported via the '__init__.py' from the '_mapnik.so' shared object
+    (_mapnik.pyd on win) which references libmapnik.so (linux), libmapnik.dylib (mac), or
+    mapnik.dll (win32).
+
+ 2) The paths to the input plugins and font directories are imported from the 'paths.py'
+    file which was constructed and installed during SCons installation.
+
+ 3) All available input plugins and TrueType fonts are automatically registered.
+
+ 4) Boost Python metaclass injectors are used in the '__init__.py' to extend several
+    objects adding extra convenience when accessed via Python.
+
+"""
+
+import itertools
+import os
+import warnings
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+def bootstrap_env():
+    """
+    If an optional settings file exists, inherit its
+    environment settings before loading the mapnik library.
+
+    This feature is intended for customized packages of mapnik.
+
+    The settings file should be a python file with an 'env' variable
+    that declares a dictionary of key:value pairs to push into the
+    global process environment, if not already set, like:
+
+        env = {'ICU_DATA':'/usr/local/share/icu/'}
+    """
+    if os.path.exists(os.path.join(os.path.dirname(__file__),'mapnik_settings.py')):
+        from mapnik_settings import env
+        process_keys = os.environ.keys()
+        for key, value in env.items():
+            if key not in process_keys:
+                os.environ[key] = value
+
+bootstrap_env()
+
+from _mapnik import *
+
+import printing
+printing.renderer = render
+
+# The base Boost.Python class
+BoostPythonMetaclass = Coord.__class__
+
+class _MapnikMetaclass(BoostPythonMetaclass):
+    def __init__(self, name, bases, dict):
+        for b in bases:
+            if type(b) not in (self, type):
+                for k,v in list(dict.items()):
+                    if hasattr(b, k):
+                        setattr(b, '_c_'+k, getattr(b, k))
+                    setattr(b,k,v)
+        return type.__init__(self, name, bases, dict)
+
+# metaclass injector compatible with both python 2 and 3
+# http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/
+_injector = _MapnikMetaclass('_injector', (object, ), {})
+
+def Filter(*args,**kwargs):
+    warnings.warn("'Filter' is deprecated and will be removed in Mapnik 3.x, use 'Expression' instead",
+    DeprecationWarning, 2)
+    return Expression(*args, **kwargs)
+
+class Envelope(Box2d):
+    def __init__(self, *args, **kwargs):
+        warnings.warn("'Envelope' is deprecated and will be removed in Mapnik 3.x, use 'Box2d' instead",
+        DeprecationWarning, 2)
+        Box2d.__init__(self, *args, **kwargs)
+
+class _Coord(Coord,_injector):
+    """
+    Represents a point with two coordinates (either lon/lat or x/y).
+
+    Following operators are defined for Coord:
+
+    Addition and subtraction of Coord objects:
+
+    >>> Coord(10, 10) + Coord(20, 20)
+    Coord(30.0, 30.0)
+    >>> Coord(10, 10) - Coord(20, 20)
+    Coord(-10.0, -10.0)
+
+    Addition, subtraction, multiplication and division between
+    a Coord and a float:
+
+    >>> Coord(10, 10) + 1
+    Coord(11.0, 11.0)
+    >>> Coord(10, 10) - 1
+    Coord(-9.0, -9.0)
+    >>> Coord(10, 10) * 2
+    Coord(20.0, 20.0)
+    >>> Coord(10, 10) / 2
+    Coord(5.0, 5.0)
+
+    Equality of coords (as pairwise equality of components):
+    >>> Coord(10, 10) is Coord(10, 10)
+    False
+    >>> Coord(10, 10) == Coord(10, 10)
+    True
+    """
+    def __repr__(self):
+        return 'Coord(%s,%s)' % (self.x, self.y)
+
+    def forward(self, projection):
+        """
+        Projects the point from the geographic coordinate
+        space  into the cartesian space. The x component is
+        considered to be longitude, the y component the
+        latitude.
+
+        Returns the easting (x) and northing (y) as a
+        coordinate pair.
+
+        Example: Project the geographic coordinates of the
+                 city center of Stuttgart into the local
+                 map projection (GK Zone 3/DHDN, EPSG 31467)
+        >>> p = Projection('+init=epsg:31467')
+        >>> Coord(9.1, 48.7).forward(p)
+        Coord(3507360.12813,5395719.2749)
+        """
+        return forward_(self, projection)
+
+    def inverse(self, projection):
+        """
+        Projects the point from the cartesian space
+        into the geographic space. The x component is
+        considered to be the easting, the y component
+        to be the northing.
+
+        Returns the longitude (x) and latitude (y) as a
+        coordinate pair.
+
+        Example: Project the cartesian coordinates of the
+                 city center of Stuttgart in the local
+                 map projection (GK Zone 3/DHDN, EPSG 31467)
+                 into geographic coordinates:
+        >>> p = Projection('+init=epsg:31467')
+        >>> Coord(3507360.12813,5395719.2749).inverse(p)
+        Coord(9.1, 48.7)
+        """
+        return inverse_(self, projection)
+
+class _Box2d(Box2d,_injector):
+    """
+    Represents a spatial envelope (i.e. bounding box).
+
+
+    Following operators are defined for Box2d:
+
+    Addition:
+    e1 + e2 is equvalent to e1.expand_to_include(e2) but yields
+    a new envelope instead of modifying e1
+
+    Subtraction:
+    Currently e1 - e2 returns e1.
+
+    Multiplication and division with floats:
+    Multiplication and division change the width and height of the envelope
+    by the given factor without modifying its center..
+
+    That is, e1 * x is equivalent to:
+           e1.width(x * e1.width())
+           e1.height(x * e1.height()),
+    except that a new envelope is created instead of modifying e1.
+
+    e1 / x is equivalent to e1 * (1.0/x).
+
+    Equality: two envelopes are equal if their corner points are equal.
+    """
+
+    def __repr__(self):
+        return 'Box2d(%s,%s,%s,%s)' % \
+            (self.minx,self.miny,self.maxx,self.maxy)
+
+    def forward(self, projection):
+        """
+        Projects the envelope from the geographic space
+        into the cartesian space by projecting its corner
+        points.
+
+        See also:
+           Coord.forward(self, projection)
+        """
+        return forward_(self, projection)
+
+    def inverse(self, projection):
+        """
+        Projects the envelope from the cartesian space
+        into the geographic space by projecting its corner
+        points.
+
+        See also:
+          Coord.inverse(self, projection).
+        """
+        return inverse_(self, projection)
+
+class _Projection(Projection,_injector):
+
+    def __repr__(self):
+        return "Projection('%s')" % self.params()
+
+    def forward(self,obj):
+        """
+        Projects the given object (Box2d or Coord)
+        from the geographic space into the cartesian space.
+
+        See also:
+          Box2d.forward(self, projection),
+          Coord.forward(self, projection).
+        """
+        return forward_(obj,self)
+
+    def inverse(self,obj):
+        """
+        Projects the given object (Box2d or Coord)
+        from the cartesian space into the geographic space.
+
+        See also:
+          Box2d.inverse(self, projection),
+          Coord.inverse(self, projection).
+        """
+        return inverse_(obj,self)
+
+class _Feature(Feature,_injector):
+    __geo_interface__ = property(lambda self: json.loads(self.to_geojson()))
+
+class _Geometry(Geometry,_injector):
+    __geo_interface__ = property(lambda self: json.loads(self.to_geojson()))
+
+class _Datasource(Datasource,_injector):
+
+    def all_features(self,fields=None,variables={}):
+        query = Query(self.envelope())
+        query.set_variables(variables);
+        attributes = fields or self.fields()
+        for fld in attributes:
+            query.add_property_name(fld)
+        return self.features(query).features
+
+    def featureset(self,fields=None,variables={}):
+        query = Query(self.envelope())
+        query.set_variables(variables);
+        attributes = fields or self.fields()
+        for fld in attributes:
+            query.add_property_name(fld)
+        return self.features(query)
+
+class _Color(Color,_injector):
+    def __repr__(self):
+        return "Color(R=%d,G=%d,B=%d,A=%d)" % (self.r,self.g,self.b,self.a)
+
+class _SymbolizerBase(SymbolizerBase,_injector):
+     # back compatibility
+     @property
+     def filename(self):
+         return self['file']
+
+     @filename.setter
+     def filename(self, val):
+         self['file'] = val
+
+def _add_symbol_method_to_symbolizers(vars=globals()):
+
+    def symbol_for_subcls(self):
+        return self
+
+    def symbol_for_cls(self):
+        return getattr(self,self.type())()
+
+    for name, obj in vars.items():
+        if name.endswith('Symbolizer') and not name.startswith('_'):
+            if name == 'Symbolizer':
+                symbol = symbol_for_cls
+            else:
+                symbol = symbol_for_subcls
+            type('dummy', (obj,_injector), {'symbol': symbol})
+_add_symbol_method_to_symbolizers()
+
+def Datasource(**keywords):
+    """Wrapper around CreateDatasource.
+
+    Create a Mapnik Datasource using a dictionary of parameters.
+
+    Keywords must include:
+
+      type='plugin_name' # e.g. type='gdal'
+
+    See the convenience factory methods of each input plugin for
+    details on additional required keyword arguments.
+
+    """
+
+    return CreateDatasource(keywords)
+
+# convenience factory methods
+
+def Shapefile(**keywords):
+    """Create a Shapefile Datasource.
+
+    Required keyword arguments:
+      file -- path to shapefile without extension
+
+    Optional keyword arguments:
+      base -- path prefix (default None)
+      encoding -- file encoding (default 'utf-8')
+
+    >>> from mapnik import Shapefile, Layer
+    >>> shp = Shapefile(base='/home/mapnik/data',file='world_borders')
+    >>> lyr = Layer('Shapefile Layer')
+    >>> lyr.datasource = shp
+
+    """
+    keywords['type'] = 'shape'
+    return CreateDatasource(keywords)
+
+def CSV(**keywords):
+    """Create a CSV Datasource.
+
+    Required keyword arguments:
+      file -- path to csv
+
+    Optional keyword arguments:
+      inline -- inline CSV string (if provided 'file' argument will be ignored and non-needed)
+      base -- path prefix (default None)
+      encoding -- file encoding (default 'utf-8')
+      row_limit -- integer limit of rows to return (default: 0)
+      strict -- throw an error if an invalid row is encountered
+      escape -- The escape character to use for parsing data
+      quote -- The quote character to use for parsing data
+      separator -- The separator character to use for parsing data
+      headers -- A comma separated list of header names that can be set to add headers to data that lacks them
+      filesize_max -- The maximum filesize in MB that will be accepted
+
+    >>> from mapnik import CSV
+    >>> csv = CSV(file='test.csv')
+
+    >>> from mapnik import CSV
+    >>> csv = CSV(inline='''wkt,Name\n"POINT (120.15 48.47)","Winthrop, WA"''')
+
+    For more information see https://github.com/mapnik/mapnik/wiki/CSV-Plugin
+
+    """
+    keywords['type'] = 'csv'
+    return CreateDatasource(keywords)
+
+def GeoJSON(**keywords):
+    """Create a GeoJSON Datasource.
+
+    Required keyword arguments:
+      file -- path to json
+
+    Optional keyword arguments:
+      encoding -- file encoding (default 'utf-8')
+      base -- path prefix (default None)
+
+    >>> from mapnik import GeoJSON
+    >>> geojson = GeoJSON(file='test.json')
+
+    """
+    keywords['type'] = 'geojson'
+    return CreateDatasource(keywords)
+
+def PostGIS(**keywords):
+    """Create a PostGIS Datasource.
+
+    Required keyword arguments:
+      dbname -- database name to connect to
+      table -- table name or subselect query
+
+      *Note: if using subselects for the 'table' value consider also
+       passing the 'geometry_field' and 'srid' and 'extent_from_subquery'
+       options and/or specifying the 'geometry_table' option.
+
+    Optional db connection keyword arguments:
+      user -- database user to connect as (default: see postgres docs)
+      password -- password for database user (default: see postgres docs)
+      host -- portgres hostname (default: see postgres docs)
+      port -- postgres port (default: see postgres docs)
+      initial_size -- integer size of connection pool (default: 1)
+      max_size -- integer max of connection pool (default: 10)
+      persist_connection -- keep connection open (default: True)
+
+    Optional table-level keyword arguments:
+      extent -- manually specified data extent (comma delimited string, default: None)
+      estimate_extent -- boolean, direct PostGIS to use the faster, less accurate `estimate_extent` over `extent` (default: False)
+      extent_from_subquery -- boolean, direct Mapnik to query Postgis for the extent of the raw 'table' value (default: uses 'geometry_table')
+      geometry_table -- specify geometry table to use to look up metadata (default: automatically parsed from 'table' value)
+      geometry_field -- specify geometry field to use (default: first entry in geometry_columns)
+      srid -- specify srid to use (default: auto-detected from geometry_field)
+      row_limit -- integer limit of rows to return (default: 0)
+      cursor_size -- integer size of binary cursor to use (default: 0, no binary cursor is used)
+
+    >>> from mapnik import PostGIS, Layer
+    >>> params = dict(dbname=env['MAPNIK_NAME'],table='osm',user='postgres',password='gis')
+    >>> params['estimate_extent'] = False
+    >>> params['extent'] = '-20037508,-19929239,20037508,19929239'
+    >>> postgis = PostGIS(**params)
+    >>> lyr = Layer('PostGIS Layer')
+    >>> lyr.datasource = postgis
+
+    """
+    keywords['type'] = 'postgis'
+    return CreateDatasource(keywords)
+
+def PgRaster(**keywords):
+    """Create a PgRaster Datasource.
+
+    Required keyword arguments:
+      dbname -- database name to connect to
+      table -- table name or subselect query
+
+      *Note: if using subselects for the 'table' value consider also
+       passing the 'raster_field' and 'srid' and 'extent_from_subquery'
+       options and/or specifying the 'raster_table' option.
+
+    Optional db connection keyword arguments:
+      user -- database user to connect as (default: see postgres docs)
+      password -- password for database user (default: see postgres docs)
+      host -- portgres hostname (default: see postgres docs)
+      port -- postgres port (default: see postgres docs)
+      initial_size -- integer size of connection pool (default: 1)
+      max_size -- integer max of connection pool (default: 10)
+      persist_connection -- keep connection open (default: True)
+
+    Optional table-level keyword arguments:
+      extent -- manually specified data extent (comma delimited string, default: None)
+      estimate_extent -- boolean, direct PostGIS to use the faster, less accurate `estimate_extent` over `extent` (default: False)
+      extent_from_subquery -- boolean, direct Mapnik to query Postgis for the extent of the raw 'table' value (default: uses 'geometry_table')
+      raster_table -- specify geometry table to use to look up metadata (default: automatically parsed from 'table' value)
+      raster_field -- specify geometry field to use (default: first entry in raster_columns)
+      srid -- specify srid to use (default: auto-detected from geometry_field)
+      row_limit -- integer limit of rows to return (default: 0)
+      cursor_size -- integer size of binary cursor to use (default: 0, no binary cursor is used)
+      use_overviews -- boolean, use overviews when available (default: false)
+      prescale_rasters -- boolean, scale rasters on the db side (default: false)
+      clip_rasters -- boolean, clip rasters on the db side (default: false)
+      band -- integer, if non-zero interprets the given band (1-based offset) as a data raster (default: 0)
+
+    >>> from mapnik import PgRaster, Layer
+    >>> params = dict(dbname='mapnik',table='osm',user='postgres',password='gis')
+    >>> params['estimate_extent'] = False
+    >>> params['extent'] = '-20037508,-19929239,20037508,19929239'
+    >>> pgraster = PgRaster(**params)
+    >>> lyr = Layer('PgRaster Layer')
+    >>> lyr.datasource = pgraster
+
+    """
+    keywords['type'] = 'pgraster'
+    return CreateDatasource(keywords)
+
+def Raster(**keywords):
+    """Create a Raster (Tiff) Datasource.
+
+    Required keyword arguments:
+      file -- path to stripped or tiled tiff
+      lox -- lowest (min) x/longitude of tiff extent
+      loy -- lowest (min) y/latitude of tiff extent
+      hix -- highest (max) x/longitude of tiff extent
+      hiy -- highest (max) y/latitude of tiff extent
+
+    Hint: lox,loy,hix,hiy make a Mapnik Box2d
+
+    Optional keyword arguments:
+      base -- path prefix (default None)
+      multi -- whether the image is in tiles on disk (default False)
+
+    Multi-tiled keyword arguments:
+      x_width -- virtual image number of tiles in X direction (required)
+      y_width -- virtual image number of tiles in Y direction (required)
+      tile_size -- if an image is in tiles, how large are the tiles (default 256)
+      tile_stride -- if an image is in tiles, what's the increment between rows/cols (default 1)
+
+    >>> from mapnik import Raster, Layer
+    >>> raster = Raster(base='/home/mapnik/data',file='elevation.tif',lox=-122.8,loy=48.5,hix=-122.7,hiy=48.6)
+    >>> lyr = Layer('Tiff Layer')
+    >>> lyr.datasource = raster
+
+    """
+    keywords['type'] = 'raster'
+    return CreateDatasource(keywords)
+
+def Gdal(**keywords):
+    """Create a GDAL Raster Datasource.
+
+    Required keyword arguments:
+      file -- path to GDAL supported dataset
+
+    Optional keyword arguments:
+      base -- path prefix (default None)
+      shared -- boolean, open GdalDataset in shared mode (default: False)
+      bbox -- tuple (minx, miny, maxx, maxy). If specified, overrides the bbox detected by GDAL.
+
+    >>> from mapnik import Gdal, Layer
+    >>> dataset = Gdal(base='/home/mapnik/data',file='elevation.tif')
+    >>> lyr = Layer('GDAL Layer from TIFF file')
+    >>> lyr.datasource = dataset
+
+    """
+    keywords['type'] = 'gdal'
+    if 'bbox' in keywords:
+        if isinstance(keywords['bbox'], (tuple, list)):
+            keywords['bbox'] = ','.join([str(item) for item in keywords['bbox']])
+    return CreateDatasource(keywords)
+
+def Occi(**keywords):
+    """Create a Oracle Spatial (10g) Vector Datasource.
+
+    Required keyword arguments:
+      user -- database user to connect as
+      password -- password for database user
+      host -- oracle host to connect to (does not refer to SID in tsnames.ora)
+      table -- table name or subselect query
+
+    Optional keyword arguments:
+      initial_size -- integer size of connection pool (default 1)
+      max_size -- integer max of connection pool (default 10)
+      extent -- manually specified data extent (comma delimited string, default None)
+      estimate_extent -- boolean, direct Oracle to use the faster, less accurate estimate_extent() over extent() (default False)
+      encoding -- file encoding (default 'utf-8')
+      geometry_field -- specify geometry field (default 'GEOLOC')
+      use_spatial_index -- boolean, force the use of the spatial index (default True)
+
+    >>> from mapnik import Occi, Layer
+    >>> params = dict(host='myoracle',user='scott',password='tiger',table='test')
+    >>> params['estimate_extent'] = False
+    >>> params['extent'] = '-20037508,-19929239,20037508,19929239'
+    >>> oracle = Occi(**params)
+    >>> lyr = Layer('Oracle Spatial Layer')
+    >>> lyr.datasource = oracle
+    """
+    keywords['type'] = 'occi'
+    return CreateDatasource(keywords)
+
+def Ogr(**keywords):
+    """Create a OGR Vector Datasource.
+
+    Required keyword arguments:
+      file -- path to OGR supported dataset
+      layer -- name of layer to use within datasource (optional if layer_by_index or layer_by_sql is used)
+
+    Optional keyword arguments:
+      layer_by_index -- choose layer by index number instead of by layer name or sql.
+      layer_by_sql -- choose layer by sql query number instead of by layer name or index.
+      base -- path prefix (default None)
+      encoding -- file encoding (default 'utf-8')
+
+    >>> from mapnik import Ogr, Layer
+    >>> datasource = Ogr(base='/home/mapnik/data',file='rivers.geojson',layer='OGRGeoJSON')
+    >>> lyr = Layer('OGR Layer from GeoJSON file')
+    >>> lyr.datasource = datasource
+
+    """
+    keywords['type'] = 'ogr'
+    return CreateDatasource(keywords)
+
+def SQLite(**keywords):
+    """Create a SQLite Datasource.
+
+    Required keyword arguments:
+      file -- path to SQLite database file
+      table -- table name or subselect query
+
+    Optional keyword arguments:
+      base -- path prefix (default None)
+      encoding -- file encoding (default 'utf-8')
+      extent -- manually specified data extent (comma delimited string, default None)
+      metadata -- name of auxillary table containing record for table with xmin, ymin, xmax, ymax, and f_table_name
+      geometry_field -- name of geometry field (default 'the_geom')
+      key_field -- name of primary key field (default 'OGC_FID')
+      row_offset -- specify a custom integer row offset (default 0)
+      row_limit -- specify a custom integer row limit (default 0)
+      wkb_format -- specify a wkb type of 'spatialite' (default None)
+      use_spatial_index -- boolean, instruct sqlite plugin to use Rtree spatial index (default True)
+
+    >>> from mapnik import SQLite, Layer
+    >>> sqlite = SQLite(base='/home/mapnik/data',file='osm.db',table='osm',extent='-20037508,-19929239,20037508,19929239')
+    >>> lyr = Layer('SQLite Layer')
+    >>> lyr.datasource = sqlite
+
+    """
+    keywords['type'] = 'sqlite'
+    return CreateDatasource(keywords)
+
+def Rasterlite(**keywords):
+    """Create a Rasterlite Datasource.
+
+    Required keyword arguments:
+      file -- path to Rasterlite database file
+      table -- table name or subselect query
+
+    Optional keyword arguments:
+      base -- path prefix (default None)
+      extent -- manually specified data extent (comma delimited string, default None)
+
+    >>> from mapnik import Rasterlite, Layer
+    >>> rasterlite = Rasterlite(base='/home/mapnik/data',file='osm.db',table='osm',extent='-20037508,-19929239,20037508,19929239')
+    >>> lyr = Layer('Rasterlite Layer')
+    >>> lyr.datasource = rasterlite
+
+    """
+    keywords['type'] = 'rasterlite'
+    return CreateDatasource(keywords)
+
+def Osm(**keywords):
+    """Create a Osm Datasource.
+
+    Required keyword arguments:
+      file -- path to OSM file
+
+    Optional keyword arguments:
+      encoding -- file encoding (default 'utf-8')
+      url -- url to fetch data (default None)
+      bbox -- data bounding box for fetching data (default None)
+
+    >>> from mapnik import Osm, Layer
+    >>> datasource = Osm(file='test.osm')
+    >>> lyr = Layer('Osm Layer')
+    >>> lyr.datasource = datasource
+
+    """
+    # note: parser only supports libxml2 so not exposing option
+    # parser -- xml parser to use (default libxml2)
+    keywords['type'] = 'osm'
+    return CreateDatasource(keywords)
+
+def Python(**keywords):
+    """Create a Python Datasource.
+
+    >>> from mapnik import Python, PythonDatasource
+    >>> datasource = Python('PythonDataSource')
+    >>> lyr = Layer('Python datasource')
+    >>> lyr.datasource = datasource
+    """
+    keywords['type'] = 'python'
+    return CreateDatasource(keywords)
+
+def MemoryDatasource(**keywords):
+    """Create a Memory Datasource.
+
+    Optional keyword arguments:
+        (TODO)
+    """
+    params = Parameters()
+    params.append(Parameter('type','memory'))
+    return MemoryDatasourceBase(params)
+
+class PythonDatasource(object):
+    """A base class for a Python data source.
+
+    Optional arguments:
+      envelope -- a mapnik.Box2d (minx, miny, maxx, maxy) envelope of the data source, default (-180,-90,180,90)
+      geometry_type -- one of the DataGeometryType enumeration values, default Point
+      data_type -- one of the DataType enumerations, default Vector
+    """
+    def __init__(self, envelope=None, geometry_type=None, data_type=None):
+        self.envelope = envelope or Box2d(-180, -90, 180, 90)
+        self.geometry_type = geometry_type or DataGeometryType.Point
+        self.data_type = data_type or DataType.Vector
+
+    def features(self, query):
+        """Return an iterable which yields instances of Feature for features within the passed query.
+
+        Required arguments:
+          query -- a Query instance specifying the region for which features should be returned
+        """
+        return None
+
+    def features_at_point(self, point):
+        """Rarely uses. Return an iterable which yields instances of Feature for the specified point."""
+        return None
+
+    @classmethod
+    def wkb_features(cls, keys, features):
+        """A convenience function to wrap an iterator yielding pairs of WKB format geometry and dictionaries of
+        key-value pairs into mapnik features. Return this from PythonDatasource.features() passing it a sequence of keys
+        to appear in the output and an iterator yielding features.
+
+        For example. One might have a features() method in a derived class like the following:
+
+        def features(self, query):
+            # ... create WKB features feat1 and feat2
+
+            return mapnik.PythonDatasource.wkb_features(
+                keys = ( 'name', 'author' ),
+                features = [
+                    (feat1, { 'name': 'feat1', 'author': 'alice' }),
+                    (feat2, { 'name': 'feat2', 'author': 'bob' }),
+                ]
+            )
+
+        """
+        ctx = Context()
+        [ctx.push(x) for x in keys]
+
+        def make_it(feat, idx):
+            f = Feature(ctx, idx)
+            geom, attrs = feat
+            f.add_geometries_from_wkb(geom)
+            for k, v in attrs.iteritems():
+                f[k] = v
+            return f
+
+        return itertools.imap(make_it, features, itertools.count(1))
+
+    @classmethod
+    def wkt_features(cls, keys, features):
+        """A convenience function to wrap an iterator yielding pairs of WKT format geometry and dictionaries of
+        key-value pairs into mapnik features. Return this from PythonDatasource.features() passing it a sequence of keys
+        to appear in the output and an iterator yielding features.
+
+        For example. One might have a features() method in a derived class like the following:
+
+        def features(self, query):
+            # ... create WKT features feat1 and feat2
+
+            return mapnik.PythonDatasource.wkt_features(
+                keys = ( 'name', 'author' ),
+                features = [
+                    (feat1, { 'name': 'feat1', 'author': 'alice' }),
+                    (feat2, { 'name': 'feat2', 'author': 'bob' }),
+                ]
+            )
+
+        """
+        ctx = Context()
+        [ctx.push(x) for x in keys]
+
+        def make_it(feat, idx):
+            f = Feature(ctx, idx)
+            geom, attrs = feat
+            f.add_geometries_from_wkt(geom)
+            for k, v in attrs.iteritems():
+                f[k] = v
+            return f
+
+        return itertools.imap(make_it, features, itertools.count(1))
+
+class _TextSymbolizer(TextSymbolizer,_injector):
+    @property
+    def name(self):
+        if isinstance(self.properties.format_tree, FormattingText):
+            return self.properties.format_tree.text
+        else:
+            # There is no single expression which could be returned as name
+            raise RuntimeError("TextSymbolizer uses complex formatting features, but old compatibility interface is used to access it. Use self.properties.format_tree instead.")
+
+    @name.setter
+    def name(self, name):
+        self.properties.format_tree = FormattingText(name)
+
+    @property
+    def text_size(self):
+        return self.format.text_size
+
+    @text_size.setter
+    def text_size(self, text_size):
+        self.format.text_size = text_size
+
+    @property
+    def face_name(self):
+        return self.format.face_name
+
+    @face_name.setter
+    def face_name(self, face_name):
+        self.format.face_name = face_name
+
+
+    @property
+    def fontset(self):
+        return self.format.fontset
+
+    @fontset.setter
+    def fontset(self, fontset):
+        self.format.fontset = fontset
+
+
+    @property
+    def character_spacing(self):
+        return self.format.character_spacing
+
+    @character_spacing.setter
+    def character_spacing(self, character_spacing):
+        self.format.character_spacing = character_spacing
+
+
+    @property
+    def line_spacing(self):
+        return self.format.line_spacing
+
+    @line_spacing.setter
+    def line_spacing(self, line_spacing):
+        self.format.line_spacing = line_spacing
+
+
+    @property
+    def text_opacity(self):
+        return self.format.text_opacity
+
+    @text_opacity.setter
+    def text_opacity(self, text_opacity):
+        self.format.text_opacity = text_opacity
+
+
+    @property
+    def wrap_before(self):
+        return self.format.wrap_before
+
+    @wrap_before.setter
+    def wrap_before(self, wrap_before):
+        self.format.wrap_before = wrap_before
+
+
+    @property
+    def text_transform(self):
+        return self.format.text_transform
+
+    @text_transform.setter
+    def text_transform(self, text_transform):
+        self.format.text_transform = text_transform
+
+
+    @property
+    def fill(self):
+        return self.format.fill
+
+    @fill.setter
+    def fill(self, fill):
+        self.format.fill = fill
+
+
+    @property
+    def halo_fill(self):
+        return self.format.halo_fill
+
+    @halo_fill.setter
+    def halo_fill(self, halo_fill):
+        self.format.halo_fill = halo_fill
+
+
+
+    @property
+    def halo_radius(self):
+        return self.format.halo_radius
+
+    @halo_radius.setter
+    def halo_radius(self, halo_radius):
+        self.format.halo_radius = halo_radius
+
+
+    @property
+    def label_placement(self):
+        return self.properties.label_placement
+
+    @label_placement.setter
+    def label_placement(self, label_placement):
+        self.properties.label_placement = label_placement
+
+
+
+    @property
+    def horizontal_alignment(self):
+        return self.properties.horizontal_alignment
+
+    @horizontal_alignment.setter
+    def horizontal_alignment(self, horizontal_alignment):
+        self.properties.horizontal_alignment = horizontal_alignment
+
+
+
+    @property
+    def justify_alignment(self):
+        return self.properties.justify_alignment
+
+    @justify_alignment.setter
+    def justify_alignment(self, justify_alignment):
+        self.properties.justify_alignment = justify_alignment
+
+
+
+    @property
+    def vertical_alignment(self):
+        return self.properties.vertical_alignment
+
+    @vertical_alignment.setter
+    def vertical_alignment(self, vertical_alignment):
+        self.properties.vertical_alignment = vertical_alignment
+
+
+
+    @property
+    def orientation(self):
+        return self.properties.orientation
+
+    @orientation.setter
+    def orientation(self, orientation):
+        self.properties.orientation = orientation
+
+
+
+    @property
+    def displacement(self):
+        return self.properties.displacement
+
+    @displacement.setter
+    def displacement(self, displacement):
+        self.properties.displacement = displacement
+
+
+
+    @property
+    def label_spacing(self):
+        return self.properties.label_spacing
+
+    @label_spacing.setter
+    def label_spacing(self, label_spacing):
+        self.properties.label_spacing = label_spacing
+
+
+
+    @property
+    def label_position_tolerance(self):
+        return self.properties.label_position_tolerance
+
+    @label_position_tolerance.setter
+    def label_position_tolerance(self, label_position_tolerance):
+        self.properties.label_position_tolerance = label_position_tolerance
+
+
+
+    @property
+    def avoid_edges(self):
+        return self.properties.avoid_edges
+
+    @avoid_edges.setter
+    def avoid_edges(self, avoid_edges):
+        self.properties.avoid_edges = avoid_edges
+
+
+
+    @property
+    def minimum_distance(self):
+        return self.properties.minimum_distance
+
+    @minimum_distance.setter
+    def minimum_distance(self, minimum_distance):
+        self.properties.minimum_distance = minimum_distance
+
+
+
+    @property
+    def minimum_padding(self):
+        return self.properties.minimum_padding
+
+    @minimum_padding.setter
+    def minimum_padding(self, minimum_padding):
+        self.properties.minimum_padding = minimum_padding
+
+
+
+    @property
+    def minimum_path_length(self):
+        return self.properties.minimum_path_length
+
+    @minimum_path_length.setter
+    def minimum_path_length(self, minimum_path_length):
+        self.properties.minimum_path_length = minimum_path_length
+
+
+
+    @property
+    def maximum_angle_char_delta(self):
+        return self.properties.maximum_angle_char_delta
+
+    @maximum_angle_char_delta.setter
+    def maximum_angle_char_delta(self, maximum_angle_char_delta):
+        self.properties.maximum_angle_char_delta = maximum_angle_char_delta
+
+
+    @property
+    def allow_overlap(self):
+        return self.properties.allow_overlap
+
+    @allow_overlap.setter
+    def allow_overlap(self, allow_overlap):
+        self.properties.allow_overlap = allow_overlap
+
+
+
+    @property
+    def text_ratio(self):
+        return self.properties.text_ratio
+
+    @text_ratio.setter
+    def text_ratio(self, text_ratio):
+        self.properties.text_ratio = text_ratio
+
+
+
+    @property
+    def wrap_width(self):
+        return self.properties.wrap_width
+
+    @wrap_width.setter
+    def wrap_width(self, wrap_width):
+        self.properties.wrap_width = wrap_width
+
+
+def mapnik_version_from_string(version_string):
+    """Return the Mapnik version from a string."""
+    n = version_string.split('.')
+    return (int(n[0]) * 100000) + (int(n[1]) * 100) + (int(n[2]));
+
+def register_plugins(path=None):
+    """Register plugins located by specified path"""
+    if not path:
+        if os.environ.has_key('MAPNIK_INPUT_PLUGINS_DIRECTORY'):
+            path = os.environ.get('MAPNIK_INPUT_PLUGINS_DIRECTORY')
+        else:
+            from paths import inputpluginspath
+            path = inputpluginspath
+    DatasourceCache.register_datasources(path)
+
+def register_fonts(path=None,valid_extensions=['.ttf','.otf','.ttc','.pfa','.pfb','.ttc','.dfont','.woff']):
+    """Recursively register fonts using path argument as base directory"""
+    if not path:
+       if os.environ.has_key('MAPNIK_FONT_DIRECTORY'):
+           path = os.environ.get('MAPNIK_FONT_DIRECTORY')
+       else:
+           from paths import fontscollectionpath
+           path = fontscollectionpath
+    for dirpath, _, filenames in os.walk(path):
+        for filename in filenames:
+            if os.path.splitext(filename.lower())[1] in valid_extensions:
+                FontEngine.instance().register_font(os.path.join(dirpath, filename))
+
+# auto-register known plugins and fonts
+register_plugins()
+register_fonts()
diff --git a/mapnik/mapnik_settings.py b/mapnik/mapnik_settings.py
new file mode 100644
index 0000000..6c48cea
--- /dev/null
+++ b/mapnik/mapnik_settings.py
@@ -0,0 +1,13 @@
+import os
+mapnik_data_dir = os.path.dirname(os.path.realpath(__file__))
+
+env = {}
+icu_path = os.path.join(mapnik_data_dir, 'plugins', 'icu')
+if os.path.isdir(icu_path):
+    env['ICU_DATA'] = icu_path
+gdal_path = os.path.join(mapnik_data_dir, 'plugins', 'gdal')
+if os.path.isdir(gdal_path):
+    env['GDAL_DATA'] = gdal_path
+proj_path = os.path.join(mapnik_data_dir, 'plugins', 'proj')
+if os.path.isdir(proj_path):
+    env['PROJ_LIB'] = proj_path
diff --git a/mapnik/printing.py b/mapnik/printing.py
new file mode 100644
index 0000000..e61f7c0
--- /dev/null
+++ b/mapnik/printing.py
@@ -0,0 +1,1027 @@
+# -*- coding: utf-8 -*-
+
+"""Mapnik classes to assist in creating printable maps
+
+basic usage is along the lines of
+
+import mapnik
+
+page = mapnik.printing.PDFPrinter()
+m = mapnik.Map(100,100)
+mapnik.load_map(m, "my_xml_map_description", True)
+m.zoom_all()
+page.render_map(m,"my_output_file.pdf")
+
+see the documentation of mapnik.printing.PDFPrinter() for options
+
+"""
+from __future__ import absolute_import
+
+from . import render, Map, Box2d, Layer, Feature, Projection, Coord, Style, Geometry
+import math
+import os
+import tempfile
+
+try:
+    import cairo
+    HAS_PYCAIRO_MODULE = True
+except ImportError:
+    HAS_PYCAIRO_MODULE = False
+
+try:
+    import pangocairo
+    import pango
+    HAS_PANGOCAIRO_MODULE = True
+except ImportError:
+    HAS_PANGOCAIRO_MODULE = False
+
+try:
+    import pyPdf
+    HAS_PYPDF = True
+except ImportError:
+    HAS_PYPDF = False
+
+class centering:
+    """Style of centering to use with the map, the default is constrained
+
+    none: map will be placed flush with the margin/box in the top left corner
+    constrained: map will be centered on the most constrained axis (for a portrait page
+                 and a square map this will be horizontally)
+    unconstrained: map will be centered on the unconstrained axis
+    vertical:
+    horizontal:
+    both:
+    """
+    none=0
+    constrained=1
+    unconstrained=2
+    vertical=3
+    horizontal=4
+    both=5
+
+"""Some predefined page sizes custom sizes can also be passed
+a tuple of the page width and height in meters"""
+pagesizes = {
+    "a0": (0.841000,1.189000),
+    "a0l": (1.189000,0.841000),
+    "b0": (1.000000,1.414000),
+    "b0l": (1.414000,1.000000),
+    "c0": (0.917000,1.297000),
+    "c0l": (1.297000,0.917000),
+    "a1": (0.594000,0.841000),
+    "a1l": (0.841000,0.594000),
+    "b1": (0.707000,1.000000),
+    "b1l": (1.000000,0.707000),
+    "c1": (0.648000,0.917000),
+    "c1l": (0.917000,0.648000),
+    "a2": (0.420000,0.594000),
+    "a2l": (0.594000,0.420000),
+    "b2": (0.500000,0.707000),
+    "b2l": (0.707000,0.500000),
+    "c2": (0.458000,0.648000),
+    "c2l": (0.648000,0.458000),
+    "a3": (0.297000,0.420000),
+    "a3l": (0.420000,0.297000),
+    "b3": (0.353000,0.500000),
+    "b3l": (0.500000,0.353000),
+    "c3": (0.324000,0.458000),
+    "c3l": (0.458000,0.324000),
+    "a4": (0.210000,0.297000),
+    "a4l": (0.297000,0.210000),
+    "b4": (0.250000,0.353000),
+    "b4l": (0.353000,0.250000),
+    "c4": (0.229000,0.324000),
+    "c4l": (0.324000,0.229000),
+    "a5": (0.148000,0.210000),
+    "a5l": (0.210000,0.148000),
+    "b5": (0.176000,0.250000),
+    "b5l": (0.250000,0.176000),
+    "c5": (0.162000,0.229000),
+    "c5l": (0.229000,0.162000),
+    "a6": (0.105000,0.148000),
+    "a6l": (0.148000,0.105000),
+    "b6": (0.125000,0.176000),
+    "b6l": (0.176000,0.125000),
+    "c6": (0.114000,0.162000),
+    "c6l": (0.162000,0.114000),
+    "a7": (0.074000,0.105000),
+    "a7l": (0.105000,0.074000),
+    "b7": (0.088000,0.125000),
+    "b7l": (0.125000,0.088000),
+    "c7": (0.081000,0.114000),
+    "c7l": (0.114000,0.081000),
+    "a8": (0.052000,0.074000),
+    "a8l": (0.074000,0.052000),
+    "b8": (0.062000,0.088000),
+    "b8l": (0.088000,0.062000),
+    "c8": (0.057000,0.081000),
+    "c8l": (0.081000,0.057000),
+    "a9": (0.037000,0.052000),
+    "a9l": (0.052000,0.037000),
+    "b9": (0.044000,0.062000),
+    "b9l": (0.062000,0.044000),
+    "c9": (0.040000,0.057000),
+    "c9l": (0.057000,0.040000),
+    "a10": (0.026000,0.037000),
+    "a10l": (0.037000,0.026000),
+    "b10": (0.031000,0.044000),
+    "b10l": (0.044000,0.031000),
+    "c10": (0.028000,0.040000),
+    "c10l": (0.040000,0.028000),
+    "letter": (0.216,0.279),
+    "letterl": (0.279,0.216),
+    "legal": (0.216,0.356),
+    "legall": (0.356,0.216),
+}
+"""size of a pt in meters"""
+pt_size=0.0254/72.0
+
+def m2pt(x):
+    """convert distance from meters to points"""
+    return x/pt_size
+
+def pt2m(x):
+    """convert distance from points to meters"""
+    return x*pt_size
+
+def m2in(x):
+    """convert distance from meters to inches"""
+    return x/0.0254
+
+def m2px(x,resolution):
+    """convert distance from meters to pixels at the given resolution in DPI/PPI"""
+    return m2in(x)*resolution
+
+class resolutions:
+    """some predefined resolutions in DPI"""
+    dpi72=72
+    dpi150=150
+    dpi300=300
+    dpi600=600
+
+def any_scale(scale):
+    """Scale helper function that allows any scale"""
+    return scale
+
+def sequence_scale(scale,scale_sequence):
+    """Default scale helper, this rounds scale to a 'sensible' value"""
+    factor = math.floor(math.log10(scale))
+    norm = scale/(10**factor)
+
+    for s in scale_sequence:
+        if norm <= s:
+            return s*10**factor
+    return scale_sequence[0]*10**(factor+1)
+
+def default_scale(scale):
+    """Default scale helper, this rounds scale to a 'sensible' value"""
+    return sequence_scale(scale, (1,1.25,1.5,1.75,2,2.5,3,4,5,6,7.5,8,9,10))
+
+def deg_min_sec_scale(scale):
+    for x in (1.0/3600,
+              2.0/3600,
+              5.0/3600,
+              10.0/3600,
+              30.0/3600,
+              1.0/60,
+              2.0/60,
+              5.0/60,
+              10.0/60,
+              30.0/60,
+              1,
+              2,
+              5,
+              10,
+              30,
+              60
+              ):
+        if scale < x:
+            return x
+    else:
+        return x
+
+def format_deg_min_sec(value):
+    deg = math.floor(value)
+    min = math.floor((value-deg)/(1.0/60))
+    sec = int((value - deg*1.0/60)/1.0/3600)
+    return "%d°%d'%d\"" % (deg,min,sec)
+
+def round_grid_generator(first,last,step):
+        val = (math.floor(first / step) + 1) * step
+        yield val
+        while val < last:
+            val += step
+            yield val
+
+
+def convert_pdf_pages_to_layers(filename,output_name=None,layer_names=(),reverse_all_but_last=True):
+    """
+    opens the given multipage PDF and converts each page to be a layer in a single page PDF
+    layer_names should be a sequence of the user visible names of the layers, if not given
+    or if shorter than num pages generic names will be given to the unnamed layers
+
+    if output_name is not provided a temporary file will be used for the conversion which
+    will then be copied back over the source file.
+
+    requires pyPdf >= 1.13 to be available"""
+
+
+    if not HAS_PYPDF:
+        raise Exception("pyPdf Not available")
+
+    infile = file(filename, 'rb')
+    if output_name:
+        outfile = file(output_name, 'wb')
+    else:
+        (outfd,outfilename) = tempfile.mkstemp(dir=os.path.dirname(filename))
+        outfile = os.fdopen(outfd,'wb')
+
+    i = pyPdf.PdfFileReader(infile)
+    o = pyPdf.PdfFileWriter()
+
+    template_page_size = i.pages[0].mediaBox
+    op = o.addBlankPage(width=template_page_size.getWidth(),height=template_page_size.getHeight())
+
+    contentkey = pyPdf.generic.NameObject('/Contents')
+    resourcekey = pyPdf.generic.NameObject('/Resources')
+    propertieskey = pyPdf.generic.NameObject('/Properties')
+    op[contentkey] = pyPdf.generic.ArrayObject()
+    op[resourcekey] = pyPdf.generic.DictionaryObject()
+    properties = pyPdf.generic.DictionaryObject()
+    ocgs = pyPdf.generic.ArrayObject()
+
+    for (i, p) in enumerate(i.pages):
+        # first start an OCG for the layer
+        ocgname = pyPdf.generic.NameObject('/oc%d' % i)
+        ocgstart = pyPdf.generic.DecodedStreamObject()
+        ocgstart._data = "/OC %s BDC\n" % ocgname
+        ocgend = pyPdf.generic.DecodedStreamObject()
+        ocgend._data = "EMC\n"
+        if isinstance(p['/Contents'],pyPdf.generic.ArrayObject):
+            p[pyPdf.generic.NameObject('/Contents')].insert(0,ocgstart)
+            p[pyPdf.generic.NameObject('/Contents')].append(ocgend)
+        else:
+            p[pyPdf.generic.NameObject('/Contents')] = pyPdf.generic.ArrayObject((ocgstart,p['/Contents'],ocgend))
+
+        op.mergePage(p)
+
+        ocg = pyPdf.generic.DictionaryObject()
+        ocg[pyPdf.generic.NameObject('/Type')] = pyPdf.generic.NameObject('/OCG')
+        if len(layer_names) > i:
+            ocg[pyPdf.generic.NameObject('/Name')] = pyPdf.generic.TextStringObject(layer_names[i])
+        else:
+            ocg[pyPdf.generic.NameObject('/Name')] = pyPdf.generic.TextStringObject('Layer %d' % (i+1))
+        indirect_ocg = o._addObject(ocg)
+        properties[ocgname] = indirect_ocg
+        ocgs.append(indirect_ocg)
+
+    op[resourcekey][propertieskey] = o._addObject(properties)
+
+    ocproperties = pyPdf.generic.DictionaryObject()
+    ocproperties[pyPdf.generic.NameObject('/OCGs')] = ocgs
+    defaultview = pyPdf.generic.DictionaryObject()
+    defaultview[pyPdf.generic.NameObject('/Name')] = pyPdf.generic.TextStringObject('Default')
+    defaultview[pyPdf.generic.NameObject('/BaseState ')] = pyPdf.generic.NameObject('/ON ')
+    defaultview[pyPdf.generic.NameObject('/ON')] = ocgs
+    if reverse_all_but_last:
+        defaultview[pyPdf.generic.NameObject('/Order')] = pyPdf.generic.ArrayObject(reversed(ocgs[:-1]))
+        defaultview[pyPdf.generic.NameObject('/Order')].append(ocgs[-1])
+    else:
+        defaultview[pyPdf.generic.NameObject('/Order')] = pyPdf.generic.ArrayObject(reversed(ocgs))
+    defaultview[pyPdf.generic.NameObject('/OFF')] = pyPdf.generic.ArrayObject()
+
+    ocproperties[pyPdf.generic.NameObject('/D')] = o._addObject(defaultview)
+
+    o._root.getObject()[pyPdf.generic.NameObject('/OCProperties')] = o._addObject(ocproperties)
+
+    o.write(outfile)
+
+    outfile.close()
+    infile.close()
+
+    if not output_name:
+        os.rename(outfilename, filename)
+
+class PDFPrinter:
+    """Main class for creating PDF print outs, basically contruct an instance
+    with appropriate options and then call render_map with your mapnik map
+    """
+    def __init__(self,
+                 pagesize=pagesizes["a4"],
+                 margin=0.005,
+                 box=None,
+                 percent_box=None,
+                 scale=default_scale,
+                 resolution=resolutions.dpi72,
+                 preserve_aspect=True,
+                 centering=centering.constrained,
+                 is_latlon=False,
+                 use_ocg_layers=False):
+        """Creates a cairo surface and context to render a PDF with.
+
+        pagesize: tuple of page size in meters, see predefined sizes in pagessizes dict (default a4)
+        margin: page margin in meters (default 0.01)
+        box: box within the page to render the map into (will not render over margin). This should be
+             a Mapnik Box2d object. Default is the full page within the margin
+        percent_box: as per box, but specified as a percent (0->1) of the full page size. If both box
+                     and percent_box are specified percent_box will be used.
+        scale: scale helper to use when rounding the map scale. This should be a function that
+               takes a single float and returns a float which is at least as large as the value
+               passed in. This is a 1:x scale.
+        resolution: the resolution to render non vector elements at (in DPI), defaults to 72 DPI
+        preserve_aspect: whether to preserve map aspect ratio. This defaults to True and it
+                         is recommended you do not change it unless you know what you are doing
+                         scales and so on will not work if this is False.
+        centering: Centering rules for maps where the scale rounding has reduced the map size.
+                   This should be a value from the centering class. The default is to center on the
+                   maps constrained axis, typically this will be horizontal for portrait pages and
+                   vertical for landscape pages.
+        is_latlon: Is the map in lat lon degrees. If true magic anti meridian logic is enabled
+        use_ocg_layers: Create OCG layers in the PDF, requires pyPdf >= 1.13
+        """
+        self._pagesize = pagesize
+        self._margin = margin
+        self._box = box
+        self._scale = scale
+        self._resolution = resolution
+        self._preserve_aspect = preserve_aspect
+        self._centering = centering
+        self._is_latlon = is_latlon
+        self._use_ocg_layers = use_ocg_layers
+
+        self._s = None
+        self._layer_names = []
+        self._filename = None
+
+        self.map_box = None
+        self.scale = None
+
+        # don't both to round the scale if they are not preserving the aspect ratio
+        if not preserve_aspect:
+            self._scale = any_scale
+
+        if percent_box:
+            self._box = Box2d(percent_box[0]*pagesize[0],percent_box[1]*pagesize[1],
+                         percent_box[2]*pagesize[0],percent_box[3]*pagesize[1])
+
+        if not HAS_PYCAIRO_MODULE:
+            raise Exception("PDF rendering only available when pycairo is available")
+
+        self.font_name = "DejaVu Sans"
+
+    def finish(self):
+        if self._s:
+            self._s.finish()
+            self._s = None
+
+        if self._use_ocg_layers:
+            convert_pdf_pages_to_layers(self._filename,layer_names=self._layer_names + ["Legend and Information"],reverse_all_but_last=True)
+
+    def add_geospatial_pdf_header(self,m,filename,epsg=None,wkt=None):
+        """ Postprocessing step to add geospatial PDF information to PDF file as per
+        PDF standard 1.7 extension level 3 (also in draft PDF v2 standard at time of writing)
+
+        one of either the epsg code or wkt text for the projection must be provided
+
+        Should be called *after* the page has had .finish() called"""
+        if HAS_PYPDF and (epsg or wkt):
+            infile=file(filename,'rb')
+            (outfd,outfilename) = tempfile.mkstemp(dir=os.path.dirname(filename))
+            outfile = os.fdopen(outfd,'wb')
+
+            i=pyPdf.PdfFileReader(infile)
+            o=pyPdf.PdfFileWriter()
+
+            # preserve OCProperties at document root if we have one
+            if i.trailer['/Root'].has_key(pyPdf.generic.NameObject('/OCProperties')):
+                o._root.getObject()[pyPdf.generic.NameObject('/OCProperties')] = i.trailer['/Root'].getObject()[pyPdf.generic.NameObject('/OCProperties')]
+
+            for p in i.pages:
+                gcs = pyPdf.generic.DictionaryObject()
+                gcs[pyPdf.generic.NameObject('/Type')]=pyPdf.generic.NameObject('/PROJCS')
+                if epsg:
+                    gcs[pyPdf.generic.NameObject('/EPSG')]=pyPdf.generic.NumberObject(int(epsg))
+                if wkt:
+                    gcs[pyPdf.generic.NameObject('/WKT')]=pyPdf.generic.TextStringObject(wkt)
+
+                measure = pyPdf.generic.DictionaryObject()
+                measure[pyPdf.generic.NameObject('/Type')]=pyPdf.generic.NameObject('/Measure')
+                measure[pyPdf.generic.NameObject('/Subtype')]=pyPdf.generic.NameObject('/GEO')
+                measure[pyPdf.generic.NameObject('/GCS')]=gcs
+                bounds=pyPdf.generic.ArrayObject()
+                for x in (0.0,0.0,0.0,1.0,1.0,1.0,1.0,0.0):
+                    bounds.append(pyPdf.generic.FloatObject(str(x)))
+                measure[pyPdf.generic.NameObject('/Bounds')]=bounds
+                measure[pyPdf.generic.NameObject('/LPTS')]=bounds
+                gpts=pyPdf.generic.ArrayObject()
+
+                proj=Projection(m.srs)
+                env=m.envelope()
+                for x in ((env.minx, env.miny), (env.minx, env.maxy), (env.maxx, env.maxy), (env.maxx, env.miny)):
+                    latlon_corner=proj.inverse(Coord(*x))
+                    # these are in lat,lon order according to the standard
+                    gpts.append(pyPdf.generic.FloatObject(str(latlon_corner.y)))
+                    gpts.append(pyPdf.generic.FloatObject(str(latlon_corner.x)))
+                measure[pyPdf.generic.NameObject('/GPTS')]=gpts
+
+                vp=pyPdf.generic.DictionaryObject()
+                vp[pyPdf.generic.NameObject('/Type')]=pyPdf.generic.NameObject('/Viewport')
+                bbox=pyPdf.generic.ArrayObject()
+
+                for x in self.map_box:
+                    bbox.append(pyPdf.generic.FloatObject(str(x)))
+                vp[pyPdf.generic.NameObject('/BBox')]=bbox
+                vp[pyPdf.generic.NameObject('/Measure')]=measure
+
+                vpa = pyPdf.generic.ArrayObject()
+                vpa.append(vp)
+                p[pyPdf.generic.NameObject('/VP')]=vpa
+                o.addPage(p)
+
+            o.write(outfile)
+            infile=None
+            outfile.close()
+            os.rename(outfilename,filename)
+
+
+    def get_context(self):
+        """allow access so that extra 'bits' can be rendered to the page directly"""
+        return cairo.Context(self._s)
+
+    def get_width(self):
+        return self._pagesize[0]
+
+    def get_height(self):
+        return self._pagesize[1]
+
+    def get_margin(self):
+        return self._margin
+
+    def write_text(self,ctx,text,box_width=None,size=10, fill_color=(0.0, 0.0, 0.0), alignment=None):
+        if HAS_PANGOCAIRO_MODULE:
+            (attr,t,accel) = pango.parse_markup(text)
+            pctx = pangocairo.CairoContext(ctx)
+            l = pctx.create_layout()
+            l.set_attributes(attr)
+            fd = pango.FontDescription("%s %d" % (self.font_name,size))
+            l.set_font_description(fd)
+            if box_width:
+                l.set_width(int(box_width*pango.SCALE))
+            if alignment:
+                l.set_alignment(alignment)
+            pctx.update_layout(l)
+            l.set_text(t)
+            pctx.set_source_rgb(*fill_color)
+            pctx.show_layout(l)
+            return l.get_pixel_extents()[0]
+
+        else:
+            ctx.rel_move_to(0,size)
+            ctx.select_font_face(self.font_name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+            ctx.set_font_size(size)
+            ctx.show_text(text)
+            ctx.rel_move_to(0,size)
+            return (0,0,len(text)*size,size)
+
+    def _get_context(self):
+        if HAS_PANGOCAIRO_MODULE:
+            return
+        elif HAS_PYCAIRO_MODULE:
+            return cairo.Context(self._s)
+        return None
+
+    def _get_render_area(self):
+        """return a bounding box with the area of the page we are allowed to render out map to
+        in page coordinates (i.e. meters)
+        """
+        # take off our page margins
+        render_area = Box2d(self._margin,self._margin,self._pagesize[0]-self._margin,self._pagesize[1]-self._margin)
+
+        #then if user specified a box to render get intersection with that
+        if self._box:
+            return render_area.intersect(self._box)
+
+        return render_area
+
+    def _get_render_area_size(self):
+        """Get the width and height (in meters) of the area we can render the map to, returned as a tuple"""
+        render_area = self._get_render_area()
+        return (render_area.width(),render_area.height())
+
+    def _is_h_contrained(self,m):
+        """Test if the map size is constrained on the horizontal or vertical axes"""
+        available_area = self._get_render_area_size()
+        map_aspect = m.envelope().width()/m.envelope().height()
+        page_aspect = available_area[0]/available_area[1]
+
+        return map_aspect > page_aspect
+
+    def _get_meta_info_corner(self,render_size,m):
+        """Get the corner (in page coordinates) of a possibly
+        sensible place to render metadata such as a legend or scale"""
+        (x,y) = self._get_render_corner(render_size,m)
+        if self._is_h_contrained(m):
+            y += render_size[1]+0.005
+            x = self._margin
+        else:
+            x += render_size[0]+0.005
+            y = self._margin
+
+        return (x,y)
+
+    def _get_render_corner(self,render_size,m):
+        """Get the corner of the box we should render our map into"""
+        available_area = self._get_render_area()
+
+        x=available_area[0]
+        y=available_area[1]
+
+        h_is_contrained = self._is_h_contrained(m)
+
+        if (self._centering == centering.both or
+            self._centering == centering.horizontal or
+            (self._centering == centering.constrained and h_is_contrained) or
+            (self._centering == centering.unconstrained and not h_is_contrained)):
+            x+=(available_area.width()-render_size[0])/2
+
+        if (self._centering == centering.both or
+            self._centering == centering.vertical or
+            (self._centering == centering.constrained and not h_is_contrained) or
+            (self._centering == centering.unconstrained and h_is_contrained)):
+            y+=(available_area.height()-render_size[1])/2
+        return (x,y)
+
+    def _get_map_pixel_size(self, width_page_m, height_page_m):
+        """for a given map size in paper coordinates return a tuple of the map 'pixel' size we
+        should create at the defined resolution"""
+        return (int(m2px(width_page_m,self._resolution)), int(m2px(height_page_m,self._resolution)))
+
+    def render_map(self,m, filename):
+        """Render the given map to filename"""
+
+        # store this for later so we can post process the PDF
+        self._filename = filename
+
+        # work out the best scale to render out map at given the available space
+        (eff_width,eff_height) = self._get_render_area_size()
+        map_aspect = m.envelope().width()/m.envelope().height()
+        page_aspect = eff_width/eff_height
+
+        scalex=m.envelope().width()/eff_width
+        scaley=m.envelope().height()/eff_height
+
+        scale=max(scalex,scaley)
+
+        rounded_mapscale=self._scale(scale)
+        scalefactor = scale/rounded_mapscale
+        mapw=eff_width*scalefactor
+        maph=eff_height*scalefactor
+        if self._preserve_aspect:
+            if map_aspect > page_aspect:
+                maph=mapw*(1/map_aspect)
+            else:
+                mapw=maph*map_aspect
+
+        # set the map size so that raster elements render at the correct resolution
+        m.resize(*self._get_map_pixel_size(mapw,maph))
+        # calculate the translation for the map starting point
+        (tx,ty) = self._get_render_corner((mapw,maph),m)
+
+        # create our cairo surface and context and then render the map into it
+        self._s = cairo.PDFSurface(filename, m2pt(self._pagesize[0]),m2pt(self._pagesize[1]))
+        ctx=cairo.Context(self._s)
+
+        for l in m.layers:
+            # extract the layer names for naming layers if we use OCG
+            self._layer_names.append(l.name)
+
+            layer_map = Map(m.width,m.height,m.srs)
+            layer_map.layers.append(l)
+            for s in l.styles:
+                layer_map.append_style(s,m.find_style(s))
+            layer_map.zoom_to_box(m.envelope())
+
+            def render_map():
+                ctx.save()
+                ctx.translate(m2pt(tx),m2pt(ty))
+                #cairo defaults to 72dpi
+                ctx.scale(72.0/self._resolution,72.0/self._resolution)
+                render(layer_map, ctx)
+                ctx.restore()
+
+            # antimeridian
+            render_map()
+            if self._is_latlon and (m.envelope().minx < -180 or m.envelope().maxx > 180):
+                old_env = m.envelope()
+                if m.envelope().minx < -180:
+                    delta = 360
+                else:
+                    delta = -360
+                m.zoom_to_box(Box2d(old_env.minx+delta,old_env.miny,old_env.maxx+delta,old_env.maxy))
+                render_map()
+                # restore the original env
+                m.zoom_to_box(old_env)
+
+            if self._use_ocg_layers:
+                self._s.show_page()
+
+        self.scale = rounded_mapscale
+        self.map_box = Box2d(tx,ty,tx+mapw,ty+maph)
+
+    def render_on_map_lat_lon_grid(self,m,dec_degrees=True):
+        # don't render lat_lon grid if we are already in latlon
+        if self._is_latlon:
+            return
+        p2=Projection(m.srs)
+
+        latlon_bounds = p2.inverse(m.envelope())
+        if p2.inverse(m.envelope().center()).x > latlon_bounds.maxx:
+            latlon_bounds = Box2d(latlon_bounds.maxx,latlon_bounds.miny,latlon_bounds.minx+360,latlon_bounds.maxy)
+
+        if p2.inverse(m.envelope().center()).y > latlon_bounds.maxy:
+            latlon_bounds = Box2d(latlon_bounds.miny,latlon_bounds.maxy,latlon_bounds.maxx,latlon_bounds.miny+360)
+
+        latlon_mapwidth = latlon_bounds.width()
+        # render an extra 20% so we generally won't miss the ends of lines
+        latlon_buffer = 0.2*latlon_mapwidth
+        if dec_degrees:
+            latlon_divsize = default_scale(latlon_mapwidth/7.0)
+        else:
+            latlon_divsize = deg_min_sec_scale(latlon_mapwidth/7.0)
+        latlon_interpsize = latlon_mapwidth/m.width
+
+        self._render_lat_lon_axis(m,p2,latlon_bounds.minx,latlon_bounds.maxx,latlon_bounds.miny,latlon_bounds.maxy,latlon_buffer,latlon_interpsize,latlon_divsize,dec_degrees,True)
+        self._render_lat_lon_axis(m,p2,latlon_bounds.miny,latlon_bounds.maxy,latlon_bounds.minx,latlon_bounds.maxx,latlon_buffer,latlon_interpsize,latlon_divsize,dec_degrees,False)
+
+    def _render_lat_lon_axis(self,m,p2,x1,x2,y1,y2,latlon_buffer,latlon_interpsize,latlon_divsize,dec_degrees,is_x_axis):
+        ctx=cairo.Context(self._s)
+        ctx.set_source_rgb(1,0,0)
+        ctx.set_line_width(1)
+        latlon_labelsize = 6
+
+        ctx.translate(m2pt(self.map_box.minx),m2pt(self.map_box.miny))
+        ctx.rectangle(0,0,m2pt(self.map_box.width()),m2pt(self.map_box.height()))
+        ctx.clip()
+
+        ctx.select_font_face("DejaVu", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+        ctx.set_font_size(latlon_labelsize)
+
+        box_top = self.map_box.height()
+        if not is_x_axis:
+            ctx.translate(m2pt(self.map_box.width()/2),m2pt(self.map_box.height()/2))
+            ctx.rotate(-math.pi/2)
+            ctx.translate(-m2pt(self.map_box.height()/2),-m2pt(self.map_box.width()/2))
+            box_top = self.map_box.width()
+
+        for xvalue in round_grid_generator(x1 - latlon_buffer,x2 + latlon_buffer,latlon_divsize):
+            yvalue = y1 - latlon_buffer
+            start_cross = None
+            end_cross = None
+            while yvalue < y2+latlon_buffer:
+                if is_x_axis:
+                    start = m.view_transform().forward(p2.forward(Coord(xvalue,yvalue)))
+                else:
+                    temp = m.view_transform().forward(p2.forward(Coord(yvalue,xvalue)))
+                    start = Coord(m2pt(self.map_box.height())-temp.y,temp.x)
+                yvalue += latlon_interpsize
+                if is_x_axis:
+                    end = m.view_transform().forward(p2.forward(Coord(xvalue,yvalue)))
+                else:
+                    temp = m.view_transform().forward(p2.forward(Coord(yvalue,xvalue)))
+                    end = Coord(m2pt(self.map_box.height())-temp.y,temp.x)
+
+                ctx.move_to(start.x,start.y)
+                ctx.line_to(end.x,end.y)
+                ctx.stroke()
+
+                if cmp(start.y, 0) != cmp(end.y,0):
+                    start_cross = end.x
+                if cmp(start.y,m2pt(self.map_box.height())) != cmp(end.y, m2pt(self.map_box.height())):
+                    end_cross = end.x
+
+            if dec_degrees:
+                line_text = "%g" % (xvalue)
+            else:
+                line_text = format_deg_min_sec(xvalue)
+            if start_cross:
+                ctx.move_to(start_cross+2,latlon_labelsize)
+                ctx.show_text(line_text)
+            if end_cross:
+                ctx.move_to(end_cross+2,m2pt(box_top)-2)
+                ctx.show_text(line_text)
+
+    def render_on_map_scale(self,m):
+        (div_size,page_div_size) = self._get_sensible_scalebar_size(m)
+
+        first_value_x = (math.floor(m.envelope().minx / div_size) + 1) * div_size
+        first_value_x_percent = (first_value_x-m.envelope().minx)/m.envelope().width()
+        self._render_scale_axis(first_value_x,first_value_x_percent,self.map_box.minx,self.map_box.maxx,page_div_size,div_size,self.map_box.miny,self.map_box.maxy,True)
+
+        first_value_y = (math.floor(m.envelope().miny / div_size) + 1) * div_size
+        first_value_y_percent = (first_value_y-m.envelope().miny)/m.envelope().height()
+        self._render_scale_axis(first_value_y,first_value_y_percent,self.map_box.miny,self.map_box.maxy,page_div_size,div_size,self.map_box.minx,self.map_box.maxx,False)
+
+        if self._use_ocg_layers:
+            self._s.show_page()
+            self._layer_names.append("Coordinate Grid Overlay")
+
+    def _get_sensible_scalebar_size(self,m,width=-1):
+        # aim for about 8 divisions across the map
+        # also make sure we can fit the bar with in page area width if specified
+        div_size = sequence_scale(m.envelope().width()/8, [1,2,5])
+        page_div_size = self.map_box.width()*div_size/m.envelope().width()
+        while width > 0 and page_div_size > width:
+            div_size /=2
+            page_div_size /= 2
+        return (div_size,page_div_size)
+
+    def _render_box(self,ctx,x,y,w,h,text=None,stroke_color=(0,0,0),fill_color=(0,0,0)):
+        ctx.set_line_width(1)
+        ctx.set_source_rgb(*fill_color)
+        ctx.rectangle(x,y,w,h)
+        ctx.fill()
+
+        ctx.set_source_rgb(*stroke_color)
+        ctx.rectangle(x,y,w,h)
+        ctx.stroke()
+
+        if text:
+            ctx.move_to(x+1,y)
+            self.write_text(ctx,text,fill_color=[1-z for z in fill_color],size=h-2)
+
+    def _render_scale_axis(self,first,first_percent,start,end,page_div_size,div_size,boundary_start,boundary_end,is_x_axis):
+        prev = start
+        text = None
+        fill=(0,0,0)
+        border_size=8
+        value = first_percent * (end-start) + start
+        label_value = first-div_size
+        if self._is_latlon and label_value < -180:
+            label_value += 360
+
+        ctx=cairo.Context(self._s)
+
+        if not is_x_axis:
+            ctx.translate(m2pt(self.map_box.center().x),m2pt(self.map_box.center().y))
+            ctx.rotate(-math.pi/2)
+            ctx.translate(-m2pt(self.map_box.center().y),-m2pt(self.map_box.center().x))
+
+        while value < end:
+            ctx.move_to(m2pt(value),m2pt(boundary_start))
+            ctx.line_to(m2pt(value),m2pt(boundary_end))
+            ctx.set_source_rgb(0.5,0.5,0.5)
+            ctx.set_line_width(1)
+            ctx.stroke()
+
+            for bar in (m2pt(boundary_start)-border_size,m2pt(boundary_end)):
+                self._render_box(ctx,m2pt(prev),bar,m2pt(value-prev),border_size,text,fill_color=fill)
+
+            prev = value
+            value+=page_div_size
+            fill = [1-z for z in fill]
+            label_value += div_size
+            if self._is_latlon and label_value > 180:
+                label_value -= 360
+            text = "%d" % label_value
+        else:
+            for bar in (m2pt(boundary_start)-border_size,m2pt(boundary_end)):
+                self._render_box(ctx,m2pt(prev),bar,m2pt(end-prev),border_size,fill_color=fill)
+
+
+    def render_scale(self,m,ctx=None,width=0.05):
+        """ m: map to render scale for
+        ctx: A cairo context to render the scale to. If this is None (the default) then
+            automatically create a context and choose the best location for the scale bar.
+        width: Width of area available to render scale bar in (in m)
+
+        will return the size of the rendered scale block in pts
+        """
+
+        (w,h) = (0,0)
+
+        # don't render scale if we are lat lon
+        # dont report scale if we have warped the aspect ratio
+        if self._preserve_aspect and not self._is_latlon:
+            bar_size=8.0
+            box_count=3
+            if ctx is None:
+                ctx=cairo.Context(self._s)
+                (tx,ty) = self._get_meta_info_corner((self.map_box.width(),self.map_box.height()),m)
+                ctx.translate(tx,ty)
+
+            (div_size,page_div_size) = self._get_sensible_scalebar_size(m, width/box_count)
+
+
+            div_unit = "m"
+            if div_size > 1000:
+                div_size /= 1000
+                div_unit = "km"
+
+            text = "0%s" % div_unit
+            ctx.save()
+            if width > 0:
+                ctx.translate(m2pt(width-box_count*page_div_size)/2,0)
+            for ii in range(box_count):
+                fill=(ii%2,)*3
+                self._render_box(ctx, m2pt(ii*page_div_size), h, m2pt(page_div_size), bar_size, text, fill_color=fill)
+                fill = [1-z for z in fill]
+                text = "%g%s" % ((ii+1)*div_size,div_unit)
+            #else:
+            #    self._render_box(ctx, m2pt(box_count*page_div_size), h, m2pt(page_div_size), bar_size, text, fill_color=(1,1,1), stroke_color=(1,1,1))
+            w = (box_count)*page_div_size
+            h += bar_size
+            ctx.restore()
+
+            if width > 0:
+                box_width=m2pt(width)
+            else:
+                box_width = None
+
+            font_size=6
+            ctx.move_to(0,h)
+            if HAS_PANGOCAIRO_MODULE:
+                alignment = pango.ALIGN_CENTER
+            else:
+                alignment = None
+
+            text_ext=self.write_text(ctx,"Scale 1:%d" % self.scale,box_width=box_width,size=font_size, alignment=alignment)
+            h+=text_ext[3]+2
+
+        return (w,h)
+
+    def render_legend(self,m, page_break=False, ctx=None, collumns=1,width=None, height=None, item_per_rule=False, attribution={}, legend_item_box_size=(0.015,0.0075)):
+        """ m: map to render legend for
+        ctx: A cairo context to render the legend to. If this is None (the default) then
+            automatically create a context and choose the best location for the legend.
+        width: Width of area available to render legend in (in m)
+        page_break: move to next page if legen over flows this one
+        collumns: number of collumns available in legend box
+        attribution: additional text that will be rendered in gray under the layer name. keyed by layer name
+        legend_item_box_size:  two tuple with width and height of legend item box size in meters
+
+        will return the size of the rendered block in pts
+        """
+
+        (w,h) = (0,0)
+        if self._s:
+            if ctx is None:
+                ctx=cairo.Context(self._s)
+                (tx,ty) = self._get_meta_info_corner((self.map_box.width(),self.map_box.height()),m)
+                ctx.translate(m2pt(tx),m2pt(ty))
+                width = self._pagesize[0]-2*tx
+                height = self._pagesize[1]-self._margin-ty
+
+            x=0
+            y=0
+            if width:
+                cwidth = width/collumns
+                w=m2pt(width)
+            else:
+                cwidth = None
+            current_collumn = 0
+
+            processed_layers = []
+            for l in reversed(m.layers):
+                have_layer_header = False
+                added_styles={}
+                layer_title = l.name
+                if layer_title in processed_layers:
+                    continue
+                processed_layers.append(layer_title)
+
+                # check through the features to find which combinations of styles are active
+                # for each unique combination add a legend entry
+                for f in l.datasource.all_features():
+                    if f.num_geometries() > 0:
+                        active_rules = []
+                        rule_text = ""
+                        for s in l.styles:
+                            st = m.find_style(s)
+                            for r in st.rules:
+                                # we need to do the scale test here as well so we don't
+                                # add unused scale rules to the legend description
+                                if ((not r.filter) or r.filter.evaluate(f) == '1') and \
+                                    r.min_scale <= m.scale_denominator() and m.scale_denominator() < r.max_scale:
+                                    active_rules.append((s,r.name))
+                                    if r.filter and str(r.filter) != "true":
+                                        if len(rule_text) > 0:
+                                            rule_text += " AND "
+                                        if r.name:
+                                            rule_text += r.name
+                                        else:
+                                            rule_text += str(r.filter)
+                        active_rules = tuple(active_rules)
+                        if added_styles.has_key(active_rules):
+                            continue
+
+                        added_styles[active_rules] = (f,rule_text)
+                        if not item_per_rule:
+                            break
+                    else:
+                        added_styles[l] = (None,None)
+
+                legend_items = added_styles.keys()
+                legend_items.sort()
+                for li in legend_items:
+                    if True:
+                        (f,rule_text) = added_styles[li]
+
+
+                        legend_map_size = (int(m2pt(legend_item_box_size[0])),int(m2pt(legend_item_box_size[1])))
+                        lemap=Map(legend_map_size[0],legend_map_size[1],srs=m.srs)
+                        if m.background:
+                            lemap.background = m.background
+                        # the buffer is needed to ensure that text labels that overflow the edge of the
+                        # map still render for the legend
+                        lemap.buffer_size=1000
+                        for s in l.styles:
+                            sty=m.find_style(s)
+                            lestyle = Style()
+                            for r in sty.rules:
+                                for sym in r.symbols:
+                                    try:
+                                        sym.avoid_edges=False
+                                    except:
+                                        print "**** Cant set avoid edges for rule", r.name
+                                if r.min_scale <= m.scale_denominator() and m.scale_denominator() < r.max_scale:
+                                    lerule = r
+                                    lerule.min_scale = 0
+                                    lerule.max_scale = float("inf")
+                                    lestyle.rules.append(lerule)
+                            lemap.append_style(s,lestyle)
+
+                        ds = MemoryDatasource()
+                        if f is None:
+                            ds=l.datasource
+                            layer_srs = l.srs
+                        elif f.envelope().width() == 0:
+                            ds.add_feature(Feature(f.id(),Geometry2d.from_wkt("POINT(0 0)"),**f.attributes))
+                            lemap.zoom_to_box(Box2d(-1,-1,1,1))
+                            layer_srs = m.srs
+                        else:
+                            ds.add_feature(f)
+                            layer_srs = l.srs
+
+                        lelayer = Layer("LegendLayer",layer_srs)
+                        lelayer.datasource = ds
+                        for s in l.styles:
+                            lelayer.styles.append(s)
+                        lemap.layers.append(lelayer)
+
+                        if f is None or f.envelope().width() != 0:
+                            lemap.zoom_all()
+                            lemap.zoom(1.1)
+
+                        item_size = legend_map_size[1]
+                        if not have_layer_header:
+                            item_size += 8
+
+                        if y+item_size > m2pt(height):
+                            current_collumn += 1
+                            y=0
+                            if current_collumn >= collumns:
+                                if page_break:
+                                    self._s.show_page()
+                                    x=0
+                                    current_collumn = 0
+                                else:
+                                    break
+
+                        if not have_layer_header and item_per_rule:
+                            ctx.move_to(x+m2pt(current_collumn*cwidth),y)
+                            e=self.write_text(ctx, l.name, m2pt(cwidth), 8)
+                            y+=e[3]+2
+                            have_layer_header = True
+                        ctx.save()
+                        ctx.translate(x+m2pt(current_collumn*cwidth),y)
+                        #extra save around map render as it sets up a clip box and doesn't clear it
+                        ctx.save()
+                        render(lemap, ctx)
+                        ctx.restore()
+
+                        ctx.rectangle(0,0,*legend_map_size)
+                        ctx.set_source_rgb(0.5,0.5,0.5)
+                        ctx.set_line_width(1)
+                        ctx.stroke()
+                        ctx.restore()
+
+                        ctx.move_to(x+legend_map_size[0]+m2pt(current_collumn*cwidth)+2,y)
+                        legend_entry_size = legend_map_size[1]
+                        legend_text_size = 0
+                        if not item_per_rule:
+                            rule_text = layer_title
+                        if rule_text:
+                            e=self.write_text(ctx, rule_text, m2pt(cwidth-legend_item_box_size[0]-0.005), 6)
+                            legend_text_size += e[3]
+                            ctx.rel_move_to(0,e[3])
+                        if attribution.has_key(layer_title):
+                            e=self.write_text(ctx, attribution[layer_title], m2pt(cwidth-legend_item_box_size[0]-0.005), 6, fill_color=(0.5,0.5,0.5))
+                            legend_text_size += e[3]
+
+                        if legend_text_size > legend_entry_size:
+                            legend_entry_size=legend_text_size
+
+                        y+=legend_entry_size +2
+                        if y > h:
+                            h = y
+        return (w,h)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..f4ca59d
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[nosetests]
+verbosity=1
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..2980471
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,226 @@
+#! /usr/bin/env python
+
+from distutils import sysconfig
+from setuptools import setup, Extension
+import os
+import subprocess
+import sys
+import shutil
+import re
+
+cflags = sysconfig.get_config_var('CFLAGS')
+sysconfig._config_vars['CFLAGS'] = re.sub(' +', ' ', cflags.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+opt = sysconfig.get_config_var('OPT')
+sysconfig._config_vars['OPT'] = re.sub(' +', ' ', opt.replace('-g', '').replace('-Os', ''))
+ldshared = sysconfig.get_config_var('LDSHARED')
+sysconfig._config_vars['LDSHARED'] = re.sub(' +', ' ', ldshared.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+ldflags = sysconfig.get_config_var('LDFLAGS')
+sysconfig._config_vars['LDFLAGS'] = re.sub(' +', ' ', ldflags.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+pycflags = sysconfig.get_config_var('PY_CFLAGS')
+sysconfig._config_vars['PY_CFLAGS'] = re.sub(' +', ' ', pycflags.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+sysconfig._config_vars['CFLAGSFORSHARED'] = ''
+os.environ['ARCHFLAGS'] = ''
+
+if os.environ.get("MASON_BUILD", "false") == "true":
+    # run bootstrap.sh to get mason builds
+    subprocess.call(['./bootstrap.sh'])
+    mapnik_config = 'mason_packages/.link/bin/mapnik-config'
+    mason_build = True
+else:
+    mapnik_config = 'mapnik-config'
+    mason_build = False
+
+boost_python_lib = os.environ.get("BOOST_PYTHON_LIB", 'boost_python')
+
+try:
+    linkflags = subprocess.check_output([mapnik_config, '--libs']).rstrip('\n').split(' ')
+    lib_path = linkflags[0][2:]
+    linkflags.extend(subprocess.check_output([mapnik_config, '--ldflags']).rstrip('\n').split(' '))
+except:
+    raise Exception("Failed to find proper linking flags from mapnik config");
+
+## Dynamically make the mapnik/paths.py file if it doesn't exist.
+if os.path.isfile('mapnik/paths.py'):
+    create_paths = False
+else:
+    create_paths = True
+    f_paths = open('mapnik/paths.py', 'w')
+    f_paths.write('import os\n')
+    f_paths.write('\n')
+
+if mason_build:
+    try:
+        if sys.platform == 'darwin':
+            base_f = 'libmapnik.dylib'
+        else:
+            base_f = 'libmapnik.so.3.0'   
+        f = os.path.join(lib_path, base_f) 
+        shutil.copyfile(f, os.path.join('mapnik', base_f))
+    except shutil.Error:
+        pass
+    input_plugin_path = subprocess.check_output([mapnik_config, '--input-plugins']).rstrip('\n')
+    input_plugin_files = os.listdir(input_plugin_path)
+    input_plugin_files = [os.path.join(input_plugin_path, f) for f in input_plugin_files]
+    if not os.path.exists(os.path.join('mapnik','plugins','input')):
+        os.makedirs(os.path.join('mapnik','plugins', 'input'))
+    for f in input_plugin_files:
+        try:
+            shutil.copyfile(f, os.path.join('mapnik', 'plugins', 'input', os.path.basename(f)))
+        except shutil.Error:
+            pass
+    font_path = subprocess.check_output([mapnik_config, '--fonts']).rstrip('\n')
+    font_files = os.listdir(font_path)
+    font_files = [os.path.join(font_path, f) for f in font_files]
+    if not os.path.exists(os.path.join('mapnik','plugins','fonts')):
+        os.makedirs(os.path.join('mapnik','plugins','fonts'))
+    for f in font_files:
+        try:
+            shutil.copyfile(f, os.path.join('mapnik','plugins','fonts', os.path.basename(f)))
+        except shutil.Error:
+            pass
+    if create_paths:
+        f_paths.write('mapniklibpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")\n')
+elif create_paths:
+    f_paths.write("mapniklibpath = '"+lib_path+"/mapnik'\n")
+    f_paths.write('mapniklibpath = os.path.normpath(mapniklibpath)\n')
+
+if create_paths:
+    f_paths.write("inputpluginspath = os.path.join(mapniklibpath,'input')\n")
+    f_paths.write("fontscollectionpath = os.path.join(mapniklibpath,'fonts')\n")
+    f_paths.write("__all__ = [mapniklibpath,inputpluginspath,fontscollectionpath]\n")
+    f_paths.close()
+
+
+if not mason_build:
+    icu_path = subprocess.check_output([mapnik_config, '--icu-data']).rstrip('\n')
+else:
+    icu_path = 'mason_packages/.link/share/icu/'
+if icu_path:
+    icu_files = os.listdir(icu_path)
+    icu_files = [os.path.join(icu_path, f) for f in icu_files]
+    if not os.path.exists(os.path.join('mapnik','plugins','icu')):
+        os.makedirs(os.path.join('mapnik','plugins','icu'))
+    for f in icu_files:
+        try:
+            shutil.copyfile(f, os.path.join('mapnik','plugins','icu', os.path.basename(f)))
+        except shutil.Error:
+            pass
+
+if not mason_build:
+    gdal_path = subprocess.check_output([mapnik_config, '--gdal-data']).rstrip('\n')
+else:
+    gdal_path = 'mason_packages/.link/share/gdal/'
+    if os.path.exists('mason_packages/.link/share/gdal/gdal/'):
+        gdal_path = 'mason_packages/.link/share/gdal/gdal/'
+if gdal_path:
+    gdal_files = os.listdir(gdal_path)
+    gdal_files = [os.path.join(gdal_path, f) for f in gdal_files]
+    if not os.path.exists(os.path.join('mapnik','plugins','gdal')):
+        os.makedirs(os.path.join('mapnik','plugins','gdal'))
+    for f in gdal_files:
+        try:
+            shutil.copyfile(f, os.path.join('mapnik','plugins','gdal', os.path.basename(f)))
+        except shutil.Error:
+            pass
+
+if not mason_build:
+    proj_path = subprocess.check_output([mapnik_config, '--proj-lib']).rstrip('\n')
+else:
+    proj_path = 'mason_packages/.link/share/proj/'
+    if os.path.exists('mason_packages/.link/share/proj/proj/'):
+        proj_path = 'mason_packages/.link/share/proj/proj/'
+if proj_path:
+    proj_files = os.listdir(proj_path)
+    proj_files = [os.path.join(proj_path, f) for f in proj_files]
+    if not os.path.exists(os.path.join('mapnik','plugins','proj')):
+        os.makedirs(os.path.join('mapnik','plugins','proj'))
+    for f in proj_files:
+        try:
+            shutil.copyfile(f, os.path.join('mapnik','plugins','proj', os.path.basename(f)))
+        except shutil.Error:
+            pass
+
+extra_comp_args = subprocess.check_output([mapnik_config, '--cflags']).rstrip('\n').split(' ')
+
+if sys.platform == 'darwin':
+    extra_comp_args.append('-mmacosx-version-min=10.8')
+    linkflags.append('-mmacosx-version-min=10.8')
+else:
+    linkflags.append('-lrt') 
+    linkflags.append('-Wl,-z,origin') 
+    linkflags.append('-Wl,-rpath=$ORIGIN')
+
+if os.environ.get("CC",False) == False:
+    os.environ["CC"] = subprocess.check_output([mapnik_config, '--cxx']).rstrip('\n')
+if os.environ.get("CXX",False) == False:
+    os.environ["CXX"] = subprocess.check_output([mapnik_config, '--cxx']).rstrip('\n')
+
+setup(
+    name = "mapnik",
+    version = "0.1",
+    packages = ['mapnik'],
+    author = "Blake Thompson",
+    author_email = "flippmoke at gmail.com",
+    description = "Python bindings for Mapnik",
+    license = "GNU LESSER GENERAL PUBLIC LICENSE",
+    keywords = "mapnik mapbox mapping carteography",
+    url = "http://mapnik.org/", 
+    tests_require = [
+        'nose',
+    ],
+    package_data = {
+        'mapnik': ['libmapnik.*', 'plugins/*/*'],
+    },
+    test_suite = 'nose.collector',
+    ext_modules = [
+        Extension('mapnik._mapnik', [
+                'src/mapnik_color.cpp',
+                'src/mapnik_coord.cpp',
+                'src/mapnik_datasource.cpp',
+                'src/mapnik_datasource_cache.cpp',
+                'src/mapnik_envelope.cpp',
+                'src/mapnik_expression.cpp',
+                'src/mapnik_feature.cpp',
+                'src/mapnik_featureset.cpp',
+                'src/mapnik_font_engine.cpp',
+                'src/mapnik_fontset.cpp',
+                'src/mapnik_gamma_method.cpp',
+                'src/mapnik_geometry.cpp',
+                'src/mapnik_grid.cpp',
+                'src/mapnik_grid_view.cpp',
+                'src/mapnik_image.cpp',
+                'src/mapnik_image_view.cpp',
+                'src/mapnik_label_collision_detector.cpp',
+                'src/mapnik_layer.cpp',
+                'src/mapnik_logger.cpp',
+                'src/mapnik_map.cpp',
+                'src/mapnik_palette.cpp',
+                'src/mapnik_parameters.cpp',
+                'src/mapnik_proj_transform.cpp',
+                'src/mapnik_projection.cpp',
+                'src/mapnik_python.cpp',
+                'src/mapnik_query.cpp',
+                'src/mapnik_raster_colorizer.cpp',
+                'src/mapnik_rule.cpp',
+                'src/mapnik_scaling_method.cpp',
+                'src/mapnik_style.cpp',
+                'src/mapnik_svg_generator_grammar.cpp',
+                'src/mapnik_symbolizer.cpp',
+                'src/mapnik_text_placement.cpp',
+                'src/mapnik_view_transform.cpp',
+                'src/python_grid_utils.cpp',
+            ],
+            language='c++',
+            libraries = [
+                'mapnik', 
+                'mapnik-wkt',
+                'mapnik-json',
+                'boost_thread',
+                'boost_system',
+                boost_python_lib,
+            ],
+            extra_compile_args = extra_comp_args,
+            extra_link_args = linkflags,
+        )
+    ]
+)
diff --git a/src/boost_std_shared_shim.hpp b/src/boost_std_shared_shim.hpp
new file mode 100644
index 0000000..8b603e5
--- /dev/null
+++ b/src/boost_std_shared_shim.hpp
@@ -0,0 +1,49 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#ifndef MAPNIK_PYTHON_BOOST_STD_SHARED_SHIM
+#define MAPNIK_PYTHON_BOOST_STD_SHARED_SHIM
+
+// boost
+#include <boost/version.hpp>
+#include <boost/config.hpp>
+
+#if BOOST_VERSION < 105300 || defined BOOST_NO_CXX11_SMART_PTR
+
+// https://github.com/mapnik/mapnik/issues/2022
+#include <memory>
+
+namespace boost {
+template<class T> const T* get_pointer(std::shared_ptr<T> const& p)
+{
+    return p.get();
+}
+
+template<class T> T* get_pointer(std::shared_ptr<T>& p)
+{
+    return p.get();
+}
+} // namespace boost
+
+#endif
+
+#endif // MAPNIK_PYTHON_BOOST_STD_SHARED_SHIM
diff --git a/src/mapnik_color.cpp b/src/mapnik_color.cpp
new file mode 100644
index 0000000..54f0c9a
--- /dev/null
+++ b/src/mapnik_color.cpp
@@ -0,0 +1,130 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+#include "boost_std_shared_shim.hpp"
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+//mapnik
+#include <mapnik/color.hpp>
+
+
+using mapnik::color;
+
+struct color_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const color& c)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(c.red(),c.green(),c.blue(),c.alpha());
+    }
+};
+
+void export_color ()
+{
+    using namespace boost::python;
+    class_<color>("Color", init<int,int,int,int>(
+                      ( arg("r"), arg("g"), arg("b"), arg("a") ),
+                      "Creates a new color from its RGB components\n"
+                      "and an alpha value.\n"
+                      "All values between 0 and 255.\n")
+        )
+        .def(init<int,int,int,int,bool>(
+                      ( arg("r"), arg("g"), arg("b"), arg("a"), arg("premultiplied") ),
+                      "Creates a new color from its RGB components\n"
+                      "and an alpha value.\n"
+                      "All values between 0 and 255.\n")
+        )
+        .def(init<int,int,int>(
+                 ( arg("r"), arg("g"), arg("b") ),
+                 "Creates a new color from its RGB components.\n"
+                 "All values between 0 and 255.\n")
+            )
+        .def(init<uint32_t>(
+                 ( arg("val") ),
+                 "Creates a new color from an unsigned integer.\n"
+                 "All values between 0 and 2^32-1\n")
+            )
+        .def(init<uint32_t, bool>(
+                 ( arg("val"), arg("premultiplied") ),
+                 "Creates a new color from an unsigned integer.\n"
+                 "All values between 0 and 2^32-1\n")
+            )
+        .def(init<std::string>(
+                 ( arg("color_string") ),
+                 "Creates a new color from its CSS string representation.\n"
+                 "The string may be a CSS color name (e.g. 'blue')\n"
+                 "or a hex color string (e.g. '#0000ff').\n")
+            )
+        .def(init<std::string, bool>(
+                 ( arg("color_string"), arg("premultiplied") ),
+                 "Creates a new color from its CSS string representation.\n"
+                 "The string may be a CSS color name (e.g. 'blue')\n"
+                 "or a hex color string (e.g. '#0000ff').\n")
+            )
+        .add_property("r",
+                      &color::red,
+                      &color::set_red,
+                      "Gets or sets the red component.\n"
+                      "The value is between 0 and 255.\n")
+        .add_property("g",
+                      &color::green,
+                      &color::set_green,
+                      "Gets or sets the green component.\n"
+                      "The value is between 0 and 255.\n")
+        .add_property("b",
+                      &color::blue,
+                      &color::set_blue,
+                      "Gets or sets the blue component.\n"
+                      "The value is between 0 and 255.\n")
+        .add_property("a",
+                      &color::alpha,
+                      &color::set_alpha,
+                      "Gets or sets the alpha component.\n"
+                      "The value is between 0 and 255.\n")
+        .def(self == self)
+        .def(self != self)
+        .def_pickle(color_pickle_suite())
+        .def("__str__",&color::to_string)
+        .def("set_premultiplied",&color::set_premultiplied)
+        .def("get_premultiplied",&color::get_premultiplied)
+        .def("premultiply",&color::premultiply)
+        .def("demultiply",&color::demultiply)
+        .def("packed",&color::rgba)
+        .def("to_hex_string",&color::to_hex_string,
+             "Returns the hexadecimal representation of this color.\n"
+             "\n"
+             "Example:\n"
+             ">>> c = Color('blue')\n"
+             ">>> c.to_hex_string()\n"
+             "'#0000ff'\n")
+        ;
+}
diff --git a/src/mapnik_coord.cpp b/src/mapnik_coord.cpp
new file mode 100644
index 0000000..7c480f2
--- /dev/null
+++ b/src/mapnik_coord.cpp
@@ -0,0 +1,73 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#include <mapnik/config.hpp>
+#include "boost_std_shared_shim.hpp"
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+
+// mapnik
+#include <mapnik/coord.hpp>
+
+using mapnik::coord;
+
+struct coord_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const coord<double,2>& c)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(c.x,c.y);
+    }
+};
+
+void export_coord()
+{
+    using namespace boost::python;
+    class_<coord<double,2> >("Coord",init<double, double>(
+                                 // class docstring is in mapnik/__init__.py, class _Coord
+                                 (arg("x"), arg("y")),
+                                 "Constructs a new point with the given coordinates.\n")
+        )
+        .def_pickle(coord_pickle_suite())
+        .def_readwrite("x", &coord<double,2>::x,
+                       "Gets or sets the x/lon coordinate of the point.\n")
+        .def_readwrite("y", &coord<double,2>::y,
+                       "Gets or sets the y/lat coordinate of the point.\n")
+        .def(self == self) // __eq__
+        .def(self + self) // __add__
+        .def(self + float())
+        .def(float() + self)
+        .def(self - self) // __sub__
+        .def(self - float())
+        .def(self * float()) //__mult__
+        .def(float() * self)
+        .def(self / float()) // __div__
+        ;
+}
diff --git a/src/mapnik_datasource.cpp b/src/mapnik_datasource.cpp
new file mode 100644
index 0000000..b11ecd7
--- /dev/null
+++ b/src/mapnik_datasource.cpp
@@ -0,0 +1,217 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/version.hpp>
+#pragma GCC diagnostic pop
+
+// stl
+#include <vector>
+
+// mapnik
+#include <mapnik/box2d.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/datasource_cache.hpp>
+#include <mapnik/feature_layer_desc.hpp>
+#include <mapnik/memory_datasource.hpp>
+
+
+using mapnik::datasource;
+using mapnik::memory_datasource;
+using mapnik::layer_descriptor;
+using mapnik::attribute_descriptor;
+using mapnik::parameters;
+
+namespace
+{
+//user-friendly wrapper that uses Python dictionary
+using namespace boost::python;
+std::shared_ptr<mapnik::datasource> create_datasource(dict const& d)
+{
+    mapnik::parameters params;
+    boost::python::list keys=d.keys();
+    for (int i=0; i < len(keys); ++i)
+    {
+        std::string key = extract<std::string>(keys[i]);
+        object obj = d[key];
+        if (PyUnicode_Check(obj.ptr()))
+        {
+            PyObject* temp = PyUnicode_AsUTF8String(obj.ptr());
+            if (temp)
+            {
+#if PY_VERSION_HEX >= 0x03000000
+                char* c_str = PyBytes_AsString(temp);
+#else
+                char* c_str = PyString_AsString(temp);
+#endif
+                params[key] = std::string(c_str);
+                Py_DecRef(temp);
+            }
+            continue;
+        }
+
+        extract<std::string> ex0(obj);
+        extract<mapnik::value_integer> ex1(obj);
+        extract<double> ex2(obj);
+        if (ex0.check())
+        {
+            params[key] = ex0();
+        }
+        else if (ex1.check())
+        {
+            params[key] = ex1();
+        }
+        else if (ex2.check())
+        {
+            params[key] = ex2();
+        }
+    }
+
+    return mapnik::datasource_cache::instance().create(params);
+}
+
+boost::python::dict describe(std::shared_ptr<mapnik::datasource> const& ds)
+{
+    boost::python::dict description;
+    mapnik::layer_descriptor ld = ds->get_descriptor();
+    description["type"] = ds->type();
+    description["name"] = ld.get_name();
+    description["geometry_type"] = ds->get_geometry_type();
+    description["encoding"] = ld.get_encoding();
+    for (auto const& param : ld.get_extra_parameters())
+    {
+        description[param.first] = param.second;
+    }
+    return description;
+}
+
+boost::python::list fields(std::shared_ptr<mapnik::datasource> const& ds)
+{
+    boost::python::list flds;
+    if (ds)
+    {
+        layer_descriptor ld = ds->get_descriptor();
+        std::vector<attribute_descriptor> const& desc_ar = ld.get_descriptors();
+        std::vector<attribute_descriptor>::const_iterator it = desc_ar.begin();
+        std::vector<attribute_descriptor>::const_iterator end = desc_ar.end();
+        for (; it != end; ++it)
+        {
+            flds.append(it->get_name());
+        }
+    }
+    return flds;
+}
+boost::python::list field_types(std::shared_ptr<mapnik::datasource> const& ds)
+{
+    boost::python::list fld_types;
+    if (ds)
+    {
+        layer_descriptor ld = ds->get_descriptor();
+        std::vector<attribute_descriptor> const& desc_ar = ld.get_descriptors();
+        std::vector<attribute_descriptor>::const_iterator it = desc_ar.begin();
+        std::vector<attribute_descriptor>::const_iterator end = desc_ar.end();
+        for (; it != end; ++it)
+        {
+            unsigned type = it->get_type();
+            if (type == mapnik::Integer)
+                // this crashes, so send back strings instead
+                //fld_types.append(boost::python::object(boost::python::handle<>(&PyInt_Type)));
+                fld_types.append(boost::python::str("int"));
+            else if (type == mapnik::Float)
+                fld_types.append(boost::python::str("float"));
+            else if (type == mapnik::Double)
+                fld_types.append(boost::python::str("float"));
+            else if (type == mapnik::String)
+                fld_types.append(boost::python::str("str"));
+            else if (type == mapnik::Boolean)
+                fld_types.append(boost::python::str("bool"));
+            else if (type == mapnik::Geometry)
+                fld_types.append(boost::python::str("geometry"));
+            else if (type == mapnik::Object)
+                fld_types.append(boost::python::str("object"));
+            else
+                fld_types.append(boost::python::str("unknown"));
+        }
+    }
+    return fld_types;
+}}
+
+mapnik::parameters const& (mapnik::datasource::*params_const)() const =  &mapnik::datasource::params;
+
+
+void export_datasource()
+{
+    using namespace boost::python;
+
+    enum_<mapnik::datasource::datasource_t>("DataType")
+        .value("Vector",mapnik::datasource::Vector)
+        .value("Raster",mapnik::datasource::Raster)
+        ;
+
+    enum_<mapnik::datasource_geometry_t>("DataGeometryType")
+        .value("Point",mapnik::datasource_geometry_t::Point)
+        .value("LineString",mapnik::datasource_geometry_t::LineString)
+        .value("Polygon",mapnik::datasource_geometry_t::Polygon)
+        .value("Collection",mapnik::datasource_geometry_t::Collection)
+        ;
+
+    class_<datasource,std::shared_ptr<datasource>,
+        boost::noncopyable>("Datasource",no_init)
+        .def("type",&datasource::type)
+        .def("geometry_type",&datasource::get_geometry_type)
+        .def("describe",&describe)
+        .def("envelope",&datasource::envelope)
+        .def("features",&datasource::features)
+        .def("fields",&fields)
+        .def("field_types",&field_types)
+        .def("features_at_point",&datasource::features_at_point, (arg("coord"),arg("tolerance")=0))
+        .def("params",make_function(params_const,return_value_policy<copy_const_reference>()),
+             "The configuration parameters of the data source. "
+             "These vary depending on the type of data source.")
+        .def(self == self)
+        ;
+
+    def("CreateDatasource",&create_datasource);
+
+    class_<memory_datasource,
+           bases<datasource>, std::shared_ptr<memory_datasource>,
+           boost::noncopyable>("MemoryDatasourceBase", init<parameters>())
+        .def("add_feature",&memory_datasource::push,
+             "Adds a Feature:\n"
+             ">>> ms = MemoryDatasource()\n"
+             ">>> feature = Feature(1)\n"
+             ">>> ms.add_feature(Feature(1))\n")
+        .def("num_features",&memory_datasource::size)
+        ;
+
+    implicitly_convertible<std::shared_ptr<memory_datasource>,std::shared_ptr<datasource> >();
+}
diff --git a/src/mapnik_datasource_cache.cpp b/src/mapnik_datasource_cache.cpp
new file mode 100644
index 0000000..7122468
--- /dev/null
+++ b/src/mapnik_datasource_cache.cpp
@@ -0,0 +1,104 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/value_types.hpp>
+#include <mapnik/params.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/datasource_cache.hpp>
+
+namespace  {
+
+using namespace boost::python;
+
+std::shared_ptr<mapnik::datasource> create_datasource(const dict& d)
+{
+    mapnik::parameters params;
+    boost::python::list keys=d.keys();
+    for (int i=0; i<len(keys); ++i)
+    {
+        std::string key = extract<std::string>(keys[i]);
+        object obj = d[key];
+        extract<std::string> ex0(obj);
+        extract<mapnik::value_integer> ex1(obj);
+        extract<double> ex2(obj);
+
+        if (ex0.check())
+        {
+            params[key] = ex0();
+        }
+        else if (ex1.check())
+        {
+            params[key] = ex1();
+        }
+        else if (ex2.check())
+        {
+            params[key] = ex2();
+        }
+    }
+
+    return mapnik::datasource_cache::instance().create(params);
+}
+
+void register_datasources(std::string const& path)
+{
+    mapnik::datasource_cache::instance().register_datasources(path);
+}
+
+std::vector<std::string> plugin_names()
+{
+    return mapnik::datasource_cache::instance().plugin_names();
+}
+
+std::string plugin_directories()
+{
+    return mapnik::datasource_cache::instance().plugin_directories();
+}
+
+}
+
+void export_datasource_cache()
+{
+    using mapnik::datasource_cache;
+    class_<datasource_cache,
+           boost::noncopyable>("DatasourceCache",no_init)
+        .def("create",&create_datasource)
+        .staticmethod("create")
+        .def("register_datasources",&register_datasources)
+        .staticmethod("register_datasources")
+        .def("plugin_names",&plugin_names)
+        .staticmethod("plugin_names")
+        .def("plugin_directories",&plugin_directories)
+        .staticmethod("plugin_directories")
+        ;
+}
diff --git a/src/mapnik_enumeration.hpp b/src/mapnik_enumeration.hpp
new file mode 100644
index 0000000..ce2266a
--- /dev/null
+++ b/src/mapnik_enumeration.hpp
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_PYTHON_BINDING_ENUMERATION_INCLUDED
+#define MAPNIK_PYTHON_BINDING_ENUMERATION_INCLUDED
+
+#include <boost/python/converter/registered.hpp>  // for registered
+#include <boost/python/enum.hpp>        // for enum_
+#include <boost/python/implicit.hpp>    // for implicitly_convertible
+#include <boost/python/to_python_converter.hpp>
+
+namespace mapnik {
+
+template <typename EnumWrapper>
+class enumeration_ :
+        public boost::python::enum_<typename EnumWrapper::native_type>
+{
+    // some short cuts
+    using base_type = boost::python::enum_<typename EnumWrapper::native_type>;
+    using native_type = typename EnumWrapper::native_type;
+public:
+    enumeration_() :
+        base_type( EnumWrapper::get_name().c_str() )
+    {
+        init();
+    }
+    enumeration_(const char * python_alias) :
+        base_type( python_alias )
+    {
+        init();
+    }
+    enumeration_(const char * python_alias, const char * doc) :
+        base_type( python_alias, doc )
+    {
+        init();
+    }
+
+private:
+    struct converter
+    {
+        static PyObject* convert(EnumWrapper const& v)
+        {
+            // Redirect conversion to a static method of our base class's
+            // base class. A free template converter will not work because
+            // the base_type::base typedef is protected.
+            // Lets hope MSVC agrees that this is legal C++
+            using namespace boost::python::converter;
+            return base_type::base::to_python(
+                registered<native_type>::converters.m_class_object
+                ,  static_cast<long>( v ));
+
+        }
+    };
+
+    void init() {
+        boost::python::implicitly_convertible<native_type, EnumWrapper>();
+        boost::python::to_python_converter<EnumWrapper, converter >();
+
+        for (unsigned i = 0; i < EnumWrapper::MAX; ++i)
+        {
+            // Register the strings already defined for this enum.
+            base_type::value( EnumWrapper::get_string( i ), native_type( i ) );
+        }
+    }
+
+};
+
+} // end of namespace mapnik
+
+#endif // MAPNIK_PYTHON_BINDING_ENUMERATION_INCLUDED
diff --git a/src/mapnik_enumeration_wrapper_converter.hpp b/src/mapnik_enumeration_wrapper_converter.hpp
new file mode 100644
index 0000000..45e5f7f
--- /dev/null
+++ b/src/mapnik_enumeration_wrapper_converter.hpp
@@ -0,0 +1,45 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#ifndef MAPNIK_BINDINGS_PYTHON_ENUMERATION_WRAPPPER
+#define MAPNIK_BINDINGS_PYTHON_ENUMERATION_WRAPPPER
+
+// mapnik
+#include <mapnik/symbolizer.hpp>
+
+// boost
+#include <boost/python.hpp>
+
+
+namespace boost { namespace python {
+
+    struct mapnik_enumeration_wrapper_to_python
+    {
+        static PyObject* convert(mapnik::enumeration_wrapper const& v)
+        {
+            return ::PyLong_FromLongLong(v.value); // FIXME: this is a temp hack!!
+        }
+    };
+
+}}
+
+#endif // MAPNIK_BINDINGS_PYTHON_ENUMERATION_WRAPPPER
diff --git a/src/mapnik_envelope.cpp b/src/mapnik_envelope.cpp
new file mode 100644
index 0000000..2104c4f
--- /dev/null
+++ b/src/mapnik_envelope.cpp
@@ -0,0 +1,301 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/box2d.hpp>
+#include <mapnik/value_error.hpp>
+
+using mapnik::coord;
+using mapnik::box2d;
+
+struct envelope_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const box2d<double>& e)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(e.minx(),e.miny(),e.maxx(),e.maxy());
+    }
+};
+
+box2d<double> from_string(std::string const& s)
+{
+    box2d<double> bbox;
+    bool success = bbox.from_string(s);
+    if (success)
+    {
+        return bbox;
+    }
+    else
+    {
+        std::stringstream ss;
+        ss << "Could not parse bbox from string: '" << s << "'";
+        throw mapnik::value_error(ss.str());
+    }
+}
+
+//define overloads here
+void (box2d<double>::*width_p1)(double) = &box2d<double>::width;
+double (box2d<double>::*width_p2)() const = &box2d<double>::width;
+
+void (box2d<double>::*height_p1)(double) = &box2d<double>::height;
+double (box2d<double>::*height_p2)() const = &box2d<double>::height;
+
+void (box2d<double>::*expand_to_include_p1)(double,double) = &box2d<double>::expand_to_include;
+void (box2d<double>::*expand_to_include_p2)(coord<double,2> const& ) = &box2d<double>::expand_to_include;
+void (box2d<double>::*expand_to_include_p3)(box2d<double> const& ) = &box2d<double>::expand_to_include;
+
+bool (box2d<double>::*contains_p1)(double,double) const = &box2d<double>::contains;
+bool (box2d<double>::*contains_p2)(coord<double,2> const&) const = &box2d<double>::contains;
+bool (box2d<double>::*contains_p3)(box2d<double> const&) const = &box2d<double>::contains;
+
+//intersects
+bool (box2d<double>::*intersects_p1)(double,double) const = &box2d<double>::intersects;
+bool (box2d<double>::*intersects_p2)(coord<double,2> const&) const = &box2d<double>::intersects;
+bool (box2d<double>::*intersects_p3)(box2d<double> const&) const = &box2d<double>::intersects;
+
+// intersect
+box2d<double> (box2d<double>::*intersect)(box2d<double> const&) const = &box2d<double>::intersect;
+
+// re_center
+void (box2d<double>::*re_center_p1)(double,double) = &box2d<double>::re_center;
+void (box2d<double>::*re_center_p2)(coord<double,2> const& ) = &box2d<double>::re_center;
+
+// clip
+void (box2d<double>::*clip)(box2d<double> const&) = &box2d<double>::clip;
+
+// pad
+void (box2d<double>::*pad)(double) = &box2d<double>::pad;
+
+// deepcopy
+box2d<double> box2d_deepcopy(box2d<double> & obj, boost::python::dict const&)
+{
+    // FIXME::ignore memo for now
+    box2d<double> result(obj);
+    return result;
+}
+
+void export_envelope()
+{
+    using namespace boost::python;
+    class_<box2d<double> >("Box2d",
+                           // class docstring is in mapnik/__init__.py, class _Coord
+                           init<double,double,double,double>(
+                               (arg("minx"),arg("miny"),arg("maxx"),arg("maxy")),
+                               "Constructs a new envelope from the coordinates\n"
+                               "of its lower left and upper right corner points.\n"))
+        .def(init<>("Equivalent to Box2d(0, 0, -1, -1).\n"))
+        .def(init<const coord<double,2>&, const coord<double,2>&>(
+                 (arg("ll"),arg("ur")),
+                 "Equivalent to Box2d(ll.x, ll.y, ur.x, ur.y).\n"))
+        .def("from_string",from_string)
+        .staticmethod("from_string")
+        .add_property("minx", &box2d<double>::minx,
+                      "X coordinate for the lower left corner")
+        .add_property("miny", &box2d<double>::miny,
+                      "Y coordinate for the lower left corner")
+        .add_property("maxx", &box2d<double>::maxx,
+                      "X coordinate for the upper right corner")
+        .add_property("maxy", &box2d<double>::maxy,
+                      "Y coordinate for the upper right corner")
+        .def("center", &box2d<double>::center,
+             "Returns the coordinates of the center of the bounding box.\n"
+             "\n"
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.center()\n"
+             "Coord(50, 50)\n")
+        .def("center", re_center_p1,
+             (arg("x"), arg("y")),
+             "Moves the envelope so that the given coordinates become its new center.\n"
+             "The width and the height are preserved.\n"
+             "\n "
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.center(60, 60)\n"
+             ">>> e.center()\n"
+             "Coord(60.0,60.0)\n"
+             ">>> (e.width(), e.height())\n"
+             "(100.0, 100.0)\n"
+             ">>> e\n"
+             "Box2d(10.0, 10.0, 110.0, 110.0)\n"
+            )
+        .def("center", re_center_p2,
+             (arg("Coord")),
+             "Moves the envelope so that the given coordinates become its new center.\n"
+             "The width and the height are preserved.\n"
+             "\n "
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.center(Coord60, 60)\n"
+             ">>> e.center()\n"
+             "Coord(60.0,60.0)\n"
+             ">>> (e.width(), e.height())\n"
+             "(100.0, 100.0)\n"
+             ">>> e\n"
+             "Box2d(10.0, 10.0, 110.0, 110.0)\n"
+            )
+        .def("clip", clip,
+             (arg("other")),
+             "Clip the envelope based on the bounds of another envelope.\n"
+             "\n "
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> c = Box2d(-50, -50, 50, 50)\n"
+             ">>> e.clip(c)\n"
+             ">>> e\n"
+             "Box2d(0.0,0.0,50.0,50.0\n"
+            )
+        .def("pad", pad,
+             (arg("padding")),
+             "Pad the envelope based on a padding value.\n"
+             "\n "
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.pad(10)\n"
+             ">>> e\n"
+             "Box2d(-10.0,-10.0,110.0,110.0\n"
+            )
+        .def("width", width_p1,
+             (arg("new_width")),
+             "Sets the width to new_width of the envelope preserving its center.\n"
+             "\n "
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.width(120)\n"
+             ">>> e.center()\n"
+             "Coord(50.0,50.0)\n"
+             ">>> e\n"
+             "Box2d(-10.0, 0.0, 110.0, 100.0)\n"
+            )
+        .def("width", width_p2,
+             "Returns the width of this envelope.\n"
+            )
+        .def("height", height_p1,
+             (arg("new_height")),
+             "Sets the height to new_height of the envelope preserving its center.\n"
+             "\n "
+             "Example:\n"
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.height(120)\n"
+             ">>> e.center()\n"
+             "Coord(50.0,50.0)\n"
+             ">>> e\n"
+             "Box2d(0.0, -10.0, 100.0, 110.0)\n"
+            )
+        .def("height", height_p2,
+             "Returns the height of this envelope.\n"
+            )
+        .def("expand_to_include",expand_to_include_p1,
+             (arg("x"),arg("y")),
+             "Expands this envelope to include the point given by x and y.\n"
+             "\n"
+             "Example:\n",
+             ">>> e = Box2d(0, 0, 100, 100)\n"
+             ">>> e.expand_to_include(110, 110)\n"
+             ">>> e\n"
+             "Box2d(0.0, 00.0, 110.0, 110.0)\n"
+            )
+        .def("expand_to_include",expand_to_include_p2,
+             (arg("p")),
+             "Equivalent to expand_to_include(p.x, p.y)\n"
+            )
+        .def("expand_to_include",expand_to_include_p3,
+             (arg("other")),
+             "Equivalent to:\n"
+             "  expand_to_include(other.minx, other.miny)\n"
+             "  expand_to_include(other.maxx, other.maxy)\n"
+            )
+        .def("contains",contains_p1,
+             (arg("x"),arg("y")),
+             "Returns True iff this envelope contains the point\n"
+             "given by x and y.\n"
+            )
+        .def("contains",contains_p2,
+             (arg("p")),
+             "Equivalent to contains(p.x, p.y)\n"
+            )
+        .def("contains",contains_p3,
+             (arg("other")),
+             "Equivalent to:\n"
+             "  contains(other.minx, other.miny) and contains(other.maxx, other.maxy)\n"
+            )
+        .def("intersects",intersects_p1,
+             (arg("x"),arg("y")),
+             "Returns True iff this envelope intersects the point\n"
+             "given by x and y.\n"
+             "\n"
+             "Note: For points, intersection is equivalent\n"
+             "to containment, i.e. the following holds:\n"
+             "   e.contains(x, y) == e.intersects(x, y)\n"
+            )
+        .def("intersects",intersects_p2,
+             (arg("p")),
+             "Equivalent to contains(p.x, p.y)\n")
+        .def("intersects",intersects_p3,
+             (arg("other")),
+             "Returns True iff this envelope intersects the other envelope,\n"
+             "This relationship is symmetric."
+             "\n"
+             "Example:\n"
+             ">>> e1 = Box2d(0, 0, 100, 100)\n"
+             ">>> e2 = Box2d(50, 50, 150, 150)\n"
+             ">>> e1.intersects(e2)\n"
+             "True\n"
+             ">>> e1.contains(e2)\n"
+             "False\n"
+            )
+        .def("intersect",intersect,
+             (arg("other")),
+             "Returns the overlap of this envelope and the other envelope\n"
+             "as a new envelope.\n"
+             "\n"
+             "Example:\n"
+             ">>> e1 = Box2d(0, 0, 100, 100)\n"
+             ">>> e2 = Box2d(50, 50, 150, 150)\n"
+             ">>> e1.intersect(e2)\n"
+             "Box2d(50.0, 50.0, 100.0, 100.0)\n"
+            )
+        .def(self == self) // __eq__
+        .def(self != self) // __neq__
+        .def(self + self)  // __add__
+        .def(self * float()) // __mult__
+        .def(float() * self)
+        .def(self / float()) // __div__
+        .def("__getitem__",&box2d<double>::operator[])
+        .def("valid",&box2d<double>::valid)
+        .def_pickle(envelope_pickle_suite())
+        .def("__deepcopy__", &box2d_deepcopy)
+        ;
+
+}
diff --git a/src/mapnik_expression.cpp b/src/mapnik_expression.cpp
new file mode 100644
index 0000000..0a07482
--- /dev/null
+++ b/src/mapnik_expression.cpp
@@ -0,0 +1,111 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+#include "python_to_value.hpp"
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/util/variant.hpp>
+#include <mapnik/feature.hpp>
+#include <mapnik/expression.hpp>
+#include <mapnik/expression_string.hpp>
+#include <mapnik/expression_evaluator.hpp>
+#include <mapnik/parse_path.hpp>
+#include <mapnik/value.hpp>
+
+using mapnik::expression_ptr;
+using mapnik::parse_expression;
+using mapnik::to_expression_string;
+using mapnik::path_expression_ptr;
+
+
+// expression
+expression_ptr parse_expression_(std::string const& wkt)
+{
+    return parse_expression(wkt);
+}
+
+std::string expression_to_string_(mapnik::expr_node const& expr)
+{
+    return mapnik::to_expression_string(expr);
+}
+
+mapnik::value expression_evaluate_(mapnik::expr_node const& expr, mapnik::feature_impl const& f, boost::python::dict const& d)
+{
+    // will be auto-converted to proper python type by `mapnik_value_to_python`
+    return mapnik::util::apply_visitor(mapnik::evaluate<mapnik::feature_impl,mapnik::value,mapnik::attributes>(f,mapnik::dict2attr(d)),expr);
+}
+
+bool expression_evaluate_to_bool_(mapnik::expr_node const& expr, mapnik::feature_impl const& f, boost::python::dict const& d)
+{
+    return mapnik::util::apply_visitor(mapnik::evaluate<mapnik::feature_impl,mapnik::value,mapnik::attributes>(f,mapnik::dict2attr(d)),expr).to_bool();
+}
+
+// path expression
+path_expression_ptr parse_path_(std::string const& path)
+{
+    return mapnik::parse_path(path);
+}
+
+std::string path_to_string_(mapnik::path_expression const& expr)
+{
+    return mapnik::path_processor_type::to_string(expr);
+}
+
+std::string path_evaluate_(mapnik::path_expression const& expr, mapnik::feature_impl const& f)
+{
+    return mapnik::path_processor_type::evaluate(expr, f);
+}
+
+void export_expression()
+{
+    using namespace boost::python;
+    class_<mapnik::expr_node ,boost::noncopyable>("Expression",
+                                                  "TODO"
+                                                  "",no_init)
+        .def("evaluate", &expression_evaluate_,(arg("feature"),arg("variables")=boost::python::dict()))
+        .def("to_bool", &expression_evaluate_to_bool_,(arg("feature"),arg("variables")=boost::python::dict()))
+        .def("__str__",&expression_to_string_);
+    ;
+
+    def("Expression",&parse_expression_,(arg("expr")),"Expression string");
+
+    class_<mapnik::path_expression ,boost::noncopyable>("PathExpression",
+                                                        "TODO"
+                                                        "",no_init)
+        .def("evaluate", &path_evaluate_) // note: "pass" is a reserved word in Python
+        .def("__str__",&path_to_string_);
+    ;
+
+    def("PathExpression",&parse_path_,(arg("expr")),"PathExpression string");
+}
diff --git a/src/mapnik_feature.cpp b/src/mapnik_feature.cpp
new file mode 100644
index 0000000..a80ab15
--- /dev/null
+++ b/src/mapnik_feature.cpp
@@ -0,0 +1,237 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/iterator.hpp>
+#include <boost/python/call_method.hpp>
+#include <boost/python/tuple.hpp>
+#include <boost/python/to_python_converter.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/value_types.hpp>
+#include <mapnik/feature.hpp>
+#include <mapnik/feature_factory.hpp>
+#include <mapnik/feature_kv_iterator.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/wkb.hpp>
+//#include <mapnik/wkt/wkt_factory.hpp>
+#include <mapnik/json/feature_parser.hpp>
+#include <mapnik/json/feature_generator.hpp>
+
+// stl
+#include <stdexcept>
+
+namespace {
+
+using mapnik::geometry_utils;
+using mapnik::context_type;
+using mapnik::context_ptr;
+using mapnik::feature_kv_iterator;
+
+mapnik::feature_ptr from_geojson_impl(std::string const& json, mapnik::context_ptr const& ctx)
+{
+    mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx,1));
+    if (!mapnik::json::from_geojson(json,*feature))
+    {
+        throw std::runtime_error("Failed to parse geojson feature");
+    }
+    return feature;
+}
+
+std::string feature_to_geojson(mapnik::feature_impl const& feature)
+{
+    std::string json;
+    if (!mapnik::json::to_geojson(json,feature))
+    {
+        throw std::runtime_error("Failed to generate GeoJSON");
+    }
+    return json;
+}
+
+mapnik::value  __getitem__(mapnik::feature_impl const& feature, std::string const& name)
+{
+    return feature.get(name);
+}
+
+mapnik::value  __getitem2__(mapnik::feature_impl const& feature, std::size_t index)
+{
+    return feature.get(index);
+}
+
+void __setitem__(mapnik::feature_impl & feature, std::string const& name, mapnik::value const& val)
+{
+    feature.put_new(name,val);
+}
+
+boost::python::dict attributes(mapnik::feature_impl const& f)
+{
+    boost::python::dict attributes;
+    feature_kv_iterator itr = f.begin();
+    feature_kv_iterator end = f.end();
+
+    for ( ;itr!=end; ++itr)
+    {
+        attributes[std::get<0>(*itr)] = std::get<1>(*itr);
+    }
+
+    return attributes;
+}
+
+} // end anonymous namespace
+
+struct unicode_string_from_python_str
+{
+    unicode_string_from_python_str()
+    {
+        boost::python::converter::registry::push_back(
+            &convertible,
+            &construct,
+            boost::python::type_id<mapnik::value_unicode_string>());
+    }
+
+    static void* convertible(PyObject* obj_ptr)
+    {
+        if (!(
+#if PY_VERSION_HEX >= 0x03000000
+                PyBytes_Check(obj_ptr)
+#else
+                PyString_Check(obj_ptr)
+#endif
+                || PyUnicode_Check(obj_ptr)))
+            return 0;
+        return obj_ptr;
+    }
+
+    static void construct(
+        PyObject* obj_ptr,
+        boost::python::converter::rvalue_from_python_stage1_data* data)
+    {
+        char * value=0;
+        if (PyUnicode_Check(obj_ptr)) {
+            PyObject *encoded = PyUnicode_AsEncodedString(obj_ptr, "utf8", "replace");
+            if (encoded) {
+#if PY_VERSION_HEX >= 0x03000000
+                value = PyBytes_AsString(encoded);
+#else
+                value = PyString_AsString(encoded);
+#endif
+                Py_DecRef(encoded);
+            }
+        } else {
+#if PY_VERSION_HEX >= 0x03000000
+            value = PyBytes_AsString(obj_ptr);
+#else
+            value = PyString_AsString(obj_ptr);
+#endif
+        }
+        if (value == 0) boost::python::throw_error_already_set();
+        void* storage = (
+            (boost::python::converter::rvalue_from_python_storage<mapnik::value_unicode_string>*)
+            data)->storage.bytes;
+        new (storage) mapnik::value_unicode_string(value);
+        data->convertible = storage;
+    }
+};
+
+
+struct value_null_from_python
+{
+    value_null_from_python()
+    {
+        boost::python::converter::registry::push_back(
+            &convertible,
+            &construct,
+            boost::python::type_id<mapnik::value_null>());
+    }
+
+    static void* convertible(PyObject* obj_ptr)
+    {
+        if (obj_ptr == Py_None) return obj_ptr;
+        return 0;
+    }
+
+    static void construct(
+        PyObject* obj_ptr,
+        boost::python::converter::rvalue_from_python_stage1_data* data)
+    {
+        if (obj_ptr != Py_None) boost::python::throw_error_already_set();
+        void* storage = (
+            (boost::python::converter::rvalue_from_python_storage<mapnik::value_null>*)
+            data)->storage.bytes;
+        new (storage) mapnik::value_null();
+        data->convertible = storage;
+    }
+};
+
+void export_feature()
+{
+    using namespace boost::python;
+
+    // Python to mapnik::value converters
+    // NOTE: order matters here. For example value_null must be listed before
+    // bool otherwise Py_None will be interpreted as bool (false)
+    implicitly_convertible<mapnik::value_unicode_string,mapnik::value>();
+    implicitly_convertible<mapnik::value_null,mapnik::value>();
+    implicitly_convertible<mapnik::value_integer,mapnik::value>();
+    implicitly_convertible<mapnik::value_double,mapnik::value>();
+    implicitly_convertible<mapnik::value_bool,mapnik::value>();
+
+    // http://misspent.wordpress.com/2009/09/27/how-to-write-boost-python-converters/
+    unicode_string_from_python_str();
+    value_null_from_python();
+
+    class_<context_type,context_ptr,boost::noncopyable>
+        ("Context",init<>("Default ctor."))
+        .def("push", &context_type::push)
+        ;
+
+    class_<mapnik::feature_impl,std::shared_ptr<mapnik::feature_impl>,
+        boost::noncopyable>("Feature",init<context_ptr,mapnik::value_integer>("Default ctor."))
+        .def("id",&mapnik::feature_impl::id)
+        .add_property("geometry",
+                      make_function(&mapnik::feature_impl::get_geometry,return_value_policy<reference_existing_object>()),
+                      &mapnik::feature_impl::set_geometry_copy)
+        .def("envelope", &mapnik::feature_impl::envelope)
+        .def("has_key", &mapnik::feature_impl::has_key)
+        .add_property("attributes",&attributes)
+        .def("__setitem__",&__setitem__)
+        .def("__contains__",&__getitem__)
+        .def("__getitem__",&__getitem__)
+        .def("__getitem__",&__getitem2__)
+        .def("__len__", &mapnik::feature_impl::size)
+        .def("context",&mapnik::feature_impl::context)
+        .def("to_geojson",&feature_to_geojson)
+        .def("from_geojson",from_geojson_impl)
+        .staticmethod("from_geojson")
+        ;
+}
diff --git a/src/mapnik_featureset.cpp b/src/mapnik_featureset.cpp
new file mode 100644
index 0000000..f239a78
--- /dev/null
+++ b/src/mapnik_featureset.cpp
@@ -0,0 +1,93 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/feature.hpp>
+#include <mapnik/datasource.hpp>
+
+namespace {
+using namespace boost::python;
+
+inline list features(mapnik::featureset_ptr const& itr)
+{
+    list l;
+    while (true)
+    {
+        mapnik::feature_ptr fp = itr->next();
+        if (!fp)
+        {
+            break;
+        }
+        l.append(fp);
+    }
+    return l;
+}
+
+inline object pass_through(object const& o) { return o; }
+
+inline mapnik::feature_ptr next(mapnik::featureset_ptr const& itr)
+{
+    mapnik::feature_ptr f = itr->next();
+    if (!f)
+    {
+        PyErr_SetString(PyExc_StopIteration, "No more features.");
+        boost::python::throw_error_already_set();
+    }
+
+    return f;
+}
+
+}
+
+void export_featureset()
+{
+    using namespace boost::python;
+    class_<mapnik::Featureset,std::shared_ptr<mapnik::Featureset>,
+        boost::noncopyable>("Featureset",no_init)
+        .def("__iter__",pass_through)
+        .def("next",next)
+        .add_property("features",features,
+                      "The list of features.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.query_map_point(0, 10, 10)\n"
+                      "<mapnik._mapnik.Featureset object at 0x1004d2938>\n"
+                      ">>> fs = m.query_map_point(0, 10, 10)\n"
+                      ">>> for f in fs.features:\n"
+                      ">>>     print f\n"
+                      "<mapnik.Feature object at 0x105e64140>\n"
+            )
+        ;
+}
diff --git a/src/mapnik_font_engine.cpp b/src/mapnik_font_engine.cpp
new file mode 100644
index 0000000..e3a881f
--- /dev/null
+++ b/src/mapnik_font_engine.cpp
@@ -0,0 +1,60 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/font_engine_freetype.hpp>
+#include <mapnik/util/singleton.hpp>
+
+void export_font_engine()
+{
+    using mapnik::freetype_engine;
+    using mapnik::singleton;
+    using mapnik::CreateStatic;
+    using namespace boost::python;
+    class_<singleton<freetype_engine,CreateStatic>,boost::noncopyable>("Singleton",no_init)
+        .def("instance",&singleton<freetype_engine,CreateStatic>::instance,
+             return_value_policy<reference_existing_object>())
+        .staticmethod("instance")
+        ;
+
+    class_<freetype_engine,bases<singleton<freetype_engine,CreateStatic> >,
+        boost::noncopyable>("FontEngine",no_init)
+        .def("register_font",&freetype_engine::register_font)
+        .def("register_fonts",&freetype_engine::register_fonts)
+        .def("face_names",&freetype_engine::face_names)
+        .staticmethod("register_font")
+        .staticmethod("register_fonts")
+        .staticmethod("face_names")
+        ;
+}
diff --git a/src/mapnik_fontset.cpp b/src/mapnik_fontset.cpp
new file mode 100644
index 0000000..9d109a7
--- /dev/null
+++ b/src/mapnik_fontset.cpp
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+//mapnik
+#include <mapnik/font_set.hpp>
+
+
+using mapnik::font_set;
+
+void export_fontset ()
+{
+    using namespace boost::python;
+    class_<font_set>("FontSet", init<std::string const&>("default fontset constructor")
+        )
+        .add_property("name",
+                       make_function(&font_set::get_name,return_value_policy<copy_const_reference>()),
+                       &font_set::set_name,
+                      "Get/Set the name of the FontSet.\n"
+            )
+        .def("add_face_name",&font_set::add_face_name,
+             (arg("name")),
+             "Add a face-name to the fontset.\n"
+             "\n"
+             "Example:\n"
+             ">>> fs = Fontset('book-fonts')\n"
+             ">>> fs.add_face_name('DejaVu Sans Book')\n")
+        .add_property("names",make_function
+                      (&font_set::get_face_names,
+                       return_value_policy<reference_existing_object>()),
+                      "List of face names belonging to a FontSet.\n"
+            )
+        ;
+}
diff --git a/src/mapnik_gamma_method.cpp b/src/mapnik_gamma_method.cpp
new file mode 100644
index 0000000..9e6b478
--- /dev/null
+++ b/src/mapnik_gamma_method.cpp
@@ -0,0 +1,49 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/symbolizer_enumerations.hpp>
+#include "mapnik_enumeration.hpp"
+
+void export_gamma_method()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::gamma_method_e>("gamma_method")
+        .value("POWER", mapnik::GAMMA_POWER)
+        .value("LINEAR",mapnik::GAMMA_LINEAR)
+        .value("NONE", mapnik::GAMMA_NONE)
+        .value("THRESHOLD", mapnik::GAMMA_THRESHOLD)
+        .value("MULTIPLY", mapnik::GAMMA_MULTIPLY)
+        ;
+
+}
diff --git a/src/mapnik_geometry.cpp b/src/mapnik_geometry.cpp
new file mode 100644
index 0000000..dee9de4
--- /dev/null
+++ b/src/mapnik_geometry.cpp
@@ -0,0 +1,290 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/def.hpp>
+#include <boost/python/exception_translator.hpp>
+#include <boost/python/manage_new_object.hpp>
+#include <boost/python/iterator.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/version.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/geometry.hpp>
+#include <mapnik/geometry_type.hpp>
+#include <mapnik/geometry_envelope.hpp>
+#include <mapnik/geometry_is_valid.hpp>
+#include <mapnik/geometry_is_simple.hpp>
+#include <mapnik/geometry_is_empty.hpp>
+#include <mapnik/geometry_correct.hpp>
+#include <mapnik/geometry_centroid.hpp>
+
+#include <mapnik/wkt/wkt_factory.hpp> // from_wkt
+#include <mapnik/json/geometry_parser.hpp> // from_geojson
+#include <mapnik/util/geometry_to_geojson.hpp> // to_geojson
+#include <mapnik/util/geometry_to_wkb.hpp> // to_wkb
+#include <mapnik/util/geometry_to_wkt.hpp> // to_wkt
+//#include <mapnik/util/geometry_to_svg.hpp>
+#include <mapnik/wkb.hpp>
+
+
+// stl
+#include <stdexcept>
+
+namespace {
+
+std::shared_ptr<mapnik::geometry::geometry<double> > from_wkb_impl(std::string const& wkb)
+{
+    std::shared_ptr<mapnik::geometry::geometry<double> > geom = std::make_shared<mapnik::geometry::geometry<double> >();
+    try
+    {
+        *geom = mapnik::geometry_utils::from_wkb(wkb.c_str(), wkb.size());
+    }
+    catch (...)
+    {
+        throw std::runtime_error("Failed to parse WKB");
+    }
+    return geom;
+}
+
+std::shared_ptr<mapnik::geometry::geometry<double> > from_wkt_impl(std::string const& wkt)
+{
+    std::shared_ptr<mapnik::geometry::geometry<double> > geom = std::make_shared<mapnik::geometry::geometry<double> >();
+    if (!mapnik::from_wkt(wkt, *geom))
+        throw std::runtime_error("Failed to parse WKT geometry");
+    return geom;
+}
+
+std::shared_ptr<mapnik::geometry::geometry<double> > from_geojson_impl(std::string const& json)
+{
+    std::shared_ptr<mapnik::geometry::geometry<double> > geom = std::make_shared<mapnik::geometry::geometry<double> >();
+    if (!mapnik::json::from_geojson(json, *geom))
+        throw std::runtime_error("Failed to parse geojson geometry");
+    return geom;
+}
+
+}
+
+inline std::string boost_version()
+{
+    std::ostringstream s;
+    s << BOOST_VERSION/100000 << "." << BOOST_VERSION/100 % 1000  << "." << BOOST_VERSION % 100;
+    return s.str();
+}
+
+PyObject* to_wkb_impl(mapnik::geometry::geometry<double> const& geom, mapnik::wkbByteOrder byte_order)
+{
+    mapnik::util::wkb_buffer_ptr wkb = mapnik::util::to_wkb(geom,byte_order);
+    if (wkb)
+    {
+        return
+#if PY_VERSION_HEX >= 0x03000000
+            ::PyBytes_FromStringAndSize
+#else
+            ::PyString_FromStringAndSize
+#endif
+            ((const char*)wkb->buffer(),wkb->size());
+    }
+    else
+    {
+        Py_RETURN_NONE;
+    }
+}
+
+std::string to_geojson_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    std::string wkt;
+    if (!mapnik::util::to_geojson(wkt, geom))
+    {
+        throw std::runtime_error("Generate JSON failed");
+    }
+    return wkt;
+}
+
+std::string to_wkt_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    std::string wkt;
+    if (!mapnik::util::to_wkt(wkt,geom))
+    {
+        throw std::runtime_error("Generate WKT failed");
+    }
+    return wkt;
+}
+
+mapnik::geometry::geometry_types geometry_type_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    return mapnik::geometry::geometry_type(geom);
+}
+
+mapnik::box2d<double> geometry_envelope_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    return mapnik::geometry::envelope(geom);
+}
+
+// only Boost >= 1.56 contains the is_valid and is_simple functions
+#if BOOST_VERSION >= 105600
+bool geometry_is_valid_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    return mapnik::geometry::is_valid(geom);
+}
+
+bool geometry_is_simple_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    return mapnik::geometry::is_simple(geom);
+}
+#endif
+
+bool geometry_is_empty_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    return mapnik::geometry::is_empty(geom);
+}
+
+void geometry_correct_impl(mapnik::geometry::geometry<double> & geom)
+{
+    mapnik::geometry::correct(geom);
+}
+
+void polygon_set_exterior_impl(mapnik::geometry::polygon<double> & poly, mapnik::geometry::linear_ring<double> const& ring)
+{
+    poly.exterior_ring = ring; // copy
+}
+
+void polygon_add_hole_impl(mapnik::geometry::polygon<double> & poly, mapnik::geometry::linear_ring<double> const& ring)
+{
+    poly.interior_rings.push_back(ring); // copy
+}
+
+mapnik::geometry::point<double> geometry_centroid_impl(mapnik::geometry::geometry<double> const& geom)
+{
+    mapnik::geometry::point<double> pt;
+    mapnik::geometry::centroid(geom, pt);
+    return pt;
+}
+
+
+void export_geometry()
+{
+    using namespace boost::python;
+
+    implicitly_convertible<mapnik::geometry::point<double>, mapnik::geometry::geometry<double> >();
+    implicitly_convertible<mapnik::geometry::line_string<double>, mapnik::geometry::geometry<double> >();
+    implicitly_convertible<mapnik::geometry::polygon<double>, mapnik::geometry::geometry<double> >();
+    enum_<mapnik::geometry::geometry_types>("GeometryType")
+        .value("Unknown",mapnik::geometry::geometry_types::Unknown)
+        .value("Point",mapnik::geometry::geometry_types::Point)
+        .value("LineString",mapnik::geometry::geometry_types::LineString)
+        .value("Polygon",mapnik::geometry::geometry_types::Polygon)
+        .value("MultiPoint",mapnik::geometry::geometry_types::MultiPoint)
+        .value("MultiLineString",mapnik::geometry::geometry_types::MultiLineString)
+        .value("MultiPolygon",mapnik::geometry::geometry_types::MultiPolygon)
+        .value("GeometryCollection",mapnik::geometry::geometry_types::GeometryCollection)
+        ;
+
+    enum_<mapnik::wkbByteOrder>("wkbByteOrder")
+        .value("XDR",mapnik::wkbXDR)
+        .value("NDR",mapnik::wkbNDR)
+        ;
+
+    using mapnik::geometry::geometry;
+    using mapnik::geometry::point;
+    using mapnik::geometry::line_string;
+    using mapnik::geometry::linear_ring;
+    using mapnik::geometry::polygon;
+
+    class_<point<double> >("Point", init<double, double>((arg("x"), arg("y")),
+                                                "Constructs a new Point object\n"))
+        .add_property("x", &point<double>::x, "X coordinate")
+        .add_property("y", &point<double>::y, "Y coordinate")
+#if BOOST_VERSION >= 105600
+        .def("is_valid", &geometry_is_valid_impl)
+        .def("is_simple", &geometry_is_simple_impl)
+#endif
+        .def("to_geojson",&to_geojson_impl)
+        .def("to_wkb",&to_wkb_impl)
+        .def("to_wkt",&to_wkt_impl)
+        ;
+
+    class_<line_string<double> >("LineString", init<>(
+                      "Constructs a new LineString object\n"))
+        .def("add_coord", &line_string<double>::add_coord, "Adds coord")
+#if BOOST_VERSION >= 105600
+        .def("is_valid", &geometry_is_valid_impl)
+        .def("is_simple", &geometry_is_simple_impl)
+#endif
+        .def("to_geojson",&to_geojson_impl)
+        .def("to_wkb",&to_wkb_impl)
+        .def("to_wkt",&to_wkt_impl)
+        ;
+
+    class_<linear_ring<double> >("LinearRing", init<>(
+                            "Constructs a new LinearRtring object\n"))
+        .def("add_coord", &linear_ring<double>::add_coord, "Adds coord")
+        ;
+
+    class_<polygon<double> >("Polygon", init<>(
+                        "Constructs a new Polygon object\n"))
+        .add_property("exterior_ring", &polygon<double>::exterior_ring , "Exterior ring")
+        .def("add_hole", &polygon_add_hole_impl, "Add interior ring")
+        .def("num_rings", polygon_set_exterior_impl, "Number of rings (at least 1)")
+#if BOOST_VERSION >= 105600
+        .def("is_valid", &geometry_is_valid_impl)
+        .def("is_simple", &geometry_is_simple_impl)
+#endif
+        .def("to_geojson",&to_geojson_impl)
+        .def("to_wkb",&to_wkb_impl)
+        .def("to_wkt",&to_wkt_impl)
+        ;
+
+    class_<geometry<double>, std::shared_ptr<geometry<double> >, boost::noncopyable>("Geometry",no_init)
+        .def("envelope",&geometry_envelope_impl)
+        .def("from_geojson", from_geojson_impl)
+        .def("from_wkt", from_wkt_impl)
+        .def("from_wkb", from_wkb_impl)
+        .staticmethod("from_geojson")
+        .staticmethod("from_wkt")
+        .staticmethod("from_wkb")
+        .def("__str__",&to_wkt_impl)
+        .def("type",&geometry_type_impl)
+#if BOOST_VERSION >= 105600
+        .def("is_valid", &geometry_is_valid_impl)
+        .def("is_simple", &geometry_is_simple_impl)
+#endif
+        .def("is_empty", &geometry_is_empty_impl)
+        .def("correct", &geometry_correct_impl)
+        .def("centroid",&geometry_centroid_impl)
+        .def("to_wkb",&to_wkb_impl)
+        .def("to_wkt",&to_wkt_impl)
+        .def("to_geojson",&to_geojson_impl)
+        //.def("to_svg",&to_svg)
+        // TODO add other geometry_type methods
+        ;
+}
diff --git a/src/mapnik_grid.cpp b/src/mapnik_grid.cpp
new file mode 100644
index 0000000..c1f4b12
--- /dev/null
+++ b/src/mapnik_grid.cpp
@@ -0,0 +1,95 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#if defined(GRID_RENDERER)
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/grid/grid.hpp>
+#include "python_grid_utils.hpp"
+
+using namespace boost::python;
+
+// help compiler see template definitions
+static dict (*encode)( mapnik::grid const&, std::string const& , bool, unsigned int) = mapnik::grid_encode;
+
+bool painted(mapnik::grid const& grid)
+{
+    return grid.painted();
+}
+
+mapnik::grid::value_type get_pixel(mapnik::grid const& grid, int x, int y)
+{
+    if (x < static_cast<int>(grid.width()) && y < static_cast<int>(grid.height()))
+    {
+        mapnik::grid::data_type const & data = grid.data();
+        return data(x,y);
+    }
+    PyErr_SetString(PyExc_IndexError, "invalid x,y for grid dimensions");
+    boost::python::throw_error_already_set();
+    return 0;
+}
+
+void export_grid()
+{
+    class_<mapnik::grid,std::shared_ptr<mapnik::grid> >(
+        "Grid",
+        "This class represents a feature hitgrid.",
+        init<int,int,std::string>(
+            ( boost::python::arg("width"), boost::python::arg("height"),boost::python::arg("key")="__id__"),
+            "Create a mapnik.Grid object\n"
+            ))
+        .def("painted",&painted)
+        .def("width",&mapnik::grid::width)
+        .def("height",&mapnik::grid::height)
+        .def("view",&mapnik::grid::get_view)
+        .def("get_pixel",&get_pixel)
+        .def("clear",&mapnik::grid::clear)
+        .def("encode",encode,
+             ( boost::python::arg("encoding")="utf", boost::python::arg("features")=true,boost::python::arg("resolution")=4 ),
+             "Encode the grid as as optimized json\n"
+            )
+        .add_property("key",
+                      make_function(&mapnik::grid::get_key,return_value_policy<copy_const_reference>()),
+                      &mapnik::grid::set_key,
+                      "Get/Set key to be used as unique indentifier for features\n"
+                      "The value should either be __id__ to refer to the feature.id()\n"
+                      "or some globally unique integer or string attribute field\n"
+            )
+        ;
+
+}
+
+#endif
diff --git a/src/mapnik_grid_view.cpp b/src/mapnik_grid_view.cpp
new file mode 100644
index 0000000..2357c6b
--- /dev/null
+++ b/src/mapnik_grid_view.cpp
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#if defined(GRID_RENDERER)
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <string>
+#include <mapnik/grid/grid_view.hpp>
+#include <mapnik/grid/grid.hpp>
+#include "python_grid_utils.hpp"
+
+using namespace boost::python;
+
+// help compiler see template definitions
+static dict (*encode)( mapnik::grid_view const&, std::string const& , bool, unsigned int) = mapnik::grid_encode;
+
+void export_grid_view()
+{
+    class_<mapnik::grid_view,
+        std::shared_ptr<mapnik::grid_view> >("GridView",
+                                               "This class represents a feature hitgrid subset.",no_init)
+        .def("width",&mapnik::grid_view::width)
+        .def("height",&mapnik::grid_view::height)
+        .def("encode",encode,
+             ( boost::python::arg("encoding")="utf",boost::python::arg("add_features")=true,boost::python::arg("resolution")=4 ),
+             "Encode the grid as as optimized json\n"
+            )
+        ;
+}
+
+#endif
diff --git a/src/mapnik_image.cpp b/src/mapnik_image.cpp
new file mode 100644
index 0000000..6e6aeac
--- /dev/null
+++ b/src/mapnik_image.cpp
@@ -0,0 +1,471 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/color.hpp>
+#include <mapnik/palette.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/image_copy.hpp>
+#include <mapnik/image_reader.hpp>
+#include <mapnik/image_compositing.hpp>
+#include <mapnik/image_view_any.hpp>
+
+// cairo
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+#include <mapnik/cairo/cairo_context.hpp>
+#include <mapnik/cairo/cairo_image_util.hpp>
+#include <pycairo.h>
+#include <cairo.h>
+#endif
+
+using mapnik::image_any;
+using mapnik::image_reader;
+using mapnik::get_image_reader;
+using mapnik::type_from_filename;
+using mapnik::save_to_file;
+
+using namespace boost::python;
+
+// output 'raw' pixels
+PyObject* tostring1( image_any const& im)
+{
+    return
+#if PY_VERSION_HEX >= 0x03000000
+        ::PyBytes_FromStringAndSize
+#else
+        ::PyString_FromStringAndSize
+#endif
+        ((const char*)im.bytes(),im.size());
+}
+
+// encode (png,jpeg)
+PyObject* tostring2(image_any const & im, std::string const& format)
+{
+    std::string s = mapnik::save_to_string(im, format);
+    return
+#if PY_VERSION_HEX >= 0x03000000
+        ::PyBytes_FromStringAndSize
+#else
+        ::PyString_FromStringAndSize
+#endif
+        (s.data(),s.size());
+}
+
+PyObject* tostring3(image_any const & im, std::string const& format, mapnik::rgba_palette const& pal)
+{
+    std::string s = mapnik::save_to_string(im, format, pal);
+    return
+#if PY_VERSION_HEX >= 0x03000000
+        ::PyBytes_FromStringAndSize
+#else
+        ::PyString_FromStringAndSize
+#endif
+        (s.data(),s.size());
+}
+
+
+void save_to_file1(mapnik::image_any const& im, std::string const& filename)
+{
+    save_to_file(im,filename);
+}
+
+void save_to_file2(mapnik::image_any const& im, std::string const& filename, std::string const& type)
+{
+    save_to_file(im,filename,type);
+}
+
+void save_to_file3(mapnik::image_any const& im, std::string const& filename, std::string const& type, mapnik::rgba_palette const& pal)
+{
+    save_to_file(im,filename,type,pal);
+}
+
+mapnik::image_view_any get_view(mapnik::image_any const& data,unsigned x,unsigned y, unsigned w,unsigned h)
+{
+    return mapnik::create_view(data,x,y,w,h);
+}
+
+bool is_solid(mapnik::image_any const& im)
+{
+    return mapnik::is_solid(im);
+}
+
+void fill_color(mapnik::image_any & im, mapnik::color const& c)
+{
+    mapnik::fill(im, c);
+}
+
+void fill_int(mapnik::image_any & im, int val)
+{
+    mapnik::fill(im, val);
+}
+
+void fill_double(mapnik::image_any & im, double val)
+{
+    mapnik::fill(im, val);
+}
+
+std::shared_ptr<image_any> copy(mapnik::image_any const& im, mapnik::image_dtype type, double offset, double scaling)
+{
+    return std::make_shared<image_any>(mapnik::image_copy(im, type, offset, scaling));
+}
+
+unsigned compare(mapnik::image_any const& im1, mapnik::image_any const& im2, double threshold, bool alpha)
+{
+    return mapnik::compare(im1, im2, threshold, alpha);
+}
+
+struct get_pixel_visitor
+{
+    get_pixel_visitor(unsigned x, unsigned y)
+        : x_(x), y_(y) {}
+
+    object operator() (mapnik::image_null const&)
+    {
+        throw std::runtime_error("Can not return a null image from a pixel (shouldn't have reached here)");
+    }
+
+    template <typename T>
+    object operator() (T const& im)
+    {
+        using pixel_type = typename T::pixel_type;
+        return object(mapnik::get_pixel<pixel_type>(im, x_, y_));
+    }
+
+  private:
+    unsigned x_;
+    unsigned y_;
+};
+
+object get_pixel(mapnik::image_any const& im, unsigned x, unsigned y, bool get_color)
+{
+    if (x < static_cast<unsigned>(im.width()) && y < static_cast<unsigned>(im.height()))
+    {
+        if (get_color)
+        {
+            return object(
+                mapnik::get_pixel<mapnik::color>(im, x, y)
+            );
+        }
+        else
+        {
+            return mapnik::util::apply_visitor(get_pixel_visitor(x, y), im);
+        }
+    }
+    PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+    boost::python::throw_error_already_set();
+    return object();
+}
+
+void set_pixel_color(mapnik::image_any & im, unsigned x, unsigned y, mapnik::color const& c)
+{
+    if (x >= static_cast<int>(im.width()) && y >= static_cast<int>(im.height()))
+    {
+        PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+        boost::python::throw_error_already_set();
+        return;
+    }
+    mapnik::set_pixel(im, x, y, c);
+}
+
+void set_pixel_double(mapnik::image_any & im, unsigned x, unsigned y, double val)
+{
+    if (x >= static_cast<int>(im.width()) && y >= static_cast<int>(im.height()))
+    {
+        PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+        boost::python::throw_error_already_set();
+        return;
+    }
+    mapnik::set_pixel(im, x, y, val);
+}
+
+void set_pixel_int(mapnik::image_any & im, unsigned x, unsigned y, int val)
+{
+    if (x >= static_cast<int>(im.width()) && y >= static_cast<int>(im.height()))
+    {
+        PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+        boost::python::throw_error_already_set();
+        return;
+    }
+    mapnik::set_pixel(im, x, y, val);
+}
+
+unsigned get_type(mapnik::image_any & im)
+{
+    return im.get_dtype();
+}
+
+std::shared_ptr<image_any> open_from_file(std::string const& filename)
+{
+    boost::optional<std::string> type = type_from_filename(filename);
+    if (type)
+    {
+        std::unique_ptr<image_reader> reader(get_image_reader(filename,*type));
+        if (reader.get())
+        {
+            return std::make_shared<image_any>(reader->read(0,0,reader->width(),reader->height()));
+        }
+        throw mapnik::image_reader_exception("Failed to load: " + filename);
+    }
+    throw mapnik::image_reader_exception("Unsupported image format:" + filename);
+}
+
+std::shared_ptr<image_any> fromstring(std::string const& str)
+{
+    std::unique_ptr<image_reader> reader(get_image_reader(str.c_str(),str.size()));
+    if (reader.get())
+    {
+        return std::make_shared<image_any>(reader->read(0,0,reader->width(), reader->height()));
+    }
+    throw mapnik::image_reader_exception("Failed to load image from buffer" );
+}
+
+std::shared_ptr<image_any> frombuffer(PyObject * obj)
+{
+    void const* buffer=0;
+    Py_ssize_t buffer_len;
+    if (PyObject_AsReadBuffer(obj, &buffer, &buffer_len) == 0)
+    {
+        std::unique_ptr<image_reader> reader(get_image_reader(reinterpret_cast<char const*>(buffer),buffer_len));
+        if (reader.get())
+        {
+            return std::make_shared<image_any>(reader->read(0,0,reader->width(),reader->height()));
+        }
+    }
+    throw mapnik::image_reader_exception("Failed to load image from buffer" );
+}
+
+void set_grayscale_to_alpha(image_any & im)
+{
+    mapnik::set_grayscale_to_alpha(im);
+}
+
+void set_grayscale_to_alpha_c(image_any & im, mapnik::color const& c)
+{
+    mapnik::set_grayscale_to_alpha(im, c);
+}
+
+void set_color_to_alpha(image_any & im, mapnik::color const& c)
+{
+    mapnik::set_color_to_alpha(im, c);
+}
+
+void apply_opacity(image_any & im, float opacity)
+{
+    mapnik::apply_opacity(im, opacity);
+}
+
+bool premultiplied(image_any &im)
+{
+    return im.get_premultiplied();
+}
+
+bool premultiply(image_any & im)
+{
+    return mapnik::premultiply_alpha(im);
+}
+
+bool demultiply(image_any & im)
+{
+    return mapnik::demultiply_alpha(im);
+}
+
+void clear(image_any & im)
+{
+    mapnik::fill(im, 0);
+}
+
+void composite(image_any & dst, image_any & src, mapnik::composite_mode_e mode, float opacity, int dx, int dy)
+{
+    bool demultiply_dst = mapnik::premultiply_alpha(dst);
+    bool demultiply_src = mapnik::premultiply_alpha(src);
+    mapnik::composite(dst,src,mode,opacity,dx,dy);
+    if (demultiply_dst)
+    {
+        mapnik::demultiply_alpha(dst);
+    }
+    if (demultiply_src)
+    {
+        mapnik::demultiply_alpha(src);
+    }
+}
+
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+std::shared_ptr<image_any> from_cairo(PycairoSurface* py_surface)
+{
+    mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+    mapnik::image_rgba8 image = mapnik::image_rgba8(cairo_image_surface_get_width(&*surface), cairo_image_surface_get_height(&*surface));
+    cairo_image_to_rgba8(image, surface);
+    return std::make_shared<image_any>(std::move(image));
+}
+#endif
+
+void export_image()
+{
+    using namespace boost::python;
+    // NOTE: must match list in include/mapnik/image_compositing.hpp
+    enum_<mapnik::composite_mode_e>("CompositeOp")
+        .value("clear", mapnik::clear)
+        .value("src", mapnik::src)
+        .value("dst", mapnik::dst)
+        .value("src_over", mapnik::src_over)
+        .value("dst_over", mapnik::dst_over)
+        .value("src_in", mapnik::src_in)
+        .value("dst_in", mapnik::dst_in)
+        .value("src_out", mapnik::src_out)
+        .value("dst_out", mapnik::dst_out)
+        .value("src_atop", mapnik::src_atop)
+        .value("dst_atop", mapnik::dst_atop)
+        .value("xor", mapnik::_xor)
+        .value("plus", mapnik::plus)
+        .value("minus", mapnik::minus)
+        .value("multiply", mapnik::multiply)
+        .value("screen", mapnik::screen)
+        .value("overlay", mapnik::overlay)
+        .value("darken", mapnik::darken)
+        .value("lighten", mapnik::lighten)
+        .value("color_dodge", mapnik::color_dodge)
+        .value("color_burn", mapnik::color_burn)
+        .value("hard_light", mapnik::hard_light)
+        .value("soft_light", mapnik::soft_light)
+        .value("difference", mapnik::difference)
+        .value("exclusion", mapnik::exclusion)
+        .value("contrast", mapnik::contrast)
+        .value("invert", mapnik::invert)
+        .value("grain_merge", mapnik::grain_merge)
+        .value("grain_extract", mapnik::grain_extract)
+        .value("hue", mapnik::hue)
+        .value("saturation", mapnik::saturation)
+        .value("color", mapnik::_color)
+        .value("value", mapnik::_value)
+        .value("linear_dodge", mapnik::linear_dodge)
+        .value("linear_burn", mapnik::linear_burn)
+        .value("divide", mapnik::divide)
+        ;
+
+    enum_<mapnik::image_dtype>("ImageType")
+        .value("rgba8", mapnik::image_dtype_rgba8)
+        .value("gray8", mapnik::image_dtype_gray8)
+        .value("gray8s", mapnik::image_dtype_gray8s)
+        .value("gray16", mapnik::image_dtype_gray16)
+        .value("gray16s", mapnik::image_dtype_gray16s)
+        .value("gray32", mapnik::image_dtype_gray32)
+        .value("gray32s", mapnik::image_dtype_gray32s)
+        .value("gray32f", mapnik::image_dtype_gray32f)
+        .value("gray64", mapnik::image_dtype_gray64)
+        .value("gray64s", mapnik::image_dtype_gray64s)
+        .value("gray64f", mapnik::image_dtype_gray64f)
+        ;
+
+    class_<image_any,std::shared_ptr<image_any>, boost::noncopyable >("Image","This class represents a image.",init<int,int>())
+        .def(init<int,int,mapnik::image_dtype>())
+        .def(init<int,int,mapnik::image_dtype,bool>())
+        .def(init<int,int,mapnik::image_dtype,bool,bool>())
+        .def(init<int,int,mapnik::image_dtype,bool,bool,bool>())
+        .def("width",&image_any::width)
+        .def("height",&image_any::height)
+        .def("view",&get_view)
+        .def("painted",&image_any::painted)
+        .def("is_solid",&is_solid)
+        .def("fill",&fill_color)
+        .def("fill",&fill_int)
+        .def("fill",&fill_double)
+        .def("set_grayscale_to_alpha",&set_grayscale_to_alpha, "Set the grayscale values to the alpha channel of the Image")
+        .def("set_grayscale_to_alpha",&set_grayscale_to_alpha_c, "Set the grayscale values to the alpha channel of the Image")
+        .def("set_color_to_alpha",&set_color_to_alpha, "Set a given color to the alpha channel of the Image")
+        .def("apply_opacity",&apply_opacity, "Set the opacity of the Image relative to the current alpha of each pixel.")
+        .def("composite",&composite,
+         ( arg("self"),
+           arg("image"),
+           arg("mode")=mapnik::src_over,
+           arg("opacity")=1.0f,
+           arg("dx")=0,
+           arg("dy")=0
+         ))
+        .def("compare",&compare,
+         ( arg("self"),
+           arg("image"),
+           arg("threshold")=0.0,
+           arg("alpha")=true
+         ))
+        .def("copy",&copy,
+         ( arg("self"),
+           arg("type"),
+           arg("offset")=0.0,
+           arg("scaling")=1.0
+         ))
+        .add_property("offset",
+                      &image_any::get_offset,
+                      &image_any::set_offset,
+                      "Gets or sets the offset component.\n")
+        .add_property("scaling",
+                      &image_any::get_scaling,
+                      &image_any::set_scaling,
+                      "Gets or sets the offset component.\n")
+        .def("premultiplied",&premultiplied)
+        .def("premultiply",&premultiply)
+        .def("demultiply",&demultiply)
+        .def("set_pixel",&set_pixel_color)
+        .def("set_pixel",&set_pixel_double)
+        .def("set_pixel",&set_pixel_int)
+        .def("get_pixel",&get_pixel,
+             ( arg("self"),
+               arg("x"),
+               arg("y"),
+               arg("get_color")=false
+             ))
+        .def("get_type",&get_type)
+        .def("clear",&clear)
+        //TODO(haoyu) The method name 'tostring' might be confusing since they actually return bytes in Python 3
+
+        .def("tostring",&tostring1)
+        .def("tostring",&tostring2)
+        .def("tostring",&tostring3)
+        .def("save", &save_to_file1)
+        .def("save", &save_to_file2)
+        .def("save", &save_to_file3)
+        .def("open",open_from_file)
+        .staticmethod("open")
+        .def("frombuffer",&frombuffer)
+        .staticmethod("frombuffer")
+        .def("fromstring",&fromstring)
+        .staticmethod("fromstring")
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+        .def("from_cairo",&from_cairo)
+        .staticmethod("from_cairo")
+#endif
+        ;
+
+}
diff --git a/src/mapnik_image_view.cpp b/src/mapnik_image_view.cpp
new file mode 100644
index 0000000..07832fb
--- /dev/null
+++ b/src/mapnik_image_view.cpp
@@ -0,0 +1,128 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/image.hpp>
+#include <mapnik/image_view.hpp>
+#include <mapnik/image_view_any.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/palette.hpp>
+#include <sstream>
+
+using mapnik::image_view_any;
+using mapnik::save_to_file;
+
+// output 'raw' pixels
+PyObject* view_tostring1(image_view_any const& view)
+{
+    std::ostringstream ss(std::ios::out|std::ios::binary);
+    mapnik::view_to_stream(view, ss);
+    return
+#if PY_VERSION_HEX >= 0x03000000
+        ::PyBytes_FromStringAndSize
+#else
+        ::PyString_FromStringAndSize
+#endif
+        ((const char*)ss.str().c_str(),ss.str().size());
+}
+
+// encode (png,jpeg)
+PyObject* view_tostring2(image_view_any const & view, std::string const& format)
+{
+    std::string s = save_to_string(view, format);
+    return
+#if PY_VERSION_HEX >= 0x03000000
+        ::PyBytes_FromStringAndSize
+#else
+        ::PyString_FromStringAndSize
+#endif
+        (s.data(),s.size());
+}
+
+PyObject* view_tostring3(image_view_any const & view, std::string const& format, mapnik::rgba_palette const& pal)
+{
+    std::string s = save_to_string(view, format, pal);
+    return
+#if PY_VERSION_HEX >= 0x03000000
+        ::PyBytes_FromStringAndSize
+#else
+        ::PyString_FromStringAndSize
+#endif
+        (s.data(),s.size());
+}
+
+bool is_solid(image_view_any const& view)
+{
+    return mapnik::is_solid(view);
+}
+
+void save_view1(image_view_any const& view,
+                std::string const& filename)
+{
+    save_to_file(view,filename);
+}
+
+void save_view2(image_view_any const& view,
+                std::string const& filename,
+                std::string const& type)
+{
+    save_to_file(view,filename,type);
+}
+
+void save_view3(image_view_any const& view,
+                std::string const& filename,
+                std::string const& type,
+                mapnik::rgba_palette const& pal)
+{
+    save_to_file(view,filename,type,pal);
+}
+
+
+void export_image_view()
+{
+    using namespace boost::python;
+    class_<image_view_any>("ImageView","A view into an image.",no_init)
+        .def("width",&image_view_any::width)
+        .def("height",&image_view_any::height)
+        .def("is_solid",&is_solid)
+        .def("tostring",&view_tostring1)
+        .def("tostring",&view_tostring2)
+        .def("tostring",&view_tostring3)
+        .def("save",&save_view1)
+        .def("save",&save_view2)
+        .def("save",&save_view3)
+        ;
+}
diff --git a/src/mapnik_label_collision_detector.cpp b/src/mapnik_label_collision_detector.cpp
new file mode 100644
index 0000000..9e5a6cb
--- /dev/null
+++ b/src/mapnik_label_collision_detector.cpp
@@ -0,0 +1,131 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/label_collision_detector.hpp>
+#include <mapnik/map.hpp>
+
+#include <list>
+
+using mapnik::label_collision_detector4;
+using mapnik::box2d;
+using mapnik::Map;
+
+namespace
+{
+
+std::shared_ptr<label_collision_detector4>
+create_label_collision_detector_from_extent(box2d<double> const &extent)
+{
+    return std::make_shared<label_collision_detector4>(extent);
+}
+
+std::shared_ptr<label_collision_detector4>
+create_label_collision_detector_from_map(Map const &m)
+{
+    double buffer = m.buffer_size();
+    box2d<double> extent(-buffer, -buffer, m.width() + buffer, m.height() + buffer);
+    return std::make_shared<label_collision_detector4>(extent);
+}
+
+boost::python::list
+make_label_boxes(std::shared_ptr<label_collision_detector4> det)
+{
+    boost::python::list boxes;
+
+    for (label_collision_detector4::query_iterator jtr = det->begin();
+         jtr != det->end(); ++jtr)
+    {
+        boxes.append<box2d<double> >(jtr->get().box);
+    }
+
+    return boxes;
+}
+
+}
+
+void export_label_collision_detector()
+{
+    using namespace boost::python;
+
+    // for overload resolution
+    void (label_collision_detector4::*insert_box)(box2d<double> const &) = &label_collision_detector4::insert;
+
+    class_<label_collision_detector4, std::shared_ptr<label_collision_detector4>, boost::noncopyable>
+        ("LabelCollisionDetector",
+         "Object to detect collisions between labels, used in the rendering process.",
+         no_init)
+
+        .def("__init__", make_constructor(create_label_collision_detector_from_extent),
+             "Creates an empty collision detection object with a given extent. Note "
+             "that the constructor from Map objects is a sensible default and usually "
+             "what you want to do.\n"
+             "\n"
+             "Example:\n"
+             ">>> m = Map(size_x, size_y)\n"
+             ">>> buf_sz = m.buffer_size\n"
+             ">>> extent = mapnik.Box2d(-buf_sz, -buf_sz, m.width + buf_sz, m.height + buf_sz)\n"
+             ">>> detector = mapnik.LabelCollisionDetector(extent)")
+
+        .def("__init__", make_constructor(create_label_collision_detector_from_map),
+             "Creates an empty collision detection object matching the given Map object. "
+             "The created detector will have the same size, including the buffer, as the "
+             "map object. This is usually what you want to do.\n"
+             "\n"
+             "Example:\n"
+             ">>> m = Map(size_x, size_y)\n"
+             ">>> detector = mapnik.LabelCollisionDetector(m)")
+
+        .def("extent", &label_collision_detector4::extent, return_value_policy<copy_const_reference>(),
+             "Returns the total extent (bounding box) of all labels inside the detector.\n"
+             "\n"
+             "Example:\n"
+             ">>> detector.extent()\n"
+             "Box2d(573.252589209,494.789179821,584.261023823,496.83610261)")
+
+        .def("boxes", &make_label_boxes,
+             "Returns a list of all the label boxes inside the detector.")
+
+        .def("insert", insert_box,
+             "Insert a 2d box into the collision detector. This can be used to ensure that "
+             "some space is left clear on the map for later overdrawing, for example by "
+             "non-Mapnik processes.\n"
+             "\n"
+             "Example:\n"
+             ">>> m = Map(size_x, size_y)\n"
+             ">>> detector = mapnik.LabelCollisionDetector(m)"
+             ">>> detector.insert(mapnik.Box2d(196, 254, 291, 389))")
+        ;
+}
diff --git a/src/mapnik_layer.cpp b/src/mapnik_layer.cpp
new file mode 100644
index 0000000..0dad77c
--- /dev/null
+++ b/src/mapnik_layer.cpp
@@ -0,0 +1,388 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/layer.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/datasource_cache.hpp>
+
+using mapnik::layer;
+using mapnik::parameters;
+using mapnik::datasource_cache;
+
+
+struct layer_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const layer& l)
+    {
+        return boost::python::make_tuple(l.name(),l.srs());
+    }
+
+    static  boost::python::tuple
+    getstate(const layer& l)
+    {
+        boost::python::list s;
+        std::vector<std::string> const& style_names = l.styles();
+        for (unsigned i = 0; i < style_names.size(); ++i)
+        {
+            s.append(style_names[i]);
+        }
+        return boost::python::make_tuple(l.clear_label_cache(),l.minimum_scale_denominator(),l.maximum_scale_denominator(),l.queryable(),l.datasource()->params(),l.cache_features(),s);
+    }
+
+    static void
+    setstate (layer& l, boost::python::tuple state)
+    {
+        using namespace boost::python;
+        if (len(state) != 9)
+        {
+            PyErr_SetObject(PyExc_ValueError,
+                            ("expected 9-item tuple in call to __setstate__; got %s"
+                             % state).ptr()
+                );
+            throw_error_already_set();
+        }
+
+        l.set_clear_label_cache(extract<bool>(state[0]));
+
+        l.set_minimum_scale_denominator(extract<double>(state[1]));
+
+        l.set_maximum_scale_denominator(extract<double>(state[2]));
+
+        l.set_queryable(extract<bool>(state[3]));
+
+        mapnik::parameters params = extract<parameters>(state[4]);
+        l.set_datasource(datasource_cache::instance().create(params));
+
+        boost::python::list s = extract<boost::python::list>(state[5]);
+        for (int i=0;i<len(s);++i)
+        {
+            l.add_style(extract<std::string>(s[i]));
+        }
+
+        l.set_cache_features(extract<bool>(state[6]));
+    }
+};
+
+std::vector<std::string> & (mapnik::layer::*_styles_)() = &mapnik::layer::styles;
+
+void set_maximum_extent(mapnik::layer & l, boost::optional<mapnik::box2d<double> > const& box)
+{
+    if (box)
+    {
+        l.set_maximum_extent(*box);
+    }
+    else
+    {
+        l.reset_maximum_extent();
+    }
+}
+
+void set_buffer_size(mapnik::layer & l, boost::optional<int> const& buffer_size)
+{
+    if (buffer_size)
+    {
+        l.set_buffer_size(*buffer_size);
+    }
+    else
+    {
+        l.reset_buffer_size();
+    }
+}
+
+PyObject * get_buffer_size(mapnik::layer & l)
+{
+    boost::optional<int> buffer_size = l.buffer_size();
+    if (buffer_size)
+    {
+#if PY_VERSION_HEX >= 0x03000000
+        return PyLong_FromLong(*buffer_size);
+#else
+        return PyInt_FromLong(*buffer_size);
+#endif
+    }
+    else
+    {
+        Py_RETURN_NONE;
+    }
+}
+
+void export_layer()
+{
+    using namespace boost::python;
+    class_<std::vector<std::string> >("Names")
+        .def(vector_indexing_suite<std::vector<std::string>,true >())
+        ;
+
+    class_<layer>("Layer", "A Mapnik map layer.", init<std::string const&,optional<std::string const&> >(
+                      "Create a Layer with a named string and, optionally, an srs string.\n"
+                      "\n"
+                      "The srs can be either a Proj.4 epsg code ('+init=epsg:<code>') or\n"
+                      "of a Proj.4 literal ('+proj=<literal>').\n"
+                      "If no srs is specified it will default to '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr\n"
+                      "<mapnik._mapnik.Layer object at 0x6a270>\n"
+                      ))
+
+        .def_pickle(layer_pickle_suite())
+
+        .def("envelope",&layer::envelope,
+             "Return the geographic envelope/bounding box."
+             "\n"
+             "Determined based on the layer datasource.\n"
+             "\n"
+             "Usage:\n"
+             ">>> from mapnik import Layer\n"
+             ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+             ">>> lyr.envelope()\n"
+             "box2d(-1.0,-1.0,0.0,0.0) # default until a datasource is loaded\n"
+            )
+
+        .def("visible", &layer::visible,
+             "Return True if this layer's data is active and visible at a given scale_denom.\n"
+             "\n"
+             "Otherwise returns False.\n"
+             "Accepts a scale value as an integer or float input.\n"
+             "Will return False if:\n"
+             "\tscale_denom >= minimum_scale_denominator - 1e-6\n"
+             "\tor:\n"
+             "\tscale_denom < maximum_scale_denominator + 1e-6\n"
+             "\n"
+             "Usage:\n"
+             ">>> from mapnik import Layer\n"
+             ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+             ">>> lyr.visible(1.0/1000000)\n"
+             "True\n"
+             ">>> lyr.active = False\n"
+             ">>> lyr.visible(1.0/1000000)\n"
+             "False\n"
+            )
+
+        .add_property("active",
+                      &layer::active,
+                      &layer::set_active,
+                      "Get/Set whether this layer is active and will be rendered (same as status property).\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.active\n"
+                      "True # Active by default\n"
+                      ">>> lyr.active = False # set False to disable layer rendering\n"
+                      ">>> lyr.active\n"
+                      "False\n"
+            )
+
+        .add_property("status",
+                      &layer::active,
+                      &layer::set_active,
+                      "Get/Set whether this layer is active and will be rendered.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.status\n"
+                      "True # Active by default\n"
+                      ">>> lyr.status = False # set False to disable layer rendering\n"
+                      ">>> lyr.status\n"
+                      "False\n"
+            )
+
+        .add_property("clear_label_cache",
+                      &layer::clear_label_cache,
+                      &layer::set_clear_label_cache,
+                      "Get/Set whether to clear the label collision detector cache for this layer during rendering\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> lyr.clear_label_cache\n"
+                      "False # False by default, meaning label positions from other layers will impact placement \n"
+                      ">>> lyr.clear_label_cache = True # set to True to clear the label collision detector cache\n"
+            )
+
+        .add_property("cache_features",
+                      &layer::cache_features,
+                      &layer::set_cache_features,
+                      "Get/Set whether features should be cached during rendering if used between multiple styles\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> lyr.cache_features\n"
+                      "False # False by default\n"
+                      ">>> lyr.cache_features = True # set to True to enable feature caching\n"
+            )
+
+        .add_property("datasource",
+                      &layer::datasource,
+                      &layer::set_datasource,
+                      "The datasource attached to this layer.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer, Datasource\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.datasource = Datasource(type='shape',file='world_borders')\n"
+                      ">>> lyr.datasource\n"
+                      "<mapnik.Datasource object at 0x65470>\n"
+            )
+
+        .add_property("buffer_size",
+                      &get_buffer_size,
+                      &set_buffer_size,
+                      "Get/Set the size of buffer around layer in pixels.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> print(l.buffer_size)\n"
+                      "None # None by default\n"
+                      ">>> l.buffer_size = 2\n"
+                      ">>> l.buffer_size\n"
+                      "2\n"
+            )
+
+        .add_property("maximum_extent",make_function
+                      (&layer::maximum_extent,return_value_policy<copy_const_reference>()),
+                      &set_maximum_extent,
+                      "The maximum extent of the map.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.maximum_extent = Box2d(-180,-90,180,90)\n"
+            )
+
+        .add_property("maximum_scale_denominator",
+                      &layer::maximum_scale_denominator,
+                      &layer::set_maximum_scale_denominator,
+                      "Get/Set the maximum scale denominator of the layer.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.maximum_scale_denominator\n"
+                      "1.7976931348623157e+308 # default is the numerical maximum\n"
+                      ">>> lyr.maximum_scale_denominator = 1.0/1000000\n"
+                      ">>> lyr.maximum_scale_denominator\n"
+                      "9.9999999999999995e-07\n"
+            )
+
+        .add_property("minimum_scale_denominator",
+                      &layer::minimum_scale_denominator,
+                      &layer::set_minimum_scale_denominator,
+                      "Get/Set the minimum scale demoninator of the layer.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.minimum_scale_denominator # default is 0\n"
+                      "0.0\n"
+                      ">>> lyr.minimum_scale_denominator = 1.0/1000000\n"
+                      ">>> lyr.minimum_scale_denominator\n"
+                      "9.9999999999999995e-07\n"
+            )
+
+        .add_property("name",
+                      make_function(&layer::name, return_value_policy<copy_const_reference>()),
+                      &layer::set_name,
+                      "Get/Set the name of the layer.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import Layer\n"
+                      ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.name\n"
+                      "'My Layer'\n"
+                      ">>> lyr.name = 'New Name'\n"
+                      ">>> lyr.name\n"
+                      "'New Name'\n"
+            )
+
+        .add_property("queryable",
+                      &layer::queryable,
+                      &layer::set_queryable,
+                      "Get/Set whether this layer is queryable.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import layer\n"
+                      ">>> lyr = layer('My layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.queryable\n"
+                      "False # Not queryable by default\n"
+                      ">>> lyr.queryable = True\n"
+                      ">>> lyr.queryable\n"
+                      "True\n"
+            )
+
+        .add_property("srs",
+                      make_function(&layer::srs,return_value_policy<copy_const_reference>()),
+                      &layer::set_srs,
+                      "Get/Set the SRS of the layer.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import layer\n"
+                      ">>> lyr = layer('My layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.srs\n"
+                      "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs' # The default srs if not initialized with custom srs\n"
+                      ">>> # set to google mercator with Proj.4 literal\n"
+                      "... \n"
+                      ">>> lyr.srs = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'\n"
+            )
+
+        .add_property("group_by",
+                      make_function(&layer::group_by,return_value_policy<copy_const_reference>()),
+                      &layer::set_group_by,
+                      "Get/Set the optional layer group name.\n"
+                      "\n"
+                      "More details at https://github.com/mapnik/mapnik/wiki/Grouped-rendering:\n"
+            )
+
+        .add_property("styles",
+                      make_function(_styles_,return_value_policy<reference_existing_object>()),
+                      "The styles list attached to this layer.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> from mapnik import layer\n"
+                      ">>> lyr = layer('My layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+                      ">>> lyr.styles\n"
+                      "<mapnik._mapnik.Names object at 0x6d3e8>\n"
+                      ">>> len(lyr.styles)\n"
+                      "0\n # no styles until you append them\n"
+                      "lyr.styles.append('My Style') # mapnik uses named styles for flexibility\n"
+                      ">>> len(lyr.styles)\n"
+                      "1\n"
+                      ">>> lyr.styles[0]\n"
+                      "'My Style'\n"
+            )
+        // comparison
+        .def(self == self)
+        ;
+}
diff --git a/src/mapnik_logger.cpp b/src/mapnik_logger.cpp
new file mode 100644
index 0000000..6a1689f
--- /dev/null
+++ b/src/mapnik_logger.cpp
@@ -0,0 +1,83 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/debug.hpp>
+#include <mapnik/util/singleton.hpp>
+#include "mapnik_enumeration.hpp"
+
+void export_logger()
+{
+    using mapnik::logger;
+    using mapnik::singleton;
+    using mapnik::CreateStatic;
+    using namespace boost::python;
+
+    class_<singleton<logger,CreateStatic>,boost::noncopyable>("Singleton",no_init)
+        .def("instance",&singleton<logger,CreateStatic>::instance,
+             return_value_policy<reference_existing_object>())
+        .staticmethod("instance")
+        ;
+
+    enum_<mapnik::logger::severity_type>("severity_type")
+        .value("Debug", logger::debug)
+        .value("Warn", logger::warn)
+        .value("Error", logger::error)
+        .value("None", logger::none)
+        ;
+
+    class_<logger,bases<singleton<logger,CreateStatic> >,
+        boost::noncopyable>("logger",no_init)
+        .def("get_severity", &logger::get_severity)
+        .def("set_severity", &logger::set_severity)
+        .def("get_object_severity", &logger::get_object_severity)
+        .def("set_object_severity", &logger::set_object_severity)
+        .def("clear_object_severity", &logger::clear_object_severity)
+        .def("get_format", &logger::get_format)
+        .def("set_format", &logger::set_format)
+        .def("str", &logger::str)
+        .def("use_file", &logger::use_file)
+        .def("use_console", &logger::use_console)
+        .staticmethod("get_severity")
+        .staticmethod("set_severity")
+        .staticmethod("get_object_severity")
+        .staticmethod("set_object_severity")
+        .staticmethod("clear_object_severity")
+        .staticmethod("get_format")
+        .staticmethod("set_format")
+        .staticmethod("str")
+        .staticmethod("use_file")
+        .staticmethod("use_console")
+        ;
+}
diff --git a/src/mapnik_map.cpp b/src/mapnik_map.cpp
new file mode 100644
index 0000000..8797c04
--- /dev/null
+++ b/src/mapnik_map.cpp
@@ -0,0 +1,543 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#include <boost/python/iterator.hpp>
+#include <boost/iterator/transform_iterator.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/rule.hpp>
+#include <mapnik/layer.hpp>
+#include <mapnik/map.hpp>
+#include <mapnik/projection.hpp>
+#include <mapnik/view_transform.hpp>
+#include <mapnik/feature_type_style.hpp>
+#include "mapnik_enumeration.hpp"
+
+using mapnik::color;
+using mapnik::coord;
+using mapnik::box2d;
+using mapnik::layer;
+using mapnik::Map;
+
+std::vector<layer>& (Map::*layers_nonconst)() =  &Map::layers;
+std::vector<layer> const& (Map::*layers_const)() const =  &Map::layers;
+mapnik::parameters& (Map::*params_nonconst)() =  &Map::get_extra_parameters;
+
+void insert_style(mapnik::Map & m, std::string const& name, mapnik::feature_type_style const& style)
+{
+    m.insert_style(name,style);
+}
+
+void insert_fontset(mapnik::Map & m, std::string const& name, mapnik::font_set const& fontset)
+{
+    m.insert_fontset(name,fontset);
+}
+
+mapnik::feature_type_style find_style(mapnik::Map const& m, std::string const& name)
+{
+    boost::optional<mapnik::feature_type_style const&> style = m.find_style(name);
+    if (!style)
+    {
+        PyErr_SetString(PyExc_KeyError, "Invalid style name");
+        boost::python::throw_error_already_set();
+    }
+    return *style;
+}
+
+mapnik::font_set find_fontset(mapnik::Map const& m, std::string const& name)
+{
+    boost::optional<mapnik::font_set const&> fontset = m.find_fontset(name);
+    if (!fontset)
+    {
+        PyErr_SetString(PyExc_KeyError, "Invalid font_set name");
+        boost::python::throw_error_already_set();
+    }
+    return *fontset;
+}
+
+// TODO - we likely should allow indexing by negative number from python
+// for now, protect against negative values and kindly throw
+mapnik::featureset_ptr query_point(mapnik::Map const& m, int index, double x, double y)
+{
+    if (index < 0){
+        PyErr_SetString(PyExc_IndexError, "Please provide a layer index >= 0");
+        boost::python::throw_error_already_set();
+    }
+    unsigned idx = index;
+    return m.query_point(idx, x, y);
+}
+
+mapnik::featureset_ptr query_map_point(mapnik::Map const& m, int index, double x, double y)
+{
+    if (index < 0){
+        PyErr_SetString(PyExc_IndexError, "Please provide a layer index >= 0");
+        boost::python::throw_error_already_set();
+    }
+    unsigned idx = index;
+    return m.query_map_point(idx, x, y);
+}
+
+void set_maximum_extent(mapnik::Map & m, boost::optional<mapnik::box2d<double> > const& box)
+{
+    if (box)
+    {
+        m.set_maximum_extent(*box);
+    }
+    else
+    {
+        m.reset_maximum_extent();
+    }
+}
+
+struct extract_style
+{
+    using result_type = boost::python::tuple;
+    result_type operator() (std::map<std::string, mapnik::feature_type_style>::value_type const& val) const
+    {
+        return boost::python::make_tuple(val.first,val.second);
+    }
+};
+
+using style_extract_iterator = boost::transform_iterator<extract_style, Map::const_style_iterator>;
+using style_range = std::pair<style_extract_iterator,style_extract_iterator>;
+
+style_range _styles_ (mapnik::Map const& m)
+{
+    return style_range(
+        boost::make_transform_iterator<extract_style>(m.begin_styles(), extract_style()),
+        boost::make_transform_iterator<extract_style>(m.end_styles(), extract_style()));
+}
+
+void export_map()
+{
+    using namespace boost::python;
+
+    // aspect ratio fix modes
+    mapnik::enumeration_<mapnik::aspect_fix_mode_e>("aspect_fix_mode")
+        .value("GROW_BBOX", mapnik::Map::GROW_BBOX)
+        .value("GROW_CANVAS",mapnik::Map::GROW_CANVAS)
+        .value("SHRINK_BBOX",mapnik::Map::SHRINK_BBOX)
+        .value("SHRINK_CANVAS",mapnik::Map::SHRINK_CANVAS)
+        .value("ADJUST_BBOX_WIDTH",mapnik::Map::ADJUST_BBOX_WIDTH)
+        .value("ADJUST_BBOX_HEIGHT",mapnik::Map::ADJUST_BBOX_HEIGHT)
+        .value("ADJUST_CANVAS_WIDTH",mapnik::Map::ADJUST_CANVAS_WIDTH)
+        .value("ADJUST_CANVAS_HEIGHT", mapnik::Map::ADJUST_CANVAS_HEIGHT)
+        .value("RESPECT", mapnik::Map::RESPECT)
+        ;
+
+    class_<std::vector<layer> >("Layers")
+        .def(vector_indexing_suite<std::vector<layer> >())
+        ;
+
+    class_<style_range>("StyleRange")
+        .def("__iter__",
+             boost::python::range(&style_range::first, &style_range::second))
+        ;
+
+    class_<Map>("Map","The map object.",init<int,int,optional<std::string const&> >(
+                    ( arg("width"),arg("height"),arg("srs") ),
+                    "Create a Map with a width and height as integers and, optionally,\n"
+                    "an srs string either with a Proj.4 epsg code ('+init=epsg:<code>')\n"
+                    "or with a Proj.4 literal ('+proj=<literal>').\n"
+                    "If no srs is specified the map will default to '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+                    "\n"
+                    "Usage:\n"
+                    ">>> from mapnik import Map\n"
+                    ">>> m = Map(600,400)\n"
+                    ">>> m\n"
+                    "<mapnik._mapnik.Map object at 0x6a240>\n"
+                    ">>> m.srs\n"
+                    "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+                    ))
+
+        .def("append_style",insert_style,
+             (arg("style_name"),arg("style_object")),
+             "Insert a Mapnik Style onto the map by appending it.\n"
+             "\n"
+             "Usage:\n"
+             ">>> sty\n"
+             "<mapnik._mapnik.Style object at 0x6a330>\n"
+             ">>> m.append_style('Style Name', sty)\n"
+             "True # style object added to map by name\n"
+             ">>> m.append_style('Style Name', sty)\n"
+             "False # you can only append styles with unique names\n"
+            )
+
+        .def("append_fontset",insert_fontset,
+             (arg("fontset")),
+             "Add a FontSet to the map."
+            )
+
+        .def("buffered_envelope",
+             &Map::get_buffered_extent,
+             "Get the Box2d() of the Map given\n"
+             "the Map.buffer_size.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m = Map(600,400)\n"
+             ">>> m.envelope()\n"
+             "Box2d(-1.0,-1.0,0.0,0.0)\n"
+             ">>> m.buffered_envelope()\n"
+             "Box2d(-1.0,-1.0,0.0,0.0)\n"
+             ">>> m.buffer_size = 1\n"
+             ">>> m.buffered_envelope()\n"
+             "Box2d(-1.02222222222,-1.02222222222,0.0222222222222,0.0222222222222)\n"
+            )
+
+        .def("envelope",
+             make_function(&Map::get_current_extent,
+                           return_value_policy<copy_const_reference>()),
+             "Return the Map Box2d object\n"
+             "and print the string representation\n"
+             "of the current extent of the map.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.envelope()\n"
+             "Box2d(-0.185833333333,-0.96,0.189166666667,-0.71)\n"
+             ">>> dir(m.envelope())\n"
+             "...'center', 'contains', 'expand_to_include', 'forward',\n"
+             "...'height', 'intersect', 'intersects', 'inverse', 'maxx',\n"
+             "...'maxy', 'minx', 'miny', 'width'\n"
+            )
+
+        .def("find_fontset",find_fontset,
+             (arg("name")),
+             "Find a fontset by name."
+            )
+
+        .def("find_style",
+             find_style,
+             (arg("name")),
+             "Query the Map for a style by name and return\n"
+             "a style object if found or raise KeyError\n"
+             "style if not found.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.find_style('Style Name')\n"
+             "<mapnik._mapnik.Style object at 0x654f0>\n"
+            )
+
+        .add_property("styles", _styles_)
+
+        .def("pan",&Map::pan,
+             (arg("x"),arg("y")),
+             "Set the Map center at a given x,y location\n"
+             "as integers in the coordinates of the pixmap or map surface.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m = Map(600,400)\n"
+             ">>> m.envelope().center()\n"
+             "Coord(-0.5,-0.5) # default Map center\n"
+             ">>> m.pan(-1,-1)\n"
+             ">>> m.envelope().center()\n"
+             "Coord(0.00166666666667,-0.835)\n"
+            )
+
+        .def("pan_and_zoom",&Map::pan_and_zoom,
+             (arg("x"),arg("y"),arg("factor")),
+             "Set the Map center at a given x,y location\n"
+             "and zoom factor as a float.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m = Map(600,400)\n"
+             ">>> m.envelope().center()\n"
+             "Coord(-0.5,-0.5) # default Map center\n"
+             ">>> m.scale()\n"
+             "-0.0016666666666666668\n"
+             ">>> m.pan_and_zoom(-1,-1,0.25)\n"
+             ">>> m.scale()\n"
+             "0.00062500000000000001\n"
+            )
+
+        .def("query_map_point",query_map_point,
+             (arg("layer_idx"),arg("pixel_x"),arg("pixel_y")),
+             "Query a Map Layer (by layer index) for features \n"
+             "intersecting the given x,y location in the pixel\n"
+             "coordinates of the rendered map image.\n"
+             "Layer index starts at 0 (first layer in map).\n"
+             "Will return a Mapnik Featureset if successful\n"
+             "otherwise will return None.\n"
+             "\n"
+             "Usage:\n"
+             ">>> featureset = m.query_map_point(0,200,200)\n"
+             ">>> featureset\n"
+             "<mapnik._mapnik.Featureset object at 0x23b0b0>\n"
+             ">>> featureset.features\n"
+             ">>> [<mapnik.Feature object at 0x3995630>]\n"
+            )
+
+        .def("query_point",query_point,
+             (arg("layer idx"),arg("x"),arg("y")),
+             "Query a Map Layer (by layer index) for features \n"
+             "intersecting the given x,y location in the coordinates\n"
+             "of map projection.\n"
+             "Layer index starts at 0 (first layer in map).\n"
+             "Will return a Mapnik Featureset if successful\n"
+             "otherwise will return None.\n"
+             "\n"
+             "Usage:\n"
+             ">>> featureset = m.query_point(0,-122,48)\n"
+             ">>> featureset\n"
+             "<mapnik._mapnik.Featureset object at 0x23b0b0>\n"
+             ">>> featureset.features\n"
+             ">>> [<mapnik.Feature object at 0x3995630>]\n"
+            )
+
+        .def("remove_all",&Map::remove_all,
+             "Remove all Mapnik Styles and layers from the Map.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.remove_all()\n"
+            )
+
+        .def("remove_style",&Map::remove_style,
+             (arg("style_name")),
+             "Remove a Mapnik Style from the map.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.remove_style('Style Name')\n"
+            )
+
+        .def("resize",&Map::resize,
+             (arg("width"),arg("height")),
+             "Resize a Mapnik Map.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.resize(64,64)\n"
+            )
+
+        .def("scale", &Map::scale,
+             "Return the Map Scale.\n"
+             "Usage:\n"
+             "\n"
+             ">>> m.scale()\n"
+            )
+
+        .def("scale_denominator", &Map::scale_denominator,
+             "Return the Map Scale Denominator.\n"
+             "Usage:\n"
+             "\n"
+             ">>> m.scale_denominator()\n"
+            )
+
+        .def("view_transform",&Map::transform,
+             "Return the map ViewTransform object\n"
+             "which is used internally to convert between\n"
+             "geographic coordinates and screen coordinates.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.view_transform()\n"
+            )
+
+        .def("zoom",&Map::zoom,
+             (arg("factor")),
+             "Zoom in or out by a given factor.\n"
+             "positive number larger than 1, zooms out\n"
+             "positive number smaller than 1, zooms in\n"
+             "\n"
+             "Usage:\n"
+             "\n"
+             ">>> m.zoom(0.25)\n"
+            )
+
+        .def("zoom_all",&Map::zoom_all,
+             "Set the geographical extent of the map\n"
+             "to the combined extents of all active layers.\n"
+             "\n"
+             "Usage:\n"
+             ">>> m.zoom_all()\n"
+            )
+
+        .def("zoom_to_box",&Map::zoom_to_box,
+             (arg("Boxd2")),
+             "Set the geographical extent of the map\n"
+             "by specifying a Mapnik Box2d.\n"
+             "\n"
+             "Usage:\n"
+             ">>> extext = Box2d(-180.0, -90.0, 180.0, 90.0)\n"
+             ">>> m.zoom_to_box(extent)\n"
+            )
+
+        .add_property("parameters",make_function(params_nonconst,return_value_policy<reference_existing_object>()),"TODO")
+
+        .add_property("aspect_fix_mode",
+                      &Map::get_aspect_fix_mode,
+                      &Map::set_aspect_fix_mode,
+                      // TODO - how to add arg info to properties?
+                      //(arg("aspect_fix_mode")),
+                      "Get/Set aspect fix mode.\n"
+                      "Usage:\n"
+                      "\n"
+                      ">>> m.aspect_fix_mode = aspect_fix_mode.GROW_BBOX\n"
+            )
+
+        .add_property("background",make_function
+                      (&Map::background,return_value_policy<copy_const_reference>()),
+                      &Map::set_background,
+                      "The background color of the map (same as background_color property).\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.background = Color('steelblue')\n"
+            )
+
+        .add_property("background_color",make_function
+                      (&Map::background,return_value_policy<copy_const_reference>()),
+                      &Map::set_background,
+                      "The background color of the map.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.background_color = Color('steelblue')\n"
+            )
+
+        .add_property("background_image",make_function
+                      (&Map::background_image,return_value_policy<copy_const_reference>()),
+                      &Map::set_background_image,
+                      "The optional background image of the map.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.background_image = '/path/to/image.png'\n"
+            )
+
+        .add_property("background_image_comp_op",&Map::background_image_comp_op,
+                      &Map::set_background_image_comp_op,
+                      "The background image compositing operation.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.background_image_comp_op = mapnik.CompositeOp.src_over\n"
+            )
+
+        .add_property("background_image_opacity",&Map::background_image_opacity,
+                      &Map::set_background_image_opacity,
+                      "The background image opacity.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.background_image_opacity = 1.0\n"
+            )
+
+        .add_property("base",
+                      make_function(&Map::base_path,return_value_policy<copy_const_reference>()),
+                      &Map::set_base_path,
+                      "The base path of the map where any files using relative \n"
+                      "paths will be interpreted as relative to.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.base_path = '.'\n"
+            )
+
+        .add_property("buffer_size",
+                      &Map::buffer_size,
+                      &Map::set_buffer_size,
+                      "Get/Set the size of buffer around map in pixels.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.buffer_size\n"
+                      "0 # zero by default\n"
+                      ">>> m.buffer_size = 2\n"
+                      ">>> m.buffer_size\n"
+                      "2\n"
+            )
+
+        .add_property("height",
+                      &Map::height,
+                      &Map::set_height,
+                      "Get/Set the height of the map in pixels.\n"
+                      "Minimum settable size is 16 pixels.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.height\n"
+                      "400\n"
+                      ">>> m.height = 600\n"
+                      ">>> m.height\n"
+                      "600\n"
+            )
+
+        .add_property("layers",make_function
+                      (layers_nonconst,return_value_policy<reference_existing_object>()),
+                      "The list of map layers.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.layers\n"
+                      "<mapnik._mapnik.layers object at 0x6d458>"
+                      ">>> m.layers[0]\n"
+                      "<mapnik._mapnik.layer object at 0x5fe130>\n"
+            )
+
+        .add_property("maximum_extent",make_function
+                      (&Map::maximum_extent,return_value_policy<copy_const_reference>()),
+                      &set_maximum_extent,
+                      "The maximum extent of the map.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.maximum_extent = Box2d(-180,-90,180,90)\n"
+            )
+
+        .add_property("srs",
+                      make_function(&Map::srs,return_value_policy<copy_const_reference>()),
+                      &Map::set_srs,
+                      "Spatial reference in Proj.4 format.\n"
+                      "Either an epsg code or proj literal.\n"
+                      "For example, a proj literal:\n"
+                      "\t'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+                      "and a proj epsg code:\n"
+                      "\t'+init=epsg:4326'\n"
+                      "\n"
+                      "Note: using epsg codes requires the installation of\n"
+                      "the Proj.4 'epsg' data file normally found in '/usr/local/share/proj'\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.srs\n"
+                      "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs' # The default srs if not initialized with custom srs\n"
+                      ">>> # set to google mercator with Proj.4 literal\n"
+                      "... \n"
+                      ">>> m.srs = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'\n"
+            )
+
+        .add_property("width",
+                      &Map::width,
+                      &Map::set_width,
+                      "Get/Set the width of the map in pixels.\n"
+                      "Minimum settable size is 16 pixels.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> m.width\n"
+                      "600\n"
+                      ">>> m.width = 800\n"
+                      ">>> m.width\n"
+                      "800\n"
+            )
+        // comparison
+        .def(self == self)
+        ;
+}
diff --git a/src/mapnik_palette.cpp b/src/mapnik_palette.cpp
new file mode 100644
index 0000000..982dbdb
--- /dev/null
+++ b/src/mapnik_palette.cpp
@@ -0,0 +1,70 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+//mapnik
+#include <mapnik/palette.hpp>
+
+// stl
+#include <stdexcept>
+
+static std::shared_ptr<mapnik::rgba_palette> make_palette( std::string const& palette, std::string const& format )
+{
+    mapnik::rgba_palette::palette_type type = mapnik::rgba_palette::PALETTE_RGBA;
+    if (format == "rgb")
+        type = mapnik::rgba_palette::PALETTE_RGB;
+    else if (format == "act")
+        type = mapnik::rgba_palette::PALETTE_ACT;
+    else
+        throw std::runtime_error("invalid type passed for mapnik.Palette: must be either rgba, rgb, or act");
+    return std::make_shared<mapnik::rgba_palette>(palette, type);
+}
+
+void export_palette ()
+{
+    using namespace boost::python;
+
+    class_<mapnik::rgba_palette,
+        std::shared_ptr<mapnik::rgba_palette>,
+        boost::noncopyable >("Palette",no_init)
+        //, init<std::string,std::string>(
+        // ( arg("palette"), arg("type")),
+        // "Creates a new color palette from a file\n"
+        // )
+        .def( "__init__", boost::python::make_constructor(make_palette))
+        .def("to_string", &mapnik::rgba_palette::to_string,
+             "Returns the palette as a string.\n"
+            )
+        ;
+}
diff --git a/src/mapnik_parameters.cpp b/src/mapnik_parameters.cpp
new file mode 100644
index 0000000..febf96a
--- /dev/null
+++ b/src/mapnik_parameters.cpp
@@ -0,0 +1,246 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/debug.hpp>
+#include <mapnik/params.hpp>
+#include <mapnik/unicode.hpp>
+#include <mapnik/value_types.hpp>
+#include <mapnik/value.hpp>
+// stl
+#include <iterator>
+
+using mapnik::parameter;
+using mapnik::parameters;
+
+struct parameter_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const parameter& p)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(p.first,p.second);
+    }
+};
+
+struct parameters_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getstate(const parameters& p)
+    {
+        using namespace boost::python;
+        dict d;
+        parameters::const_iterator pos=p.begin();
+        while(pos!=p.end())
+        {
+            d[pos->first] = pos->second;
+            ++pos;
+        }
+        return boost::python::make_tuple(d);
+    }
+
+    static void setstate(parameters& p, boost::python::tuple state)
+    {
+        using namespace boost::python;
+        if (len(state) != 1)
+        {
+            PyErr_SetObject(PyExc_ValueError,
+                            ("expected 1-item tuple in call to __setstate__; got %s"
+                             % state).ptr()
+                );
+            throw_error_already_set();
+        }
+
+        dict d = extract<dict>(state[0]);
+        boost::python::list keys = d.keys();
+        for (int i=0; i<len(keys); ++i)
+        {
+            std::string key = extract<std::string>(keys[i]);
+            object obj = d[key];
+            extract<std::string> ex0(obj);
+            extract<mapnik::value_integer> ex1(obj);
+            extract<double> ex2(obj);
+            extract<mapnik::value_unicode_string> ex3(obj);
+
+            // TODO - this is never hit - we need proper python string -> std::string to get invoked here
+            if (ex0.check())
+            {
+                p[key] = ex0();
+            }
+            else if (ex1.check())
+            {
+                p[key] = ex1();
+            }
+            else if (ex2.check())
+            {
+                p[key] = ex2();
+            }
+            else if (ex3.check())
+            {
+                std::string buffer;
+                mapnik::to_utf8(ex3(),buffer);
+                p[key] = buffer;
+            }
+            else
+            {
+                MAPNIK_LOG_DEBUG(bindings) << "parameters_pickle_suite: Could not unpickle key=" << key;
+            }
+        }
+    }
+};
+
+
+mapnik::value_holder get_params_by_key1(mapnik::parameters const& p, std::string const& key)
+{
+    parameters::const_iterator pos = p.find(key);
+    if (pos != p.end())
+    {
+        // will be auto-converted to proper python type by `mapnik_params_to_python`
+        return pos->second;
+    }
+    return mapnik::value_null();
+}
+
+mapnik::value_holder get_params_by_key2(mapnik::parameters const& p, std::string const& key)
+{
+    parameters::const_iterator pos = p.find(key);
+    if (pos == p.end())
+    {
+        PyErr_SetString(PyExc_KeyError, key.c_str());
+        boost::python::throw_error_already_set();
+    }
+    // will be auto-converted to proper python type by `mapnik_params_to_python`
+    return pos->second;
+}
+
+mapnik::parameter get_params_by_index(mapnik::parameters const& p, int index)
+{
+    if (index < 0 || static_cast<unsigned>(index) > p.size())
+    {
+        PyErr_SetString(PyExc_IndexError, "Index is out of range");
+        throw boost::python::error_already_set();
+    }
+
+    parameters::const_iterator itr = p.begin();
+    std::advance(itr, index);
+    if (itr != p.end())
+    {
+        return *itr;
+    }
+    PyErr_SetString(PyExc_IndexError, "Index is out of range");
+    throw boost::python::error_already_set();
+}
+
+unsigned get_params_size(mapnik::parameters const& p)
+{
+    return p.size();
+}
+
+void add_parameter(mapnik::parameters & p, mapnik::parameter const& param)
+{
+    p[param.first] = param.second;
+}
+
+mapnik::value_holder get_param(mapnik::parameter const& p, int index)
+{
+    if (index == 0)
+    {
+        return p.first;
+    }
+    else if (index == 1)
+    {
+        return p.second;
+    }
+    else
+    {
+        PyErr_SetString(PyExc_IndexError, "Index is out of range");
+        throw boost::python::error_already_set();
+    }
+}
+
+std::shared_ptr<mapnik::parameter> create_parameter(mapnik::value_unicode_string const& key, mapnik::value_holder const& value)
+{
+    std::string key_utf8;
+    mapnik::to_utf8(key, key_utf8);
+    return std::make_shared<mapnik::parameter>(key_utf8,value);
+}
+
+bool contains(mapnik::parameters const& p, std::string const& key)
+{
+    parameters::const_iterator pos = p.find(key);
+    return pos != p.end();
+}
+
+// needed for Python_Unicode to std::string (utf8) conversion
+
+std::shared_ptr<mapnik::parameter> create_parameter_from_string(mapnik::value_unicode_string const& key, mapnik::value_unicode_string const& ustr)
+{
+    std::string key_utf8;
+    std::string ustr_utf8;
+    mapnik::to_utf8(key, key_utf8);
+    mapnik::to_utf8(ustr,ustr_utf8);
+    return std::make_shared<mapnik::parameter>(key_utf8, ustr_utf8);
+}
+
+void export_parameters()
+{
+    using namespace boost::python;
+    implicitly_convertible<std::string,mapnik::value_holder>();
+    implicitly_convertible<mapnik::value_null,mapnik::value_holder>();
+    implicitly_convertible<mapnik::value_integer,mapnik::value_holder>();
+    implicitly_convertible<mapnik::value_double,mapnik::value_holder>();
+
+    class_<parameter,std::shared_ptr<parameter> >("Parameter",no_init)
+        .def("__init__", make_constructor(create_parameter),
+             "Create a mapnik.Parameter from a pair of values, the first being a string\n"
+             "and the second being either a string, and integer, or a float")
+        .def("__init__", make_constructor(create_parameter_from_string),
+             "Create a mapnik.Parameter from a pair of values, the first being a string\n"
+             "and the second being either a string, and integer, or a float")
+
+        .def_pickle(parameter_pickle_suite())
+        .def("__getitem__",get_param)
+        ;
+
+    class_<parameters>("Parameters",init<>())
+        .def_pickle(parameters_pickle_suite())
+        .def("get",get_params_by_key1)
+        .def("__getitem__",get_params_by_key2)
+        .def("__getitem__",get_params_by_index)
+        .def("__len__",get_params_size)
+        .def("__contains__",contains)
+        .def("append",add_parameter)
+        .def("iteritems",iterator<parameters>())
+        ;
+}
diff --git a/src/mapnik_proj_transform.cpp b/src/mapnik_proj_transform.cpp
new file mode 100644
index 0000000..8f25a90
--- /dev/null
+++ b/src/mapnik_proj_transform.cpp
@@ -0,0 +1,154 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/proj_transform.hpp>
+#include <mapnik/projection.hpp>
+#include <mapnik/coord.hpp>
+#include <mapnik/box2d.hpp>
+
+// stl
+#include <stdexcept>
+
+
+using mapnik::proj_transform;
+using mapnik::projection;
+
+struct proj_transform_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const proj_transform& p)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(p.source(),p.dest());
+    }
+};
+
+namespace  {
+
+mapnik::coord2d forward_transform_c(mapnik::proj_transform& t, mapnik::coord2d const& c)
+{
+    double x = c.x;
+    double y = c.y;
+    double z = 0.0;
+    if (!t.forward(x,y,z)) {
+        std::ostringstream s;
+        s << "Failed to forward project "
+          << "from " << t.source().params() << " to: " << t.dest().params();
+        throw std::runtime_error(s.str());
+    }
+    return mapnik::coord2d(x,y);
+}
+
+mapnik::coord2d backward_transform_c(mapnik::proj_transform& t, mapnik::coord2d const& c)
+{
+    double x = c.x;
+    double y = c.y;
+    double z = 0.0;
+    if (!t.backward(x,y,z)) {
+        std::ostringstream s;
+        s << "Failed to back project "
+          << "from " <<  t.dest().params() << " to: " << t.source().params();
+        throw std::runtime_error(s.str());
+    }
+    return mapnik::coord2d(x,y);
+}
+
+mapnik::box2d<double> forward_transform_env(mapnik::proj_transform& t, mapnik::box2d<double> const & box)
+{
+    mapnik::box2d<double> new_box = box;
+    if (!t.forward(new_box)) {
+        std::ostringstream s;
+        s << "Failed to forward project "
+          << "from " << t.source().params() << " to: " << t.dest().params();
+        throw std::runtime_error(s.str());
+    }
+    return new_box;
+}
+
+mapnik::box2d<double> backward_transform_env(mapnik::proj_transform& t, mapnik::box2d<double> const & box)
+{
+    mapnik::box2d<double> new_box = box;
+    if (!t.backward(new_box)){
+        std::ostringstream s;
+        s << "Failed to back project "
+          << "from " <<  t.dest().params() << " to: " << t.source().params();
+        throw std::runtime_error(s.str());
+    }
+    return new_box;
+}
+
+mapnik::box2d<double> forward_transform_env_p(mapnik::proj_transform& t, mapnik::box2d<double> const & box, unsigned int points)
+{
+    mapnik::box2d<double> new_box = box;
+    if (!t.forward(new_box,points)) {
+        std::ostringstream s;
+        s << "Failed to forward project "
+          << "from " << t.source().params() << " to: " << t.dest().params();
+        throw std::runtime_error(s.str());
+    }
+    return new_box;
+}
+
+mapnik::box2d<double> backward_transform_env_p(mapnik::proj_transform& t, mapnik::box2d<double> const & box, unsigned int points)
+{
+    mapnik::box2d<double> new_box = box;
+    if (!t.backward(new_box,points)){
+        std::ostringstream s;
+        s << "Failed to back project "
+          << "from " <<  t.dest().params() << " to: " << t.source().params();
+        throw std::runtime_error(s.str());
+    }
+    return new_box;
+}
+
+}
+
+void export_proj_transform ()
+{
+    using namespace boost::python;
+
+    class_<proj_transform, boost::noncopyable>("ProjTransform", init< projection const&, projection const& >())
+        .def_pickle(proj_transform_pickle_suite())
+        .def("forward", forward_transform_c)
+        .def("backward",backward_transform_c)
+        .def("forward", forward_transform_env)
+        .def("backward",backward_transform_env)
+        .def("forward", forward_transform_env_p)
+        .def("backward",backward_transform_env_p)
+        ;
+
+}
diff --git a/src/mapnik_projection.cpp b/src/mapnik_projection.cpp
new file mode 100644
index 0000000..d194d56
--- /dev/null
+++ b/src/mapnik_projection.cpp
@@ -0,0 +1,125 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/coord.hpp>
+#include <mapnik/box2d.hpp>
+#include <mapnik/projection.hpp>
+
+using mapnik::projection;
+
+struct projection_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const projection& p)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(p.params());
+    }
+};
+
+namespace {
+mapnik::coord2d forward_pt(mapnik::coord2d const& pt,
+                           mapnik::projection const& prj)
+{
+    double x = pt.x;
+    double y = pt.y;
+    prj.forward(x,y);
+    return mapnik::coord2d(x,y);
+}
+
+mapnik::coord2d inverse_pt(mapnik::coord2d const& pt,
+                           mapnik::projection const& prj)
+{
+    double x = pt.x;
+    double y = pt.y;
+    prj.inverse(x,y);
+    return mapnik::coord2d(x,y);
+}
+
+mapnik::box2d<double> forward_env(mapnik::box2d<double> const & box,
+                                  mapnik::projection const& prj)
+{
+    double minx = box.minx();
+    double miny = box.miny();
+    double maxx = box.maxx();
+    double maxy = box.maxy();
+    prj.forward(minx,miny);
+    prj.forward(maxx,maxy);
+    return mapnik::box2d<double>(minx,miny,maxx,maxy);
+}
+
+mapnik::box2d<double> inverse_env(mapnik::box2d<double> const & box,
+                                  mapnik::projection const& prj)
+{
+    double minx = box.minx();
+    double miny = box.miny();
+    double maxx = box.maxx();
+    double maxy = box.maxy();
+    prj.inverse(minx,miny);
+    prj.inverse(maxx,maxy);
+    return mapnik::box2d<double>(minx,miny,maxx,maxy);
+}
+
+}
+
+void export_projection ()
+{
+    using namespace boost::python;
+
+    class_<projection>("Projection", "Represents a map projection.",init<std::string const&>(
+                           (arg("proj4_string")),
+                           "Constructs a new projection from its PROJ.4 string representation.\n"
+                           "\n"
+                           "The constructor will throw a RuntimeError in case the projection\n"
+                           "cannot be initialized.\n"
+                           )
+        )
+        .def_pickle(projection_pickle_suite())
+        .def ("params", make_function(&projection::params,
+                                      return_value_policy<copy_const_reference>()),
+              "Returns the PROJ.4 string for this projection.\n")
+        .def ("expanded",&projection::expanded,
+              "normalize PROJ.4 definition by expanding +init= syntax\n")
+        .add_property ("geographic", &projection::is_geographic,
+                       "This property is True if the projection is a geographic projection\n"
+                       "(i.e. it uses lon/lat coordinates)\n")
+        ;
+
+    def("forward_",&forward_pt);
+    def("inverse_",&inverse_pt);
+    def("forward_",&forward_env);
+    def("inverse_",&inverse_env);
+
+}
diff --git a/src/mapnik_python.cpp b/src/mapnik_python.cpp
new file mode 100644
index 0000000..b4c4c3b
--- /dev/null
+++ b/src/mapnik_python.cpp
@@ -0,0 +1,1072 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include "python_to_value.hpp"
+#include <boost/python/args.hpp>        // for keywords, arg, etc
+#include <boost/python/converter/from_python.hpp>
+#include <boost/python/def.hpp>         // for def
+#include <boost/python/detail/defaults_gen.hpp>
+#include <boost/python/detail/none.hpp>  // for none
+#include <boost/python/dict.hpp>        // for dict
+#include <boost/python/exception_translator.hpp>
+#include <boost/python/list.hpp>        // for list
+#include <boost/python/module.hpp>      // for BOOST_PYTHON_MODULE
+#include <boost/python/object_core.hpp>  // for get_managed_object
+#include <boost/python/register_ptr_to_python.hpp>
+#include <boost/python/to_python_converter.hpp>
+#pragma GCC diagnostic pop
+
+// stl
+#include <stdexcept>
+#include <fstream>
+
+void export_color();
+void export_coord();
+void export_layer();
+void export_parameters();
+void export_envelope();
+void export_query();
+void export_geometry();
+void export_palette();
+void export_image();
+void export_image_view();
+void export_gamma_method();
+void export_scaling_method();
+#if defined(GRID_RENDERER)
+void export_grid();
+void export_grid_view();
+#endif
+void export_map();
+void export_python();
+void export_expression();
+void export_rule();
+void export_style();
+void export_feature();
+void export_featureset();
+void export_fontset();
+void export_datasource();
+void export_datasource_cache();
+void export_symbolizer();
+void export_markers_symbolizer();
+void export_point_symbolizer();
+void export_line_symbolizer();
+void export_line_pattern_symbolizer();
+void export_polygon_symbolizer();
+void export_building_symbolizer();
+void export_polygon_pattern_symbolizer();
+void export_raster_symbolizer();
+void export_text_placement();
+void export_shield_symbolizer();
+void export_debug_symbolizer();
+void export_group_symbolizer();
+void export_font_engine();
+void export_projection();
+void export_proj_transform();
+void export_view_transform();
+void export_raster_colorizer();
+void export_label_collision_detector();
+void export_logger();
+
+#include <mapnik/version.hpp>
+#include <mapnik/map.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/layer.hpp>
+#include <mapnik/agg_renderer.hpp>
+#include <mapnik/rule.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/image_any.hpp>
+#include <mapnik/load_map.hpp>
+#include <mapnik/value_error.hpp>
+#include <mapnik/save_map.hpp>
+#include <mapnik/scale_denominator.hpp>
+#if defined(GRID_RENDERER)
+#include "python_grid_utils.hpp"
+#endif
+#include "mapnik_value_converter.hpp"
+#include "mapnik_enumeration_wrapper_converter.hpp"
+#include "mapnik_threads.hpp"
+#include "python_optional.hpp"
+#include <mapnik/marker_cache.hpp>
+#if defined(SHAPE_MEMORY_MAPPED_FILE)
+#include <mapnik/mapped_memory_cache.hpp>
+#endif
+
+#if defined(SVG_RENDERER)
+#include <mapnik/svg/output/svg_renderer.hpp>
+#endif
+
+namespace mapnik {
+    class font_set;
+    class layer;
+    class color;
+    class label_collision_detector4;
+}
+void clear_cache()
+{
+    mapnik::marker_cache::instance().clear();
+#if defined(SHAPE_MEMORY_MAPPED_FILE)
+    mapnik::mapped_memory_cache::instance().clear();
+#endif
+}
+
+#if defined(HAVE_CAIRO)
+#include <mapnik/cairo_io.hpp>
+#include <mapnik/cairo/cairo_renderer.hpp>
+#include <cairo.h>
+#endif
+
+#if defined(HAVE_PYCAIRO)
+#include <boost/python/type_id.hpp>
+#include <boost/python/converter/registry.hpp>
+#include <pycairo.h>
+static Pycairo_CAPI_t *Pycairo_CAPI;
+static void *extract_surface(PyObject* op)
+{
+    if (PyObject_TypeCheck(op, const_cast<PyTypeObject*>(Pycairo_CAPI->Surface_Type)))
+    {
+        return op;
+    }
+    else
+    {
+        return 0;
+    }
+}
+
+static void *extract_context(PyObject* op)
+{
+    if (PyObject_TypeCheck(op, const_cast<PyTypeObject*>(Pycairo_CAPI->Context_Type)))
+    {
+        return op;
+    }
+    else
+    {
+        return 0;
+    }
+}
+
+void register_cairo()
+{
+#if PY_MAJOR_VERSION >= 3
+    Pycairo_CAPI = (Pycairo_CAPI_t*) PyCapsule_Import(const_cast<char *>("cairo.CAPI"), 0);
+#else
+    Pycairo_CAPI = (Pycairo_CAPI_t*) PyCObject_Import(const_cast<char *>("cairo"), const_cast<char *>("CAPI"));
+#endif
+    if (Pycairo_CAPI == nullptr) return;
+
+    boost::python::converter::registry::insert(&extract_surface, boost::python::type_id<PycairoSurface>());
+    boost::python::converter::registry::insert(&extract_context, boost::python::type_id<PycairoContext>());
+}
+#endif
+
+using mapnik::python_thread;
+using mapnik::python_unblock_auto_block;
+#ifdef MAPNIK_DEBUG
+bool python_thread::thread_support = true;
+#endif
+boost::thread_specific_ptr<PyThreadState> python_thread::state;
+
+struct agg_renderer_visitor_1
+{
+    agg_renderer_visitor_1(mapnik::Map const& m, double scale_factor, unsigned offset_x, unsigned offset_y)
+        : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {}
+
+    template <typename T>
+    void operator() (T & pixmap)
+    {
+        throw std::runtime_error("This image type is not currently supported for rendering.");
+    }
+
+  private:
+    mapnik::Map const& m_;
+    double scale_factor_;
+    unsigned offset_x_;
+    unsigned offset_y_;
+};
+
+template <>
+void agg_renderer_visitor_1::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+    mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,pixmap,scale_factor_,offset_x_, offset_y_);
+    ren.apply();
+}
+
+struct agg_renderer_visitor_2
+{
+    agg_renderer_visitor_2(mapnik::Map const &m, std::shared_ptr<mapnik::label_collision_detector4> detector,
+                 double scale_factor, unsigned offset_x, unsigned offset_y)
+        : m_(m), detector_(detector), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {}
+
+    template <typename T>
+    void operator() (T & pixmap)
+    {
+        throw std::runtime_error("This image type is not currently supported for rendering.");
+    }
+
+  private:
+    mapnik::Map const& m_;
+    std::shared_ptr<mapnik::label_collision_detector4> detector_;
+    double scale_factor_;
+    unsigned offset_x_;
+    unsigned offset_y_;
+};
+
+template <>
+void agg_renderer_visitor_2::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+    mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,pixmap,detector_, scale_factor_,offset_x_, offset_y_);
+    ren.apply();
+}
+
+struct agg_renderer_visitor_3
+{
+    agg_renderer_visitor_3(mapnik::Map const& m, mapnik::request const& req, mapnik::attributes const& vars,
+                 double scale_factor, unsigned offset_x, unsigned offset_y)
+        : m_(m), req_(req), vars_(vars), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {}
+
+    template <typename T>
+    void operator() (T & pixmap)
+    {
+        throw std::runtime_error("This image type is not currently supported for rendering.");
+    }
+
+  private:
+    mapnik::Map const& m_;
+    mapnik::request const& req_;
+    mapnik::attributes const& vars_;
+    double scale_factor_;
+    unsigned offset_x_;
+    unsigned offset_y_;
+
+};
+
+template <>
+void agg_renderer_visitor_3::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+    mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,req_, vars_, pixmap, scale_factor_, offset_x_, offset_y_);
+    ren.apply();
+}
+
+struct agg_renderer_visitor_4
+{
+    agg_renderer_visitor_4(mapnik::Map const& m, double scale_factor, unsigned offset_x, unsigned offset_y,
+                 mapnik::layer const& layer, std::set<std::string>& names)
+        : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y),
+          layer_(layer), names_(names) {}
+
+    template <typename T>
+    void operator() (T & pixmap)
+    {
+        throw std::runtime_error("This image type is not currently supported for rendering.");
+    }
+
+  private:
+    mapnik::Map const& m_;
+    double scale_factor_;
+    unsigned offset_x_;
+    unsigned offset_y_;
+    mapnik::layer const& layer_;
+    std::set<std::string> & names_;
+};
+
+template <>
+void agg_renderer_visitor_4::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+    mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,pixmap,scale_factor_,offset_x_, offset_y_);
+    ren.apply(layer_, names_);
+}
+
+
+void render(mapnik::Map const& map,
+            mapnik::image_any& image,
+            double scale_factor = 1.0,
+            unsigned offset_x = 0u,
+            unsigned offset_y = 0u)
+{
+    python_unblock_auto_block b;
+    mapnik::util::apply_visitor(agg_renderer_visitor_1(map, scale_factor, offset_x, offset_y), image);
+}
+
+void render_with_vars(mapnik::Map const& map,
+            mapnik::image_any& image,
+            boost::python::dict const& d,
+            double scale_factor = 1.0,
+            unsigned offset_x = 0u,
+            unsigned offset_y = 0u)
+{
+    mapnik::attributes vars = mapnik::dict2attr(d);
+    mapnik::request req(map.width(),map.height(),map.get_current_extent());
+    req.set_buffer_size(map.buffer_size());
+    python_unblock_auto_block b;
+    mapnik::util::apply_visitor(agg_renderer_visitor_3(map, req, vars, scale_factor, offset_x, offset_y), image);
+}
+
+void render_with_detector(
+    mapnik::Map const& map,
+    mapnik::image_any &image,
+    std::shared_ptr<mapnik::label_collision_detector4> detector,
+    double scale_factor = 1.0,
+    unsigned offset_x = 0u,
+    unsigned offset_y = 0u)
+{
+    python_unblock_auto_block b;
+    mapnik::util::apply_visitor(agg_renderer_visitor_2(map, detector, scale_factor, offset_x, offset_y), image);
+}
+
+void render_layer2(mapnik::Map const& map,
+                   mapnik::image_any& image,
+                   unsigned layer_idx,
+                   double scale_factor,
+                   unsigned offset_x,
+                   unsigned offset_y)
+{
+    std::vector<mapnik::layer> const& layers = map.layers();
+    std::size_t layer_num = layers.size();
+    if (layer_idx >= layer_num) {
+        std::ostringstream s;
+        s << "Zero-based layer index '" << layer_idx << "' not valid, only '"
+          << layer_num << "' layers are in map\n";
+        throw std::runtime_error(s.str());
+    }
+
+    python_unblock_auto_block b;
+    mapnik::layer const& layer = layers[layer_idx];
+    std::set<std::string> names;
+    mapnik::util::apply_visitor(agg_renderer_visitor_4(map, scale_factor, offset_x, offset_y, layer, names), image);
+}
+
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+
+void render3(mapnik::Map const& map,
+             PycairoSurface* py_surface,
+             double scale_factor = 1.0,
+             unsigned offset_x = 0,
+             unsigned offset_y = 0)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,mapnik::create_context(surface),scale_factor,offset_x,offset_y);
+    ren.apply();
+}
+
+void render4(mapnik::Map const& map, PycairoSurface* py_surface)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,mapnik::create_context(surface));
+    ren.apply();
+}
+
+void render5(mapnik::Map const& map,
+             PycairoContext* py_context,
+             double scale_factor = 1.0,
+             unsigned offset_x = 0,
+             unsigned offset_y = 0)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context,scale_factor,offset_x, offset_y);
+    ren.apply();
+}
+
+void render6(mapnik::Map const& map, PycairoContext* py_context)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context);
+    ren.apply();
+}
+void render_with_detector2(
+    mapnik::Map const& map,
+    PycairoContext* py_context,
+    std::shared_ptr<mapnik::label_collision_detector4> detector)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context,detector);
+    ren.apply();
+}
+
+void render_with_detector3(
+    mapnik::Map const& map,
+    PycairoContext* py_context,
+    std::shared_ptr<mapnik::label_collision_detector4> detector,
+    double scale_factor = 1.0,
+    unsigned offset_x = 0u,
+    unsigned offset_y = 0u)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context,detector,scale_factor,offset_x,offset_y);
+    ren.apply();
+}
+
+void render_with_detector4(
+    mapnik::Map const& map,
+    PycairoSurface* py_surface,
+    std::shared_ptr<mapnik::label_collision_detector4> detector)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map, mapnik::create_context(surface), detector);
+    ren.apply();
+}
+
+void render_with_detector5(
+    mapnik::Map const& map,
+    PycairoSurface* py_surface,
+    std::shared_ptr<mapnik::label_collision_detector4> detector,
+    double scale_factor = 1.0,
+    unsigned offset_x = 0u,
+    unsigned offset_y = 0u)
+{
+    python_unblock_auto_block b;
+    mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+    mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map, mapnik::create_context(surface), detector, scale_factor, offset_x, offset_y);
+    ren.apply();
+}
+
+#endif
+
+
+void render_tile_to_file(mapnik::Map const& map,
+                         unsigned offset_x, unsigned offset_y,
+                         unsigned width, unsigned height,
+                         std::string const& file,
+                         std::string const& format)
+{
+    mapnik::image_any image(width,height);
+    render(map,image,1.0,offset_x, offset_y);
+    mapnik::save_to_file(image,file,format);
+}
+
+void render_to_file1(mapnik::Map const& map,
+                     std::string const& filename,
+                     std::string const& format)
+{
+    if (format == "svg-ng")
+    {
+#if defined(SVG_RENDERER)
+        std::ofstream file (filename.c_str(), std::ios::out|std::ios::trunc|std::ios::binary);
+        if (!file)
+        {
+            throw mapnik::image_writer_exception("could not open file for writing: " + filename);
+        }
+        using iter_type = std::ostream_iterator<char>;
+        iter_type output_stream_iterator(file);
+        mapnik::svg_renderer<iter_type> ren(map,output_stream_iterator);
+        ren.apply();
+#else
+        throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format);
+#endif
+    }
+    else if (format == "pdf" || format == "svg" || format =="ps" || format == "ARGB32" || format == "RGB24")
+    {
+#if defined(HAVE_CAIRO)
+        mapnik::save_to_cairo_file(map,filename,format,1.0);
+#else
+        throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format);
+#endif
+    }
+    else
+    {
+        mapnik::image_any image(map.width(),map.height());
+        render(map,image,1.0,0,0);
+        mapnik::save_to_file(image,filename,format);
+    }
+}
+
+void render_to_file2(mapnik::Map const& map,std::string const& filename)
+{
+    std::string format = mapnik::guess_type(filename);
+    if (format == "pdf" || format == "svg" || format =="ps")
+    {
+#if defined(HAVE_CAIRO)
+        mapnik::save_to_cairo_file(map,filename,format,1.0);
+#else
+        throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format);
+#endif
+    }
+    else
+    {
+        mapnik::image_any image(map.width(),map.height());
+        render(map,image,1.0,0,0);
+        mapnik::save_to_file(image,filename);
+    }
+}
+
+void render_to_file3(mapnik::Map const& map,
+                     std::string const& filename,
+                     std::string const& format,
+                     double scale_factor = 1.0
+    )
+{
+    if (format == "svg-ng")
+    {
+#if defined(SVG_RENDERER)
+        std::ofstream file (filename.c_str(), std::ios::out|std::ios::trunc|std::ios::binary);
+        if (!file)
+        {
+            throw mapnik::image_writer_exception("could not open file for writing: " + filename);
+        }
+        using iter_type = std::ostream_iterator<char>;
+        iter_type output_stream_iterator(file);
+        mapnik::svg_renderer<iter_type> ren(map,output_stream_iterator,scale_factor);
+        ren.apply();
+#else
+        throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format);
+#endif
+    }
+    else if (format == "pdf" || format == "svg" || format =="ps" || format == "ARGB32" || format == "RGB24")
+    {
+#if defined(HAVE_CAIRO)
+        mapnik::save_to_cairo_file(map,filename,format,scale_factor);
+#else
+        throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format);
+#endif
+    }
+    else
+    {
+        mapnik::image_any image(map.width(),map.height());
+        render(map,image,scale_factor,0,0);
+        mapnik::save_to_file(image,filename,format);
+    }
+}
+
+double scale_denominator(mapnik::Map const& map, bool geographic)
+{
+    return mapnik::scale_denominator(map.scale(), geographic);
+}
+
+// http://docs.python.org/c-api/exceptions.html#standard-exceptions
+void value_error_translator(mapnik::value_error const & ex)
+{
+    PyErr_SetString(PyExc_ValueError, ex.what());
+}
+
+void runtime_error_translator(std::runtime_error const & ex)
+{
+    PyErr_SetString(PyExc_RuntimeError, ex.what());
+}
+
+void out_of_range_error_translator(std::out_of_range const & ex)
+{
+    PyErr_SetString(PyExc_IndexError, ex.what());
+}
+
+void standard_error_translator(std::exception const & ex)
+{
+    PyErr_SetString(PyExc_RuntimeError, ex.what());
+}
+
+unsigned mapnik_version()
+{
+    return MAPNIK_VERSION;
+}
+
+std::string mapnik_version_string()
+{
+    return MAPNIK_VERSION_STRING;
+}
+
+bool has_proj4()
+{
+#if defined(MAPNIK_USE_PROJ4)
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool has_svg_renderer()
+{
+#if defined(SVG_RENDERER)
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool has_grid_renderer()
+{
+#if defined(GRID_RENDERER)
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool has_jpeg()
+{
+#if defined(HAVE_JPEG)
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool has_png()
+{
+#if defined(HAVE_PNG)
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool has_tiff()
+{
+#if defined(HAVE_TIFF)
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool has_webp()
+{
+#if defined(HAVE_WEBP)
+    return true;
+#else
+    return false;
+#endif
+}
+
+// indicator for cairo rendering support inside libmapnik
+bool has_cairo()
+{
+#if defined(HAVE_CAIRO)
+    return true;
+#else
+    return false;
+#endif
+}
+
+// indicator for pycairo support in the python bindings
+bool has_pycairo()
+{
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+#if PY_MAJOR_VERSION >= 3
+    Pycairo_CAPI = (Pycairo_CAPI_t*) PyCapsule_Import(const_cast<char *>("cairo.CAPI"), 0);
+#else
+    Pycairo_CAPI = (Pycairo_CAPI_t*) PyCObject_Import(const_cast<char *>("cairo"), const_cast<char *>("CAPI"));
+#endif
+    if (Pycairo_CAPI == nullptr){
+        /*
+          Case where pycairo support has been compiled into
+          mapnik but at runtime the cairo python module
+          is unable to be imported and therefore Pycairo surfaces
+          and contexts cannot be passed to mapnik.render()
+        */
+        return false;
+    }
+    return true;
+#else
+    return false;
+#endif
+}
+
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+BOOST_PYTHON_FUNCTION_OVERLOADS(load_map_overloads, load_map, 2, 4)
+BOOST_PYTHON_FUNCTION_OVERLOADS(load_map_string_overloads, load_map_string, 2, 4)
+BOOST_PYTHON_FUNCTION_OVERLOADS(save_map_overloads, save_map, 2, 3)
+BOOST_PYTHON_FUNCTION_OVERLOADS(save_map_to_string_overloads, save_map_to_string, 1, 2)
+BOOST_PYTHON_FUNCTION_OVERLOADS(render_overloads, render, 2, 5)
+BOOST_PYTHON_FUNCTION_OVERLOADS(render_with_detector_overloads, render_with_detector, 3, 6)
+#pragma GCC diagnostic pop
+
+BOOST_PYTHON_MODULE(_mapnik)
+{
+
+    using namespace boost::python;
+
+    using mapnik::load_map;
+    using mapnik::load_map_string;
+    using mapnik::save_map;
+    using mapnik::save_map_to_string;
+
+    register_exception_translator<std::exception>(&standard_error_translator);
+    register_exception_translator<std::out_of_range>(&out_of_range_error_translator);
+    register_exception_translator<mapnik::value_error>(&value_error_translator);
+    register_exception_translator<std::runtime_error>(&runtime_error_translator);
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+    register_cairo();
+#endif
+    export_query();
+    export_geometry();
+    export_feature();
+    export_featureset();
+    export_fontset();
+    export_datasource();
+    export_parameters();
+    export_color();
+    export_envelope();
+    export_palette();
+    export_image();
+    export_image_view();
+    export_gamma_method();
+    export_scaling_method();
+#if defined(GRID_RENDERER)
+    export_grid();
+    export_grid_view();
+#endif
+    export_expression();
+    export_rule();
+    export_style();
+    export_layer();
+    export_datasource_cache();
+    export_symbolizer();
+    export_markers_symbolizer();
+    export_point_symbolizer();
+    export_line_symbolizer();
+    export_line_pattern_symbolizer();
+    export_polygon_symbolizer();
+    export_building_symbolizer();
+    export_polygon_pattern_symbolizer();
+    export_raster_symbolizer();
+    export_text_placement();
+    export_shield_symbolizer();
+    export_debug_symbolizer();
+    export_group_symbolizer();
+    export_font_engine();
+    export_projection();
+    export_proj_transform();
+    export_view_transform();
+    export_coord();
+    export_map();
+    export_raster_colorizer();
+    export_label_collision_detector();
+    export_logger();
+
+    def("clear_cache", &clear_cache,
+        "\n"
+        "Clear all global caches of markers and mapped memory regions.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import clear_cache\n"
+        ">>> clear_cache()\n"
+        );
+
+    def("render_to_file",&render_to_file1,
+        "\n"
+        "Render Map to file using explicit image type.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render_to_file, load_map\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> render_to_file(m,'image32bit.png','png')\n"
+        "\n"
+        "8 bit (paletted) PNG can be requested with 'png256':\n"
+        ">>> render_to_file(m,'8bit_image.png','png256')\n"
+        "\n"
+        "JPEG quality can be controlled by adding a suffix to\n"
+        "'jpeg' between 0 and 100 (default is 85):\n"
+        ">>> render_to_file(m,'top_quality.jpeg','jpeg100')\n"
+        ">>> render_to_file(m,'medium_quality.jpeg','jpeg50')\n"
+        );
+
+    def("render_to_file",&render_to_file2,
+        "\n"
+        "Render Map to file (type taken from file extension)\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render_to_file, load_map\n"
+        ">>> m = Map(256,256)\n"
+        ">>> render_to_file(m,'image.jpeg')\n"
+        "\n"
+        );
+
+    def("render_to_file",&render_to_file3,
+        "\n"
+        "Render Map to file using explicit image type and scale factor.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render_to_file, load_map\n"
+        ">>> m = Map(256,256)\n"
+        ">>> scale_factor = 4\n"
+        ">>> render_to_file(m,'image.jpeg',scale_factor)\n"
+        "\n"
+        );
+
+    def("render_tile_to_file",&render_tile_to_file,
+        "\n"
+        "TODO\n"
+        "\n"
+        );
+
+    def("render_with_vars",&render_with_vars,
+        (arg("map"),
+         arg("image"),
+         arg("vars"),
+         arg("scale_factor")=1.0,
+         arg("offset_x")=0,
+         arg("offset_y")=0
+        )
+        );
+
+    def("render", &render, render_overloads(
+            "\n"
+            "Render Map to an AGG image_any using offsets\n"
+            "\n"
+            "Usage:\n"
+            ">>> from mapnik import Map, Image, render, load_map\n"
+            ">>> m = Map(256,256)\n"
+            ">>> load_map(m,'mapfile.xml')\n"
+            ">>> im = Image(m.width,m.height)\n"
+            ">>> scale_factor=2.0\n"
+            ">>> offset = [100,50]\n"
+            ">>> render(m,im)\n"
+            ">>> render(m,im,scale_factor)\n"
+            ">>> render(m,im,scale_factor,offset[0],offset[1])\n"
+            "\n"
+            ));
+
+    def("render_with_detector", &render_with_detector, render_with_detector_overloads(
+            "\n"
+            "Render Map to an AGG image_any using a pre-constructed detector.\n"
+            "\n"
+            "Usage:\n"
+            ">>> from mapnik import Map, Image, LabelCollisionDetector, render_with_detector, load_map\n"
+            ">>> m = Map(256,256)\n"
+            ">>> load_map(m,'mapfile.xml')\n"
+            ">>> im = Image(m.width,m.height)\n"
+            ">>> detector = LabelCollisionDetector(m)\n"
+            ">>> render_with_detector(m, im, detector)\n"
+            ));
+
+    def("render_layer", &render_layer2,
+        (arg("map"),
+         arg("image"),
+         arg("layer"),
+         arg("scale_factor")=1.0,
+         arg("offset_x")=0,
+         arg("offset_y")=0
+        )
+        );
+
+#if defined(GRID_RENDERER)
+    def("render_layer", &mapnik::render_layer_for_grid,
+        (arg("map"),
+         arg("grid"),
+         arg("layer"),
+         arg("fields")=boost::python::list(),
+         arg("scale_factor")=1.0,
+         arg("offset_x")=0,
+         arg("offset_y")=0
+        )
+        );
+#endif
+
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+    def("render",&render3,
+        "\n"
+        "Render Map to Cairo Surface using offsets\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render, load_map\n"
+        ">>> from cairo import SVGSurface\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> render(m,surface,1,1)\n"
+        "\n"
+        );
+
+    def("render",&render4,
+        "\n"
+        "Render Map to Cairo Surface\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render, load_map\n"
+        ">>> from cairo import SVGSurface\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> render(m,surface)\n"
+        "\n"
+        );
+
+    def("render",&render5,
+        "\n"
+        "Render Map to Cairo Context using offsets\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render, load_map\n"
+        ">>> from cairo import SVGSurface, Context\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> ctx = Context(surface)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> render(m,context,1,1)\n"
+        "\n"
+        );
+
+    def("render",&render6,
+        "\n"
+        "Render Map to Cairo Context\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, render, load_map\n"
+        ">>> from cairo import SVGSurface, Context\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> ctx = Context(surface)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> render(m,context)\n"
+        "\n"
+        );
+
+    def("render_with_detector", &render_with_detector2,
+        "\n"
+        "Render Map to Cairo Context using a pre-constructed detector.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+        ">>> from cairo import SVGSurface, Context\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> ctx = Context(surface)\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> detector = LabelCollisionDetector(m)\n"
+        ">>> render_with_detector(m, ctx, detector)\n"
+        );
+
+    def("render_with_detector", &render_with_detector3,
+        "\n"
+        "Render Map to Cairo Context using a pre-constructed detector, scale and offsets.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+        ">>> from cairo import SVGSurface, Context\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> ctx = Context(surface)\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> detector = LabelCollisionDetector(m)\n"
+        ">>> render_with_detector(m, ctx, detector, 1, 1, 1)\n"
+        );
+
+    def("render_with_detector", &render_with_detector4,
+        "\n"
+        "Render Map to Cairo Surface using a pre-constructed detector.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+        ">>> from cairo import SVGSurface, Context\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> detector = LabelCollisionDetector(m)\n"
+        ">>> render_with_detector(m, surface, detector)\n"
+        );
+
+    def("render_with_detector", &render_with_detector5,
+        "\n"
+        "Render Map to Cairo Surface using a pre-constructed detector, scale and offsets.\n"
+        "\n"
+        "Usage:\n"
+        ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+        ">>> from cairo import SVGSurface, Context\n"
+        ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> detector = LabelCollisionDetector(m)\n"
+        ">>> render_with_detector(m, surface, detector, 1, 1, 1)\n"
+        );
+
+#endif
+
+    def("scale_denominator", &scale_denominator,
+        (arg("map"),arg("is_geographic")),
+        "\n"
+        "Return the Map Scale Denominator.\n"
+        "Also available as Map.scale_denominator()\n"
+        "\n"
+        "Usage:\n"
+        "\n"
+        ">>> from mapnik import Map, Projection, scale_denominator, load_map\n"
+        ">>> m = Map(256,256)\n"
+        ">>> load_map(m,'mapfile.xml')\n"
+        ">>> scale_denominator(m,Projection(m.srs).geographic)\n"
+        "\n"
+        );
+
+    def("load_map", &load_map, load_map_overloads());
+
+    def("load_map_from_string", &load_map_string, load_map_string_overloads());
+
+    def("save_map", &save_map, save_map_overloads());
+/*
+  "\n"
+  "Save Map object to XML file\n"
+  "\n"
+  "Usage:\n"
+  ">>> from mapnik import Map, load_map, save_map\n"
+  ">>> m = Map(256,256)\n"
+  ">>> load_map(m,'mapfile_wgs84.xml')\n"
+  ">>> m.srs\n"
+  "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+  ">>> m.srs = '+init=espg:3395'\n"
+  ">>> save_map(m,'mapfile_mercator.xml')\n"
+  "\n"
+  );
+*/
+
+    def("save_map_to_string", &save_map_to_string, save_map_to_string_overloads());
+    def("mapnik_version", &mapnik_version,"Get the Mapnik version number");
+    def("mapnik_version_string", &mapnik_version_string,"Get the Mapnik version string");
+    def("has_proj4", &has_proj4, "Get proj4 status");
+    def("has_jpeg", &has_jpeg, "Get jpeg read/write support status");
+    def("has_png", &has_png, "Get png read/write support status");
+    def("has_tiff", &has_tiff, "Get tiff read/write support status");
+    def("has_webp", &has_webp, "Get webp read/write support status");
+    def("has_svg_renderer", &has_svg_renderer, "Get svg_renderer status");
+    def("has_grid_renderer", &has_grid_renderer, "Get grid_renderer status");
+    def("has_cairo", &has_cairo, "Get cairo library status");
+    def("has_pycairo", &has_pycairo, "Get pycairo module status");
+
+    python_optional<mapnik::font_set>();
+    python_optional<mapnik::color>();
+    python_optional<mapnik::box2d<double> >();
+    python_optional<mapnik::composite_mode_e>();
+    python_optional<mapnik::datasource_geometry_t>();
+    python_optional<std::string>();
+    python_optional<unsigned>();
+    python_optional<double>();
+    python_optional<float>();
+    python_optional<bool>();
+    python_optional<int>();
+    python_optional<mapnik::text_transform_e>();
+    register_ptr_to_python<mapnik::expression_ptr>();
+    register_ptr_to_python<mapnik::path_expression_ptr>();
+    to_python_converter<mapnik::value_holder,mapnik_param_to_python>();
+    to_python_converter<mapnik::value,mapnik_value_to_python>();
+    to_python_converter<mapnik::enumeration_wrapper,mapnik_enumeration_wrapper_to_python>();
+}
diff --git a/src/mapnik_query.cpp b/src/mapnik_query.cpp
new file mode 100644
index 0000000..0172abe
--- /dev/null
+++ b/src/mapnik_query.cpp
@@ -0,0 +1,107 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include "python_to_value.hpp"
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/query.hpp>
+#include <mapnik/box2d.hpp>
+
+#include <string>
+#include <set>
+
+using mapnik::query;
+using mapnik::box2d;
+
+namespace python = boost::python;
+
+struct resolution_to_tuple
+{
+    static PyObject* convert(query::resolution_type const& x)
+    {
+        python::object tuple(python::make_tuple(std::get<0>(x), std::get<1>(x)));
+        return python::incref(tuple.ptr());
+    }
+
+    static PyTypeObject const* get_pytype()
+    {
+        return &PyTuple_Type;
+    }
+};
+
+struct names_to_list
+{
+    static PyObject* convert(std::set<std::string> const& names)
+    {
+        boost::python::list l;
+        for ( std::string const& name : names )
+        {
+            l.append(name);
+        }
+        return python::incref(l.ptr());
+    }
+
+    static PyTypeObject const* get_pytype()
+    {
+        return &PyList_Type;
+    }
+};
+
+namespace {
+
+    void set_variables(mapnik::query & q, boost::python::dict const& d)
+    {
+        mapnik::attributes vars = mapnik::dict2attr(d);
+        q.set_variables(vars);
+    }
+}
+
+void export_query()
+{
+    using namespace boost::python;
+
+    to_python_converter<query::resolution_type, resolution_to_tuple> ();
+    to_python_converter<std::set<std::string>, names_to_list> ();
+
+    class_<query>("Query", "a spatial query data object",
+                  init<box2d<double>,query::resolution_type const&,double>() )
+        .def(init<box2d<double> >())
+        .add_property("resolution",make_function(&query::resolution,
+                                                 return_value_policy<copy_const_reference>()))
+        .add_property("bbox", make_function(&query::get_bbox,
+                                            return_value_policy<copy_const_reference>()) )
+        .add_property("property_names", make_function(&query::property_names,
+                                                      return_value_policy<copy_const_reference>()) )
+        .def("add_property_name", &query::add_property_name)
+        .def("set_variables",&set_variables);
+}
diff --git a/src/mapnik_raster_colorizer.cpp b/src/mapnik_raster_colorizer.cpp
new file mode 100644
index 0000000..833ba6f
--- /dev/null
+++ b/src/mapnik_raster_colorizer.cpp
@@ -0,0 +1,241 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/raster_colorizer.hpp>
+#include <mapnik/symbolizer.hpp>
+
+using mapnik::raster_colorizer;
+using mapnik::raster_colorizer_ptr;
+using mapnik::symbolizer_base;
+using mapnik::colorizer_stop;
+using mapnik::colorizer_stops;
+using mapnik::colorizer_mode_enum;
+using mapnik::color;
+using mapnik::COLORIZER_INHERIT;
+using mapnik::COLORIZER_LINEAR;
+using mapnik::COLORIZER_DISCRETE;
+using mapnik::COLORIZER_EXACT;
+
+
+namespace {
+void add_stop(raster_colorizer_ptr & rc, colorizer_stop & stop)
+{
+    rc->add_stop(stop);
+}
+
+void add_stop2(raster_colorizer_ptr & rc, float v)
+{
+    colorizer_stop stop(v, rc->get_default_mode(), rc->get_default_color());
+    rc->add_stop(stop);
+}
+
+void add_stop3(raster_colorizer_ptr &rc, float v, color c)
+{
+    colorizer_stop stop(v, rc->get_default_mode(), c);
+    rc->add_stop(stop);
+}
+
+void add_stop4(raster_colorizer_ptr &rc, float v, colorizer_mode_enum m)
+{
+    colorizer_stop stop(v, m, rc->get_default_color());
+    rc->add_stop(stop);
+}
+
+void add_stop5(raster_colorizer_ptr &rc, float v, colorizer_mode_enum m, color c)
+{
+    colorizer_stop stop(v, m, c);
+    rc->add_stop(stop);
+}
+
+mapnik::color get_color(raster_colorizer_ptr &rc, float value)
+{
+    unsigned rgba = rc->get_color(value);
+    unsigned r = (rgba & 0xff);
+    unsigned g = (rgba >> 8 ) & 0xff;
+    unsigned b = (rgba >> 16) & 0xff;
+    unsigned a = (rgba >> 24) & 0xff;
+    return mapnik::color(r,g,b,a);
+}
+
+colorizer_stops const& get_stops(raster_colorizer_ptr & rc)
+{
+    return rc->get_stops();
+}
+
+}
+
+void export_raster_colorizer()
+{
+    using namespace boost::python;
+
+    implicitly_convertible<raster_colorizer_ptr, mapnik::symbolizer_base::value_type>();
+
+    class_<raster_colorizer,raster_colorizer_ptr>("RasterColorizer",
+                                                  "A Raster Colorizer object.",
+                                                  init<colorizer_mode_enum, color>(args("default_mode","default_color"))
+        )
+        .def(init<>())
+        .add_property("default_color",
+                      make_function(&raster_colorizer::get_default_color, return_value_policy<reference_existing_object>()),
+                      &raster_colorizer::set_default_color,
+                      "The default color for stops added without a color (mapnik.Color).\n")
+        .add_property("default_mode",
+                      &raster_colorizer::get_default_mode_enum,
+                      &raster_colorizer::set_default_mode_enum,
+                      "The default mode (mapnik.ColorizerMode).\n"
+                      "\n"
+                      "If a stop is added without a mode, then it will inherit this default mode\n")
+        .add_property("stops",
+                      make_function(get_stops,return_value_policy<reference_existing_object>()),
+                      "The list of stops this RasterColorizer contains\n")
+        .add_property("epsilon",
+                      &raster_colorizer::get_epsilon,
+                      &raster_colorizer::set_epsilon,
+                      "Comparison epsilon value for exact mode\n"
+                      "\n"
+                      "When comparing values in exact mode, values need only be within epsilon to match.\n")
+
+
+        .def("add_stop", add_stop,
+             (arg("ColorizerStop")),
+             "Add a colorizer stop to the raster colorizer.\n"
+             "\n"
+             "Usage:\n"
+             ">>> colorizer = mapnik.RasterColorizer()\n"
+             ">>> color = mapnik.Color(\"#0044cc\")\n"
+             ">>> stop = mapnik.ColorizerStop(3, mapnik.COLORIZER_INHERIT, color)\n"
+             ">>> colorizer.add_stop(stop)\n"
+            )
+        .def("add_stop", add_stop2,
+             (arg("value")),
+             "Add a colorizer stop to the raster colorizer, using the default mode and color.\n"
+             "\n"
+             "Usage:\n"
+             ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+             ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+             ">>> colorizer.add_stop(100)\n"
+            )
+        .def("add_stop", add_stop3,
+             (arg("value")),
+             "Add a colorizer stop to the raster colorizer, using the default mode.\n"
+             "\n"
+             "Usage:\n"
+             ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+             ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+             ">>> colorizer.add_stop(100, mapnik.Color(\"#123456\"))\n"
+            )
+        .def("add_stop", add_stop4,
+             (arg("value")),
+             "Add a colorizer stop to the raster colorizer, using the default color.\n"
+             "\n"
+             "Usage:\n"
+             ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+             ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+             ">>> colorizer.add_stop(100, mapnik.COLORIZER_EXACT)\n"
+            )
+        .def("add_stop", add_stop5,
+             (arg("value")),
+             "Add a colorizer stop to the raster colorizer.\n"
+             "\n"
+             "Usage:\n"
+             ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+             ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+             ">>> colorizer.add_stop(100, mapnik.COLORIZER_DISCRETE, mapnik.Color(\"#112233\"))\n"
+            )
+        .def("get_color", get_color,
+             "Get the color assigned to a certain value in raster data.\n"
+             "\n"
+             "Usage:\n"
+             ">>> colorizer = mapnik.RasterColorizer()\n"
+             ">>> color = mapnik.Color(\"#0044cc\")\n"
+             ">>> colorizer.add_stop(0, mapnik.COLORIZER_DISCRETE, mapnik.Color(\"#000000\"))\n"
+             ">>> colorizer.add_stop(100, mapnik.COLORIZER_DISCRETE, mapnik.Color(\"#0E0A06\"))\n"
+             ">>> colorizer.get_color(50)\n"
+             "Color('#070503')\n"
+            )
+        ;
+
+
+
+    class_<colorizer_stops>("ColorizerStops",
+                            "A RasterColorizer's collection of ordered color stops.\n"
+                            "This class is not meant to be instantiated from python. However, "
+                            "it can be accessed at a RasterColorizer's \"stops\" attribute for "
+                            "introspection purposes",
+                            no_init)
+        .def(vector_indexing_suite<colorizer_stops>())
+        ;
+
+    enum_<colorizer_mode_enum>("ColorizerMode")
+        .value("COLORIZER_INHERIT", COLORIZER_INHERIT)
+        .value("COLORIZER_LINEAR", COLORIZER_LINEAR)
+        .value("COLORIZER_DISCRETE", COLORIZER_DISCRETE)
+        .value("COLORIZER_EXACT", COLORIZER_EXACT)
+        .export_values()
+        ;
+
+
+    class_<colorizer_stop>("ColorizerStop",init<float, colorizer_mode_enum, color const&>(
+                               "A Colorizer Stop object.\n"
+                               "Create with a value, ColorizerMode, and Color\n"
+                               "\n"
+                               "Usage:"
+                               ">>> color = mapnik.Color(\"#fff000\")\n"
+                               ">>> stop= mapnik.ColorizerStop(42.42, mapnik.COLORIZER_LINEAR, color)\n"
+                               ))
+        .add_property("color",
+                      make_function(&colorizer_stop::get_color, return_value_policy<reference_existing_object>()),
+                      &colorizer_stop::set_color,
+                      "The stop color (mapnik.Color).\n")
+        .add_property("value",
+                      &colorizer_stop::get_value,
+                      &colorizer_stop::set_value,
+                      "The stop value.\n")
+        .add_property("label",
+                      make_function(&colorizer_stop::get_label, return_value_policy<copy_const_reference>()),
+                      &colorizer_stop::set_label,
+                      "The stop label.\n")
+        .add_property("mode",
+                      &colorizer_stop::get_mode_enum,
+                      &colorizer_stop::set_mode_enum,
+                      "The stop mode (mapnik.ColorizerMode).\n"
+                      "\n"
+                      "If this is COLORIZER_INHERIT then it will inherit the default mode\n"
+                      " from the RasterColorizer it is added to.\n")
+        .def(self == self)
+        .def("__str__",&colorizer_stop::to_string)
+        ;
+}
diff --git a/src/mapnik_rule.cpp b/src/mapnik_rule.cpp
new file mode 100644
index 0000000..a9210ee
--- /dev/null
+++ b/src/mapnik_rule.cpp
@@ -0,0 +1,100 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/implicit.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/rule.hpp>
+#include <mapnik/expression.hpp>
+#include <mapnik/expression_string.hpp>
+
+using mapnik::rule;
+using mapnik::expr_node;
+using mapnik::expression_ptr;
+using mapnik::point_symbolizer;
+using mapnik::line_symbolizer;
+using mapnik::line_pattern_symbolizer;
+using mapnik::polygon_symbolizer;
+using mapnik::polygon_pattern_symbolizer;
+using mapnik::raster_symbolizer;
+using mapnik::shield_symbolizer;
+using mapnik::text_symbolizer;
+using mapnik::building_symbolizer;
+using mapnik::markers_symbolizer;
+using mapnik::group_symbolizer;
+using mapnik::symbolizer;
+using mapnik::to_expression_string;
+
+void export_rule()
+{
+    using namespace boost::python;
+    implicitly_convertible<point_symbolizer,symbolizer>();
+    implicitly_convertible<line_symbolizer,symbolizer>();
+    implicitly_convertible<line_pattern_symbolizer,symbolizer>();
+    implicitly_convertible<polygon_symbolizer,symbolizer>();
+    implicitly_convertible<building_symbolizer,symbolizer>();
+    implicitly_convertible<polygon_pattern_symbolizer,symbolizer>();
+    implicitly_convertible<raster_symbolizer,symbolizer>();
+    implicitly_convertible<shield_symbolizer,symbolizer>();
+    implicitly_convertible<text_symbolizer,symbolizer>();
+    implicitly_convertible<markers_symbolizer,symbolizer>();
+    implicitly_convertible<group_symbolizer,symbolizer>();
+
+    class_<rule::symbolizers>("Symbolizers",init<>("TODO"))
+        .def(vector_indexing_suite<rule::symbolizers>())
+        ;
+
+    class_<rule>("Rule",init<>("default constructor"))
+        .def(init<std::string const&,
+             boost::python::optional<double,double> >())
+        .add_property("name",make_function
+                      (&rule::get_name,
+                       return_value_policy<copy_const_reference>()),
+                      &rule::set_name)
+        .add_property("filter",make_function
+                      (&rule::get_filter,return_value_policy<copy_const_reference>()),
+                      &rule::set_filter)
+        .add_property("min_scale",&rule::get_min_scale,&rule::set_min_scale)
+        .add_property("max_scale",&rule::get_max_scale,&rule::set_max_scale)
+        .def("set_else",&rule::set_else)
+        .def("has_else",&rule::has_else_filter)
+        .def("set_also",&rule::set_also)
+        .def("has_also",&rule::has_also_filter)
+        .def("active",&rule::active)
+        .add_property("symbols",make_function
+                      (&rule::get_symbolizers,return_value_policy<reference_existing_object>()))
+        .add_property("copy_symbols",make_function
+                      (&rule::get_symbolizers,return_value_policy<copy_const_reference>()))
+        ;
+}
diff --git a/src/mapnik_scaling_method.cpp b/src/mapnik_scaling_method.cpp
new file mode 100644
index 0000000..cdb8b34
--- /dev/null
+++ b/src/mapnik_scaling_method.cpp
@@ -0,0 +1,58 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+
+#include <mapnik/image_scaling.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+void export_scaling_method()
+{
+    using namespace boost::python;
+
+    enum_<mapnik::scaling_method_e>("scaling_method")
+        .value("NEAR", mapnik::SCALING_NEAR)
+        .value("BILINEAR", mapnik::SCALING_BILINEAR)
+        .value("BICUBIC", mapnik::SCALING_BICUBIC)
+        .value("SPLINE16", mapnik::SCALING_SPLINE16)
+        .value("SPLINE36", mapnik::SCALING_SPLINE36)
+        .value("HANNING", mapnik::SCALING_HANNING)
+        .value("HAMMING", mapnik::SCALING_HAMMING)
+        .value("HERMITE", mapnik::SCALING_HERMITE)
+        .value("KAISER", mapnik::SCALING_KAISER)
+        .value("QUADRIC", mapnik::SCALING_QUADRIC)
+        .value("CATROM", mapnik::SCALING_CATROM)
+        .value("GAUSSIAN", mapnik::SCALING_GAUSSIAN)
+        .value("BESSEL", mapnik::SCALING_BESSEL)
+        .value("MITCHELL", mapnik::SCALING_MITCHELL)
+        .value("SINC", mapnik::SCALING_SINC)
+        .value("LANCZOS", mapnik::SCALING_LANCZOS)
+        .value("BLACKMAN", mapnik::SCALING_BLACKMAN)
+        ;
+}
diff --git a/src/mapnik_style.cpp b/src/mapnik_style.cpp
new file mode 100644
index 0000000..1ddff2d
--- /dev/null
+++ b/src/mapnik_style.cpp
@@ -0,0 +1,118 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/value_error.hpp>
+#include <mapnik/rule.hpp>
+#include "mapnik_enumeration.hpp"
+#include <mapnik/feature_type_style.hpp>
+#include <mapnik/image_filter_types.hpp> // generate_image_filters
+
+using mapnik::feature_type_style;
+using mapnik::rules;
+using mapnik::rule;
+
+std::string get_image_filters(feature_type_style & style)
+{
+    std::string filters_str;
+    std::back_insert_iterator<std::string> sink(filters_str);
+    generate_image_filters(sink, style.image_filters());
+    return filters_str;
+}
+
+void set_image_filters(feature_type_style & style, std::string const& filters)
+{
+    std::vector<mapnik::filter::filter_type> new_filters;
+    bool result = parse_image_filters(filters, new_filters);
+    if (!result)
+    {
+        throw mapnik::value_error("failed to parse image-filters: '" + filters + "'");
+    }
+#ifdef _WINDOWS
+    style.image_filters() = new_filters;
+    // FIXME : https://svn.boost.org/trac/boost/ticket/2839
+#else
+    style.image_filters() = std::move(new_filters);
+#endif
+}
+
+void export_style()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::filter_mode_e>("filter_mode")
+        .value("ALL",mapnik::FILTER_ALL)
+        .value("FIRST",mapnik::FILTER_FIRST)
+        ;
+
+    class_<rules>("Rules",init<>("default ctor"))
+        .def(vector_indexing_suite<rules>())
+        ;
+    class_<feature_type_style>("Style",init<>("default style constructor"))
+
+        .add_property("rules",make_function
+                      (&feature_type_style::get_rules,
+                       return_value_policy<reference_existing_object>()),
+                      "List of rules belonging to a style as rule objects.\n"
+                      "\n"
+                      "Usage:\n"
+                      ">>> for r in m.find_style('style 1').rules:\n"
+                      ">>>    print r\n"
+                      "<mapnik._mapnik.Rule object at 0x100549910>\n"
+                      "<mapnik._mapnik.Rule object at 0x100549980>\n"
+            )
+        .add_property("filter_mode",
+                      &feature_type_style::get_filter_mode,
+                      &feature_type_style::set_filter_mode,
+                      "Set/get the filter mode of the style")
+        .add_property("opacity",
+                      &feature_type_style::get_opacity,
+                      &feature_type_style::set_opacity,
+                      "Set/get the opacity of the style")
+        .add_property("comp_op",
+                      &feature_type_style::comp_op,
+                      &feature_type_style::set_comp_op,
+                      "Set/get the comp-op (composite operation) of the style")
+        .add_property("image_filters_inflate",
+                      &feature_type_style::image_filters_inflate,
+                      &feature_type_style::image_filters_inflate,
+                      "Set/get the image_filters_inflate property of the style")
+        .add_property("image_filters",
+                      get_image_filters,
+                      set_image_filters,
+                      "Set/get the comp-op (composite operation) of the style")
+        ;
+
+}
diff --git a/src/mapnik_svg.hpp b/src/mapnik_svg.hpp
new file mode 100644
index 0000000..418ee05
--- /dev/null
+++ b/src/mapnik_svg.hpp
@@ -0,0 +1,56 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2010 Robert Coup
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_PYTHON_BINDING_SVG_INCLUDED
+#define MAPNIK_PYTHON_BINDING_SVG_INCLUDED
+
+// mapnik
+#include <mapnik/parse_transform.hpp>
+#include <mapnik/symbolizer.hpp>
+#include <mapnik/value_error.hpp>
+
+namespace mapnik {
+using namespace boost::python;
+
+template <class T>
+std::string get_svg_transform(T& symbolizer)
+{
+    return symbolizer.get_image_transform_string();
+}
+
+template <class T>
+void set_svg_transform(T& symbolizer, std::string const& transform_wkt)
+{
+    transform_list_ptr trans_expr = mapnik::parse_transform(transform_wkt);
+    if (!trans_expr)
+    {
+        std::stringstream ss;
+        ss << "Could not parse transform from '" 
+           << transform_wkt 
+           << "', expected SVG transform attribute";
+        throw mapnik::value_error(ss.str());
+    }
+    symbolizer.set_image_transform(trans_expr);
+}
+
+} // end of namespace mapnik
+
+#endif // MAPNIK_PYTHON_BINDING_SVG_INCLUDED
diff --git a/src/mapnik_svg_generator_grammar.cpp b/src/mapnik_svg_generator_grammar.cpp
new file mode 100644
index 0000000..5c02b6e
--- /dev/null
+++ b/src/mapnik_svg_generator_grammar.cpp
@@ -0,0 +1,27 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/svg/geometry_svg_generator_impl.hpp>
+#include <string>
+
+using sink_type = std::back_insert_iterator<std::string>;
+template struct mapnik::svg::svg_path_generator<sink_type, mapnik::vertex_adapter>;
diff --git a/src/mapnik_symbolizer.cpp b/src/mapnik_symbolizer.cpp
new file mode 100644
index 0000000..4bd03f8
--- /dev/null
+++ b/src/mapnik_symbolizer.cpp
@@ -0,0 +1,422 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/map_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/symbolizer.hpp>
+#include <mapnik/symbolizer_hash.hpp>
+#include <mapnik/symbolizer_utils.hpp>
+#include <mapnik/symbolizer_keys.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/parse_path.hpp>
+#include <mapnik/path_expression.hpp>
+#include "mapnik_enumeration.hpp"
+#include "mapnik_svg.hpp"
+#include <mapnik/expression_node.hpp>
+#include <mapnik/value_error.hpp>
+#include <mapnik/marker_cache.hpp> // for known_svg_prefix_
+#include <mapnik/group/group_layout.hpp>
+#include <mapnik/group/group_rule.hpp>
+#include <mapnik/group/group_symbolizer_properties.hpp>
+#include <mapnik/util/variant.hpp>
+
+// stl
+#include <sstream>
+
+using mapnik::symbolizer;
+using mapnik::point_symbolizer;
+using mapnik::line_symbolizer;
+using mapnik::line_pattern_symbolizer;
+using mapnik::polygon_symbolizer;
+using mapnik::polygon_pattern_symbolizer;
+using mapnik::raster_symbolizer;
+using mapnik::shield_symbolizer;
+using mapnik::text_symbolizer;
+using mapnik::building_symbolizer;
+using mapnik::markers_symbolizer;
+using mapnik::debug_symbolizer;
+using mapnik::group_symbolizer;
+using mapnik::symbolizer_base;
+using mapnik::color;
+using mapnik::path_processor_type;
+using mapnik::path_expression_ptr;
+using mapnik::guess_type;
+using mapnik::expression_ptr;
+using mapnik::parse_path;
+
+
+namespace {
+using namespace boost::python;
+void __setitem__(mapnik::symbolizer_base & sym, std::string const& name, mapnik::symbolizer_base::value_type const& val)
+{
+    put(sym, mapnik::get_key(name), val);
+}
+
+std::shared_ptr<mapnik::symbolizer_base::value_type> numeric_wrapper(const object& arg)
+{
+    std::shared_ptr<mapnik::symbolizer_base::value_type> result;
+    if (PyBool_Check(arg.ptr()))
+    {
+        mapnik::value_bool val = extract<mapnik::value_bool>(arg);
+        result.reset(new mapnik::symbolizer_base::value_type(val));
+    }
+    else if (PyFloat_Check(arg.ptr()))
+    {
+        mapnik::value_double val = extract<mapnik::value_double>(arg);
+        result.reset(new mapnik::symbolizer_base::value_type(val));
+    }
+    else
+    {
+        mapnik::value_integer val = extract<mapnik::value_integer>(arg);
+        result.reset(new mapnik::symbolizer_base::value_type(val));
+    }
+    return result;
+}
+
+struct extract_python_object
+{
+    using result_type = boost::python::object;
+
+    template <typename T>
+    auto operator() (T const& val) const -> result_type
+    {
+        return result_type(val); // wrap into python object
+    }
+};
+
+boost::python::object __getitem__(mapnik::symbolizer_base const& sym, std::string const& name)
+{
+    using const_iterator = symbolizer_base::cont_type::const_iterator;
+    mapnik::keys key = mapnik::get_key(name);
+    const_iterator itr = sym.properties.find(key);
+    if (itr != sym.properties.end())
+    {
+        return mapnik::util::apply_visitor(extract_python_object(), itr->second);
+    }
+    //mapnik::property_meta_type const& meta = mapnik::get_meta(key);
+    //return mapnik::util::apply_visitor(extract_python_object(), std::get<1>(meta));
+    return boost::python::object();
+}
+
+/*
+std::string __str__(mapnik::symbolizer const& sym)
+{
+    return mapnik::util::apply_visitor(mapnik::symbolizer_to_json(), sym);
+}
+*/
+
+std::string get_symbolizer_type(symbolizer const& sym)
+{
+    return mapnik::symbolizer_name(sym); // FIXME - do we need this ?
+}
+
+std::size_t hash_impl(symbolizer const& sym)
+{
+    return mapnik::util::apply_visitor(mapnik::symbolizer_hash_visitor(), sym);
+}
+
+template <typename T>
+std::size_t hash_impl_2(T const& sym)
+{
+    return mapnik::symbolizer_hash::value<T>(sym);
+}
+
+struct extract_underlying_type_visitor
+{
+    template <typename T>
+    boost::python::object operator() (T const& sym) const
+    {
+        return boost::python::object(sym);
+    }
+};
+
+boost::python::object extract_underlying_type(symbolizer const& sym)
+{
+    return mapnik::util::apply_visitor(extract_underlying_type_visitor(), sym);
+}
+
+}
+
+void export_symbolizer()
+{
+    using namespace boost::python;
+
+    //implicitly_convertible<mapnik::value_bool, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<mapnik::value_integer, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<mapnik::value_double, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<std::string, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<mapnik::color, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<mapnik::expression_ptr, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<mapnik::enumeration_wrapper, mapnik::symbolizer_base::value_type>();
+    implicitly_convertible<std::shared_ptr<mapnik::group_symbolizer_properties>, mapnik::symbolizer_base::value_type>();
+
+    enum_<mapnik::keys>("keys")
+        .value("gamma", mapnik::keys::gamma)
+        .value("gamma_method",mapnik::keys::gamma_method)
+        ;
+
+    class_<symbolizer>("Symbolizer",no_init)
+        .def("type",get_symbolizer_type)
+        .def("__hash__",hash_impl)
+        .def("extract", extract_underlying_type)
+        ;
+
+    class_<symbolizer_base::value_type>("NumericWrapper")
+        .def("__init__", make_constructor(numeric_wrapper))
+        ;
+
+    class_<symbolizer_base>("SymbolizerBase",no_init)
+        .def("__setitem__",&__setitem__)
+        .def("__setattr__",&__setitem__)
+        .def("__getitem__",&__getitem__)
+        .def("__getattr__",&__getitem__)
+        //.def("__str__", &__str__)
+        .def(self == self) // __eq__
+        ;
+}
+
+
+void export_shield_symbolizer()
+{
+    using namespace boost::python;
+    class_< shield_symbolizer, bases<text_symbolizer> >("ShieldSymbolizer",
+                                                        init<>("Default ctor"))
+        .def("__hash__",hash_impl_2<shield_symbolizer>)
+        ;
+
+}
+
+void export_polygon_symbolizer()
+{
+    using namespace boost::python;
+
+    class_<polygon_symbolizer, bases<symbolizer_base> >("PolygonSymbolizer",
+                                                        init<>("Default ctor"))
+        .def("__hash__",hash_impl_2<polygon_symbolizer>)
+        ;
+
+}
+
+void export_polygon_pattern_symbolizer()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::pattern_alignment_e>("pattern_alignment")
+        .value("LOCAL",mapnik::LOCAL_ALIGNMENT)
+        .value("GLOBAL",mapnik::GLOBAL_ALIGNMENT)
+        ;
+
+    class_<polygon_pattern_symbolizer>("PolygonPatternSymbolizer",
+                                       init<>("Default ctor"))
+        .def("__hash__",hash_impl_2<polygon_pattern_symbolizer>)
+        ;
+}
+
+void export_raster_symbolizer()
+{
+    using namespace boost::python;
+
+    class_<raster_symbolizer, bases<symbolizer_base> >("RasterSymbolizer",
+                              init<>("Default ctor"))
+        ;
+}
+
+void export_point_symbolizer()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::point_placement_e>("point_placement")
+        .value("CENTROID",mapnik::CENTROID_POINT_PLACEMENT)
+        .value("INTERIOR",mapnik::INTERIOR_POINT_PLACEMENT)
+        ;
+
+    class_<point_symbolizer, bases<symbolizer_base> >("PointSymbolizer",
+                             init<>("Default Point Symbolizer - 4x4 black square"))
+        .def("__hash__",hash_impl_2<point_symbolizer>)
+        ;
+}
+
+void export_markers_symbolizer()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::marker_placement_e>("marker_placement")
+        .value("POINT_PLACEMENT",mapnik::MARKER_POINT_PLACEMENT)
+        .value("INTERIOR_PLACEMENT",mapnik::MARKER_INTERIOR_PLACEMENT)
+        .value("LINE_PLACEMENT",mapnik::MARKER_LINE_PLACEMENT)
+        ;
+
+    mapnik::enumeration_<mapnik::marker_multi_policy_e>("marker_multi_policy")
+        .value("EACH",mapnik::MARKER_EACH_MULTI)
+        .value("WHOLE",mapnik::MARKER_WHOLE_MULTI)
+        .value("LARGEST",mapnik::MARKER_LARGEST_MULTI)
+        ;
+
+    class_<markers_symbolizer, bases<symbolizer_base> >("MarkersSymbolizer",
+                               init<>("Default Markers Symbolizer - circle"))
+        .def("__hash__",hash_impl_2<markers_symbolizer>)
+        ;
+}
+
+
+void export_line_symbolizer()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::line_rasterizer_e>("line_rasterizer")
+        .value("FULL",mapnik::RASTERIZER_FULL)
+        .value("FAST",mapnik::RASTERIZER_FAST)
+        ;
+
+    mapnik::enumeration_<mapnik::line_cap_e>("stroke_linecap",
+                             "The possible values for a line cap used when drawing\n"
+                             "with a stroke.\n")
+        .value("BUTT_CAP",mapnik::BUTT_CAP)
+        .value("SQUARE_CAP",mapnik::SQUARE_CAP)
+        .value("ROUND_CAP",mapnik::ROUND_CAP)
+        ;
+
+    mapnik::enumeration_<mapnik::line_join_e>("stroke_linejoin",
+                                      "The possible values for the line joining mode\n"
+                                      "when drawing with a stroke.\n")
+        .value("MITER_JOIN",mapnik::MITER_JOIN)
+        .value("MITER_REVERT_JOIN",mapnik::MITER_REVERT_JOIN)
+        .value("ROUND_JOIN",mapnik::ROUND_JOIN)
+        .value("BEVEL_JOIN",mapnik::BEVEL_JOIN)
+        ;
+
+
+    class_<line_symbolizer, bases<symbolizer_base> >("LineSymbolizer",
+                            init<>("Default LineSymbolizer - 1px solid black"))
+        .def("__hash__",hash_impl_2<line_symbolizer>)
+        ;
+}
+
+void export_line_pattern_symbolizer()
+{
+    using namespace boost::python;
+
+    class_<line_pattern_symbolizer, bases<symbolizer_base> >("LinePatternSymbolizer",
+                                    init<> ("Default LinePatternSymbolizer"))
+        .def("__hash__",hash_impl_2<line_pattern_symbolizer>)
+        ;
+}
+
+void export_debug_symbolizer()
+{
+    using namespace boost::python;
+
+    mapnik::enumeration_<mapnik::debug_symbolizer_mode_e>("debug_symbolizer_mode")
+        .value("COLLISION",mapnik::DEBUG_SYM_MODE_COLLISION)
+        .value("VERTEX",mapnik::DEBUG_SYM_MODE_VERTEX)
+        ;
+
+    class_<debug_symbolizer, bases<symbolizer_base> >("DebugSymbolizer",
+                             init<>("Default debug Symbolizer"))
+        .def("__hash__",hash_impl_2<debug_symbolizer>)
+        ;
+}
+
+void export_building_symbolizer()
+{
+    using namespace boost::python;
+
+    class_<building_symbolizer, bases<symbolizer_base> >("BuildingSymbolizer",
+                               init<>("Default BuildingSymbolizer"))
+        .def("__hash__",hash_impl_2<building_symbolizer>)
+        ;
+
+}
+
+namespace {
+
+void group_symbolizer_properties_set_layout_simple(mapnik::group_symbolizer_properties &p,
+                                                   mapnik::simple_row_layout &s)
+{
+    p.set_layout(s);
+}
+
+void group_symbolizer_properties_set_layout_pair(mapnik::group_symbolizer_properties &p,
+                                                 mapnik::pair_layout &s)
+{
+    p.set_layout(s);
+}
+
+std::shared_ptr<mapnik::group_rule> group_rule_construct1(mapnik::expression_ptr p)
+{
+    return std::make_shared<mapnik::group_rule>(p, mapnik::expression_ptr());
+}
+
+} // anonymous namespace
+
+void export_group_symbolizer()
+{
+    using namespace boost::python;
+    using mapnik::group_rule;
+    using mapnik::simple_row_layout;
+    using mapnik::pair_layout;
+    using mapnik::group_symbolizer_properties;
+
+    class_<group_rule, std::shared_ptr<group_rule> >("GroupRule",
+                                                     init<expression_ptr, expression_ptr>())
+        .def("__init__", boost::python::make_constructor(group_rule_construct1))
+        .def("append", &group_rule::append)
+        .def("set_filter", &group_rule::set_filter)
+        .def("set_repeat_key", &group_rule::set_repeat_key)
+        ;
+
+    class_<simple_row_layout>("SimpleRowLayout")
+        .def("item_margin", &simple_row_layout::get_item_margin)
+        .def("set_item_margin", &simple_row_layout::set_item_margin)
+        ;
+
+    class_<pair_layout>("PairLayout")
+        .def("item_margin", &simple_row_layout::get_item_margin)
+        .def("set_item_margin", &simple_row_layout::set_item_margin)
+        .def("max_difference", &pair_layout::get_max_difference)
+        .def("set_max_difference", &pair_layout::set_max_difference)
+        ;
+
+    class_<group_symbolizer_properties, std::shared_ptr<group_symbolizer_properties> >("GroupSymbolizerProperties")
+        .def("add_rule", &group_symbolizer_properties::add_rule)
+        .def("set_layout", &group_symbolizer_properties_set_layout_simple)
+        .def("set_layout", &group_symbolizer_properties_set_layout_pair)
+        ;
+
+    class_<group_symbolizer, bases<symbolizer_base> >("GroupSymbolizer",
+                                                      init<>("Default GroupSymbolizer"))
+        .def("__hash__",hash_impl_2<group_symbolizer>)
+        ;
+
+}
diff --git a/src/mapnik_text_placement.cpp b/src/mapnik_text_placement.cpp
new file mode 100644
index 0000000..468a70f
--- /dev/null
+++ b/src/mapnik_text_placement.cpp
@@ -0,0 +1,587 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/stl_iterator.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/text/text_properties.hpp>
+#include <mapnik/text/placements/simple.hpp>
+#include <mapnik/text/placements/list.hpp>
+#include <mapnik/text/formatting/text.hpp>
+#include <mapnik/text/formatting/list.hpp>
+#include <mapnik/text/formatting/format.hpp>
+#include <mapnik/text/formatting/layout.hpp>
+#include <mapnik/text/text_layout.hpp>
+#include <mapnik/symbolizer.hpp>
+
+#include "mapnik_enumeration.hpp"
+#include "mapnik_threads.hpp"
+
+using namespace mapnik;
+
+/* Notes:
+   Overriding functions in inherited classes:
+   boost.python documentation doesn't really tell you how to do it.
+   But this helps:
+   http://www.gamedev.net/topic/446225-inheritance-in-boostpython/
+
+   register_ptr_to_python is required for wrapped classes, but not for unwrapped.
+
+   Functions don't have to be members of the class, but can also be
+   normal functions taking a ref to the class as first parameter.
+*/
+
+namespace {
+
+using namespace boost::python;
+
+// This class works around a feature in boost python.
+// See http://osdir.com/ml/python.c++/2003-11/msg00158.html
+
+template <typename T,
+          typename X1 = boost::python::detail::not_specified,
+          typename X2 = boost::python::detail::not_specified,
+          typename X3 = boost::python::detail::not_specified>
+class class_with_converter : public boost::python::class_<T, X1, X2, X3>
+{
+public:
+    using self = class_with_converter<T,X1,X2,X3>;
+    // Construct with the class name, with or without docstring, and default __init__() function
+    class_with_converter(char const* name, char const* doc = 0) : boost::python::class_<T, X1, X2, X3>(name, doc)  { }
+
+    // Construct with class name, no docstring, and an uncallable __init__ function
+    class_with_converter(char const* name, boost::python::no_init_t y) : boost::python::class_<T, X1, X2, X3>(name, y) { }
+
+    // Construct with class name, docstring, and an uncallable __init__ function
+    class_with_converter(char const* name, char const* doc, boost::python::no_init_t y) : boost::python::class_<T, X1, X2, X3>(name, doc, y) { }
+
+    // Construct with class name and init<> function
+    template <class DerivedT> class_with_converter(char const* name, boost::python::init_base<DerivedT> const& i)
+        : boost::python::class_<T, X1, X2, X3>(name, i) { }
+
+    // Construct with class name, docstring and init<> function
+    template <class DerivedT>
+    inline class_with_converter(char const* name, char const* doc, boost::python::init_base<DerivedT> const& i)
+        : boost::python::class_<T, X1, X2, X3>(name, doc, i) { }
+
+    template <class D>
+    self& def_readwrite_convert(char const* name, D const& d, char const* /*doc*/=0)
+    {
+        this->add_property(name,
+                           boost::python::make_getter(d, boost::python::return_value_policy<boost::python::return_by_value>()),
+                           boost::python::make_setter(d, boost::python::default_call_policies()));
+        return *this;
+    }
+};
+
+/*
+boost::python::tuple get_displacement(text_layout_properties const& t)
+{
+    return boost::python::make_tuple(0.0,0.0);// FIXME t.displacement.x, t.displacement.y);
+}
+
+void set_displacement(text_layout_properties &t, boost::python::tuple arg)
+{
+    if (len(arg) != 2)
+    {
+        PyErr_SetObject(PyExc_ValueError,
+                        ("expected 2-item tuple in call to set_displacement; got %s"
+                         % arg).ptr()
+            );
+        throw_error_already_set();
+    }
+
+    //double x = extract<double>(arg[0]);
+    //double y = extract<double>(arg[1]);
+    //t.displacement.set(x, y); FIXME
+}
+
+
+struct NodeWrap
+    : formatting::node, wrapper<formatting::node>
+{
+    NodeWrap()
+        : formatting::node(), wrapper<formatting::node>() {}
+
+    void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        python_block_auto_unblock b;
+        this->get_override("apply")(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+    }
+
+    virtual void add_expressions(expression_set &output) const
+    {
+        override o = this->get_override("add_expressions");
+        if (o)
+        {
+            python_block_auto_unblock b;
+            o(ptr(&output));
+        } else
+        {
+            formatting::node::add_expressions(output);
+        }
+    }
+
+    void default_add_expressions(expression_set &output) const
+    {
+        formatting::node::add_expressions(output);
+    }
+};
+*/
+/*
+struct TextNodeWrap
+    : formatting::text_node, wrapper<formatting::text_node>
+{
+    TextNodeWrap(expression_ptr expr)
+        : formatting::text_node(expr), wrapper<formatting::text_node>() {}
+
+    TextNodeWrap(std::string expr_text)
+        : formatting::text_node(expr_text), wrapper<formatting::text_node>() {}
+
+    virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        if(override o = this->get_override("apply"))
+        {
+            python_block_auto_unblock b;
+            o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+        }
+        else
+        {
+            formatting::text_node::apply(p, feature, vars, output);
+        }
+    }
+
+    void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        formatting::text_node::apply(p, feature, vars, output);
+    }
+};
+*/
+/*
+struct FormatNodeWrap
+    : formatting::format_node, wrapper<formatting::format_node>
+{
+    virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        if(override o = this->get_override("apply"))
+        {
+            python_block_auto_unblock b;
+            o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+        }
+        else
+        {
+            formatting::format_node::apply(p, feature, vars ,output);
+        }
+    }
+
+    void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        formatting::format_node::apply(p, feature, vars, output);
+    }
+};
+
+struct ExprFormatWrap: formatting::expression_format, wrapper<formatting::expression_format>
+{
+    virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        if(override o = this->get_override("apply"))
+        {
+            python_block_auto_unblock b;
+            o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+        }
+        else
+        {
+            formatting::expression_format::apply(p, feature, vars, output);
+        }
+    }
+
+    void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        formatting::expression_format::apply(p, feature, vars, output);
+    }
+};
+
+struct LayoutNodeWrap: formatting::layout_node, wrapper<formatting::layout_node>
+{
+    virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        if(override o = this->get_override("apply"))
+        {
+            python_block_auto_unblock b;
+            o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+        }
+        else
+        {
+            formatting::layout_node::apply(p, feature, vars, output);
+        }
+    }
+
+    void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        formatting::layout_node::apply(p, feature, vars, output);
+    }
+};
+
+struct ListNodeWrap: formatting::list_node, wrapper<formatting::list_node>
+{
+    //Default constructor
+    ListNodeWrap() : formatting::list_node(), wrapper<formatting::list_node>()
+    {
+    }
+
+    //Special constructor: Takes a python sequence as its argument
+    ListNodeWrap(object l) : formatting::list_node(), wrapper<formatting::list_node>()
+    {
+        stl_input_iterator<formatting::node_ptr> begin(l), end;
+        while (begin != end)
+        {
+           children_.push_back(*begin);
+           ++begin;
+        }
+    }
+
+    // TODO: Add constructor taking variable number of arguments.
+       http://wiki.python.org/moin/boost.python/HowTo#A.22Raw.22_function
+
+    virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        if(override o = this->get_override("apply"))
+        {
+            python_block_auto_unblock b;
+            o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+        }
+        else
+        {
+            formatting::list_node::apply(p, feature, vars, output);
+        }
+    }
+
+    void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+    {
+        formatting::list_node::apply(p, feature, vars, output);
+    }
+
+    inline void IndexError(){
+        PyErr_SetString(PyExc_IndexError, "Index out of range");
+        throw_error_already_set();
+    }
+
+    unsigned get_length()
+    {
+        return children_.size();
+    }
+
+    formatting::node_ptr get_item(int i)
+    {
+        if (i < 0) i+= children_.size();
+        if (i < static_cast<int>(children_.size())) return children_[i];
+        IndexError();
+        return formatting::node_ptr(); //Avoid compiler warning
+    }
+
+    void set_item(int i, formatting::node_ptr ptr)
+    {
+        if (i < 0) i+= children_.size();
+        if (i < static_cast<int>(children_.size())) children_[i] = ptr;
+        IndexError();
+    }
+
+    void append(formatting::node_ptr ptr)
+    {
+        children_.push_back(ptr);
+    }
+};
+*/
+/*
+struct TextPlacementsWrap: text_placements, wrapper<text_placements>
+{
+    text_placement_info_ptr get_placement_info(double scale_factor_) const
+    {
+        python_block_auto_unblock b;
+        //return this->get_override("get_placement_info")();
+        return text_placement_info_ptr();
+    }
+};
+
+struct TextPlacementInfoWrap: text_placement_info, wrapper<text_placement_info>
+{
+    TextPlacementInfoWrap(text_placements const* parent,
+                          double scale_factor_)
+        : text_placement_info(parent, scale_factor_)
+    {
+
+    }
+
+    bool next()
+    {
+        python_block_auto_unblock b;
+        return this->get_override("next")();
+    }
+};
+
+void insert_expression(expression_set *set, expression_ptr p)
+{
+    set->insert(p);
+}
+
+
+evaluated_format_properties_ptr get_format(text_symbolizer const& sym)
+{
+    return sym.get_placement_options()->defaults.format;
+}
+
+void set_format(text_symbolizer const& sym, evaluated_format_properties_ptr format)
+{
+    sym.get_placement_options()->defaults.format = format;
+}
+
+text_symbolizer_properties & get_properties(text_symbolizer const& sym)
+{
+    return sym.get_placement_options()->defaults;
+}
+
+void set_properties(text_symbolizer const& sym, text_symbolizer_properties & defaults)
+{
+    sym.get_placement_options()->defaults = defaults;
+}
+*/
+}
+
+void export_text_placement()
+{
+    /*
+    using namespace boost::python;
+
+    enumeration_<label_placement_e>("label_placement")
+        .value("LINE_PLACEMENT",LINE_PLACEMENT)
+        .value("POINT_PLACEMENT",POINT_PLACEMENT)
+        .value("VERTEX_PLACEMENT",VERTEX_PLACEMENT)
+        .value("INTERIOR_PLACEMENT",INTERIOR_PLACEMENT)
+        ;
+    enumeration_<vertical_alignment_e>("vertical_alignment")
+        .value("TOP",V_TOP)
+        .value("MIDDLE",V_MIDDLE)
+        .value("BOTTOM",V_BOTTOM)
+        .value("AUTO",V_AUTO)
+        ;
+
+    enumeration_<horizontal_alignment_e>("horizontal_alignment")
+        .value("LEFT",H_LEFT)
+        .value("MIDDLE",H_MIDDLE)
+        .value("RIGHT",H_RIGHT)
+        .value("AUTO",H_AUTO)
+        ;
+
+    enumeration_<justify_alignment_e>("justify_alignment")
+        .value("LEFT",J_LEFT)
+        .value("MIDDLE",J_MIDDLE)
+        .value("RIGHT",J_RIGHT)
+        .value("AUTO", J_AUTO)
+        ;
+
+    enumeration_<text_transform_e>("text_transform")
+        .value("NONE",NONE)
+        .value("UPPERCASE",UPPERCASE)
+        .value("LOWERCASE",LOWERCASE)
+        .value("CAPITALIZE",CAPITALIZE)
+        ;
+
+    enumeration_<halo_rasterizer_e>("halo_rasterizer")
+        .value("FULL",HALO_RASTERIZER_FULL)
+        .value("FAST",HALO_RASTERIZER_FAST)
+        ;
+    */
+    class_<text_symbolizer>("TextSymbolizer",
+                            init<>())
+        ;
+    /*
+
+    class_with_converter<text_symbolizer_properties>
+        ("TextSymbolizerProperties")
+        .def_readwrite_convert("label_placement", &text_symbolizer_properties::label_placement)
+        .def_readwrite_convert("upright", &text_symbolizer_properties::upright)
+        .def_readwrite("label_spacing", &text_symbolizer_properties::label_spacing)
+        .def_readwrite("label_position_tolerance", &text_symbolizer_properties::label_position_tolerance)
+        .def_readwrite("avoid_edges", &text_symbolizer_properties::avoid_edges)
+        .def_readwrite("margin", &text_symbolizer_properties::margin)
+        .def_readwrite("repeat_distance", &text_symbolizer_properties::repeat_distance)
+        .def_readwrite("minimum_distance", &text_symbolizer_properties::minimum_distance)
+        .def_readwrite("minimum_padding", &text_symbolizer_properties::minimum_padding)
+        .def_readwrite("minimum_path_length", &text_symbolizer_properties::minimum_path_length)
+        .def_readwrite("maximum_angle_char_delta", &text_symbolizer_properties::max_char_angle_delta)
+        .def_readwrite("allow_overlap", &text_symbolizer_properties::allow_overlap)
+        .def_readwrite("largest_bbox_only", &text_symbolizer_properties::largest_bbox_only)
+        .def_readwrite("layout_defaults", &text_symbolizer_properties::layout_defaults)
+        //.def_readwrite("format", &text_symbolizer_properties::format)
+        .add_property ("format_tree",
+                       &text_symbolizer_properties::format_tree,
+                       &text_symbolizer_properties::set_format_tree);
+    //from_xml, to_xml operate on mapnik's internal XML tree and don't make sense in python.
+     //  add_expressions isn't useful in python either. The result is only needed by
+     //  attribute_collector (which isn't exposed in python) and
+     //  it just calls add_expressions of the associated formatting tree.
+     //  set_old_style expression is just a compatibility wrapper and doesn't need to be exposed in python.
+    ;
+
+    class_with_converter<text_layout_properties>
+        ("TextLayoutProperties")
+        .def_readwrite_convert("horizontal_alignment", &text_layout_properties::halign)
+        .def_readwrite_convert("justify_alignment", &text_layout_properties::jalign)
+        .def_readwrite_convert("vertical_alignment", &text_layout_properties::valign)
+        .def_readwrite("text_ratio", &text_layout_properties::text_ratio)
+        .def_readwrite("wrap_width", &text_layout_properties::wrap_width)
+        .def_readwrite("wrap_before", &text_layout_properties::wrap_before)
+        .def_readwrite("orientation", &text_layout_properties::orientation)
+        .def_readwrite("rotate_displacement", &text_layout_properties::rotate_displacement)
+        .add_property("displacement", &get_displacement, &set_displacement);
+
+    class_with_converter<detail::evaluated_format_properties>
+        ("CharProperties")
+        .def_readwrite_convert("text_transform", &detail::evaluated_format_properties::text_transform)
+        .def_readwrite_convert("fontset", &detail::evaluated_format_properties::fontset)
+        .def(init<detail::evaluated_format_properties const&>()) //Copy constructor
+        .def_readwrite("face_name", &detail::evaluated_format_properties::face_name)
+        .def_readwrite("text_size", &detail::evaluated_format_properties::text_size)
+        .def_readwrite("character_spacing", &detail::evaluated_format_properties::character_spacing)
+        .def_readwrite("line_spacing", &detail::evaluated_format_properties::line_spacing)
+        .def_readwrite("text_opacity", &detail::evaluated_format_properties::text_opacity)
+        .def_readwrite("fill", &detail::evaluated_format_properties::fill)
+        .def_readwrite("halo_fill", &detail::evaluated_format_properties::halo_fill)
+        .def_readwrite("halo_radius", &evaluated_format_properties::halo_radius)
+        //from_xml, to_xml operate on mapnik's internal XML tree and don't make sense in python.
+        ;
+    class_<TextPlacementsWrap,
+        std::shared_ptr<TextPlacementsWrap>,
+        boost::noncopyable>
+        ("TextPlacements")
+        .def_readwrite("defaults", &text_placements::defaults)
+        //.def("get_placement_info", pure_virtual(&text_placements::get_placement_info))
+        // TODO: add_expressions()
+        ;
+    register_ptr_to_python<std::shared_ptr<text_placements> >();
+
+    class_<TextPlacementInfoWrap,
+        std::shared_ptr<TextPlacementInfoWrap>,
+        boost::noncopyable>
+        ("TextPlacementInfo",
+         init<text_placements const*, double>())
+        .def("next", pure_virtual(&text_placement_info::next))
+        .def_readwrite("properties", &text_placement_info::properties)
+        .def_readwrite("scale_factor", &text_placement_info::scale_factor)
+        ;
+    register_ptr_to_python<std::shared_ptr<text_placement_info> >();
+
+
+    class_<expression_set,std::shared_ptr<expression_set>,
+           boost::noncopyable>("ExpressionSet")
+        .def("insert", &insert_expression);
+    ;
+
+    class_<formatting::node,std::shared_ptr<formatting::node>,
+           boost::noncopyable>("FormattingNode")
+        .def("apply", pure_virtual(&formatting::node::apply))
+        .def("add_expressions", pure_virtual(&formatting::node::add_expressions))
+        .def("to_xml", pure_virtual(&formatting::node::to_xml))
+        ;
+
+    register_ptr_to_python<std::shared_ptr<formatting::node> >();
+
+    class_<formatting::text_node,
+           std::shared_ptr<formatting::text_node>,
+           bases<formatting::node>,boost::noncopyable>("FormattingText", init<expression_ptr>())
+        .def(init<std::string>())
+        .def("apply", &formatting::text_node::apply)//, &TextNodeWrap::default_apply)
+        .add_property("text",&formatting::text_node::get_text, &formatting::text_node::set_text)
+        ;
+
+    register_ptr_to_python<std::shared_ptr<formatting::text_node> >();
+
+    class_with_converter<FormatNodeWrap,
+        std::shared_ptr<FormatNodeWrap>,
+        bases<formatting::node>,
+        boost::noncopyable>
+        ("FormattingFormat")
+        .def_readwrite_convert("text_size", &formatting::format_node::text_size)
+        .def_readwrite_convert("face_name", &formatting::format_node::face_name)
+        .def_readwrite_convert("character_spacing", &formatting::format_node::character_spacing)
+        .def_readwrite_convert("line_spacing", &formatting::format_node::line_spacing)
+        .def_readwrite_convert("text_opacity", &formatting::format_node::text_opacity)
+        .def_readwrite_convert("text_transform", &formatting::format_node::text_transform)
+        .def_readwrite_convert("fill", &formatting::format_node::fill)
+        .def_readwrite_convert("halo_fill", &formatting::format_node::halo_fill)
+        .def_readwrite_convert("halo_radius", &formatting::format_node::halo_radius)
+        .def("apply", &formatting::format_node::apply, &FormatNodeWrap::default_apply)
+        .add_property("child",
+                      &formatting::format_node::get_child,
+                      &formatting::format_node::set_child)
+        ;
+    register_ptr_to_python<std::shared_ptr<formatting::format_node> >();
+
+    class_<ListNodeWrap,
+        std::shared_ptr<ListNodeWrap>,
+        bases<formatting::node>,
+        boost::noncopyable>
+        ("FormattingList", init<>())
+        .def(init<list>())
+        .def("append", &formatting::list_node::push_back)
+        .def("apply", &formatting::list_node::apply, &ListNodeWrap::default_apply)
+        .def("__len__", &ListNodeWrap::get_length)
+        .def("__getitem__", &ListNodeWrap::get_item)
+        .def("__setitem__", &ListNodeWrap::set_item)
+        .def("append", &ListNodeWrap::append)
+        ;
+
+    register_ptr_to_python<std::shared_ptr<formatting::list_node> >();
+
+    class_<ExprFormatWrap,
+        std::shared_ptr<ExprFormatWrap>,
+        bases<formatting::node>,
+        boost::noncopyable>
+        ("FormattingExpressionFormat")
+        .def_readwrite("text_size", &formatting::expression_format::text_size)
+        .def_readwrite("face_name", &formatting::expression_format::face_name)
+        .def_readwrite("character_spacing", &formatting::expression_format::character_spacing)
+        .def_readwrite("line_spacing", &formatting::expression_format::line_spacing)
+        .def_readwrite("text_opacity", &formatting::expression_format::text_opacity)
+        .def_readwrite("fill", &formatting::expression_format::fill)
+        .def_readwrite("halo_fill", &formatting::expression_format::halo_fill)
+        .def_readwrite("halo_radius", &formatting::expression_format::halo_radius)
+        .def("apply", &formatting::expression_format::apply, &ExprFormatWrap::default_apply)
+        .add_property("child",
+                      &formatting::expression_format::get_child,
+                      &formatting::expression_format::set_child)
+        ;
+    register_ptr_to_python<std::shared_ptr<formatting::expression_format> >();
+*/
+    //TODO: registry
+}
diff --git a/src/mapnik_threads.hpp b/src/mapnik_threads.hpp
new file mode 100644
index 0000000..25b5587
--- /dev/null
+++ b/src/mapnik_threads.hpp
@@ -0,0 +1,109 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_THREADS_HPP
+#define MAPNIK_THREADS_HPP
+
+#include <boost/thread/tss.hpp>
+#include <Python.h>
+
+namespace mapnik {
+class python_thread
+{
+    /* Docs:
+       http://docs.python.org/c-api/init.html#thread-state-and-the-global-interpreter-lock
+    */
+public:
+    static void unblock()
+    {
+#ifdef MAPNIK_DEBUG
+        if (state.get())
+        {
+            std::cerr << "ERROR: Python threads are already unblocked. "
+                "Unblocking again will loose the current state and "
+                "might crash later. Aborting!\n";
+            abort(); //This is a serious error and can't be handled in any other sane way
+        }
+#endif
+        PyThreadState *_save = 0; //Name defined by python
+        Py_UNBLOCK_THREADS;
+        state.reset(_save);
+#ifdef MAPNIK_DEBUG
+        if (!_save) {
+            thread_support = false;
+        }
+#endif
+    }
+
+    static void block()
+    {
+#ifdef MAPNIK_DEBUG
+        if (thread_support && !state.get())
+        {
+            std::cerr << "ERROR: Trying to restore python thread state, "
+                "but no state is saved. Can't continue and also "
+                "can't raise an exception because the python "
+                "interpreter might be non-function. Aborting!\n";
+            abort();
+        }
+#endif
+        PyThreadState *_save = state.release(); //Name defined by python
+        Py_BLOCK_THREADS;
+    }
+
+private:
+    static boost::thread_specific_ptr<PyThreadState> state;
+#ifdef MAPNIK_DEBUG
+    static bool thread_support;
+#endif
+};
+
+class python_block_auto_unblock
+{
+public:
+    python_block_auto_unblock()
+    {
+        python_thread::block();
+    }
+
+    ~python_block_auto_unblock()
+    {
+        python_thread::unblock();
+    }
+};
+
+class python_unblock_auto_block
+{
+public:
+    python_unblock_auto_block()
+    {
+        python_thread::unblock();
+    }
+
+    ~python_unblock_auto_block()
+    {
+        python_thread::block();
+    }
+};
+
+} //namespace
+
+#endif // MAPNIK_THREADS_HPP
diff --git a/src/mapnik_value_converter.hpp b/src/mapnik_value_converter.hpp
new file mode 100644
index 0000000..dbb9e87
--- /dev/null
+++ b/src/mapnik_value_converter.hpp
@@ -0,0 +1,90 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_PYTHON_BINDING_VALUE_CONVERTER_INCLUDED
+#define MAPNIK_PYTHON_BINDING_VALUE_CONVERTER_INCLUDED
+
+// mapnik
+#include <mapnik/value.hpp>
+#include <mapnik/util/variant.hpp>
+// boost
+#include <boost/python.hpp>
+#include <boost/implicit_cast.hpp>
+
+namespace boost { namespace python {
+
+    struct value_converter
+    {
+        PyObject * operator() (mapnik::value_integer val) const
+        {
+            return ::PyLong_FromLongLong(val);
+        }
+
+        PyObject * operator() (mapnik::value_double val) const
+        {
+            return ::PyFloat_FromDouble(val);
+        }
+
+        PyObject * operator() (mapnik::value_bool val) const
+        {
+            return ::PyBool_FromLong(val);
+        }
+
+        PyObject * operator() (std::string const& s) const
+        {
+            return ::PyUnicode_DecodeUTF8(s.c_str(),implicit_cast<ssize_t>(s.length()),0);
+        }
+
+        PyObject * operator() (mapnik::value_unicode_string const& s) const
+        {
+            std::string buffer;
+            mapnik::to_utf8(s,buffer);
+            return ::PyUnicode_DecodeUTF8(buffer.c_str(),implicit_cast<ssize_t>(buffer.length()),0);
+        }
+
+        PyObject * operator() (mapnik::value_null const& /*s*/) const
+        {
+            Py_RETURN_NONE;
+        }
+    };
+
+
+    struct mapnik_value_to_python
+    {
+        static PyObject* convert(mapnik::value const& v)
+        {
+            return mapnik::util::apply_visitor(value_converter(),v);
+        }
+
+    };
+
+    struct mapnik_param_to_python
+    {
+        static PyObject* convert(mapnik::value_holder const& v)
+        {
+            return mapnik::util::apply_visitor(value_converter(),v);
+        }
+    };
+
+
+}}
+
+#endif // MAPNIK_PYTHON_BINDING_VALUE_CONVERTER_INCLUDED
diff --git a/src/mapnik_view_transform.cpp b/src/mapnik_view_transform.cpp
new file mode 100644
index 0000000..ee81914
--- /dev/null
+++ b/src/mapnik_view_transform.cpp
@@ -0,0 +1,92 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/view_transform.hpp>
+
+using mapnik::view_transform;
+
+struct view_transform_pickle_suite : boost::python::pickle_suite
+{
+    static boost::python::tuple
+    getinitargs(const view_transform& c)
+    {
+        using namespace boost::python;
+        return boost::python::make_tuple(c.width(),c.height(),c.extent());
+    }
+};
+
+namespace {
+
+mapnik::coord2d forward_point(mapnik::view_transform const& t, mapnik::coord2d const& in)
+{
+    mapnik::coord2d out(in);
+    t.forward(out);
+    return out;
+}
+
+mapnik::coord2d backward_point(mapnik::view_transform const& t, mapnik::coord2d const& in)
+{
+    mapnik::coord2d out(in);
+    t.backward(out);
+    return out;
+}
+
+mapnik::box2d<double> forward_envelope(mapnik::view_transform const& t, mapnik::box2d<double> const& in)
+{
+    return t.forward(in);
+}
+
+mapnik::box2d<double> backward_envelope(mapnik::view_transform const& t, mapnik::box2d<double> const& in)
+{
+    return t.backward(in);
+}
+}
+
+void export_view_transform()
+{
+    using namespace boost::python;
+    using mapnik::box2d;
+    using mapnik::coord2d;
+
+    class_<view_transform>("ViewTransform",init<int,int,box2d<double> const& > (
+                               "Create a ViewTransform with a width and height as integers and extent"))
+        .def_pickle(view_transform_pickle_suite())
+        .def("forward", forward_point)
+        .def("backward",backward_point)
+        .def("forward", forward_envelope)
+        .def("backward",backward_envelope)
+        .def("scale_x",&view_transform::scale_x)
+        .def("scale_y",&view_transform::scale_y)
+        ;
+}
diff --git a/src/python_grid_utils.cpp b/src/python_grid_utils.cpp
new file mode 100644
index 0000000..ec4c321
--- /dev/null
+++ b/src/python_grid_utils.cpp
@@ -0,0 +1,405 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#if defined(GRID_RENDERER)
+
+#include <mapnik/config.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/map.hpp>
+#include <mapnik/layer.hpp>
+#include <mapnik/debug.hpp>
+#include <mapnik/grid/grid_renderer.hpp>
+#include <mapnik/grid/grid.hpp>
+#include <mapnik/grid/grid_util.hpp>
+#include <mapnik/grid/grid_view.hpp>
+#include <mapnik/value_error.hpp>
+#include <mapnik/feature.hpp>
+#include <mapnik/feature_kv_iterator.hpp>
+#include "python_grid_utils.hpp"
+
+// stl
+#include <stdexcept>
+
+namespace mapnik {
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+                     boost::python::list& l,
+                     std::vector<typename T::lookup_type>& key_order)
+{
+    using keys_type = std::map< typename T::lookup_type, typename T::value_type>;
+    using keys_iterator = typename keys_type::iterator;
+
+    typename T::data_type const& data = grid_type.data();
+    typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys();
+    typename T::feature_key_type::const_iterator feature_pos;
+
+    keys_type keys;
+    // start counting at utf8 codepoint 32, aka space character
+    std::uint16_t codepoint = 32;
+
+    unsigned array_size = data.width();
+    for (unsigned y = 0; y < data.height(); ++y)
+    {
+        std::uint16_t idx = 0;
+        const std::unique_ptr<Py_UNICODE[]> line(new Py_UNICODE[array_size]);
+        typename T::value_type const* row = data.get_row(y);
+        for (unsigned x = 0; x < data.width(); ++x)
+        {
+            typename T::value_type feature_id = row[x];
+            feature_pos = feature_keys.find(feature_id);
+            if (feature_pos != feature_keys.end())
+            {
+                mapnik::grid::lookup_type val = feature_pos->second;
+                keys_iterator key_pos = keys.find(val);
+                if (key_pos == keys.end())
+                {
+                    // Create a new entry for this key. Skip the codepoints that
+                    // can't be encoded directly in JSON.
+                    if (codepoint == 34) ++codepoint;      // Skip "
+                    else if (codepoint == 92) ++codepoint; // Skip backslash
+                    if (feature_id == mapnik::grid::base_mask)
+                    {
+                        keys[""] = codepoint;
+                        key_order.push_back("");
+                    }
+                    else
+                    {
+                        keys[val] = codepoint;
+                        key_order.push_back(val);
+                    }
+                    line[idx++] = static_cast<Py_UNICODE>(codepoint);
+                    ++codepoint;
+                }
+                else
+                {
+                    line[idx++] = static_cast<Py_UNICODE>(key_pos->second);
+                }
+            }
+            // else, shouldn't get here...
+        }
+        l.append(boost::python::object(
+                     boost::python::handle<>(
+                         PyUnicode_FromUnicode(line.get(), array_size))));
+    }
+}
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+                     boost::python::list& l,
+                     std::vector<typename T::lookup_type>& key_order,
+                     unsigned int resolution)
+{
+    using keys_type = std::map< typename T::lookup_type, typename T::value_type>;
+    using keys_iterator = typename keys_type::iterator;
+
+    typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys();
+    typename T::feature_key_type::const_iterator feature_pos;
+
+    keys_type keys;
+    // start counting at utf8 codepoint 32, aka space character
+    std::uint16_t codepoint = 32;
+
+    unsigned array_size = std::ceil(grid_type.width()/static_cast<float>(resolution));
+    for (unsigned y = 0; y < grid_type.height(); y=y+resolution)
+    {
+        std::uint16_t idx = 0;
+        const std::unique_ptr<Py_UNICODE[]> line(new Py_UNICODE[array_size]);
+        mapnik::grid::value_type const* row = grid_type.get_row(y);
+        for (unsigned x = 0; x < grid_type.width(); x=x+resolution)
+        {
+            typename T::value_type feature_id = row[x];
+            feature_pos = feature_keys.find(feature_id);
+            if (feature_pos != feature_keys.end())
+            {
+                mapnik::grid::lookup_type val = feature_pos->second;
+                keys_iterator key_pos = keys.find(val);
+                if (key_pos == keys.end())
+                {
+                    // Create a new entry for this key. Skip the codepoints that
+                    // can't be encoded directly in JSON.
+                    if (codepoint == 34) ++codepoint;      // Skip "
+                    else if (codepoint == 92) ++codepoint; // Skip backslash
+                    if (feature_id == mapnik::grid::base_mask)
+                    {
+                        keys[""] = codepoint;
+                        key_order.push_back("");
+                    }
+                    else
+                    {
+                        keys[val] = codepoint;
+                        key_order.push_back(val);
+                    }
+                    line[idx++] = static_cast<Py_UNICODE>(codepoint);
+                    ++codepoint;
+                }
+                else
+                {
+                    line[idx++] = static_cast<Py_UNICODE>(key_pos->second);
+                }
+            }
+            // else, shouldn't get here...
+        }
+        l.append(boost::python::object(
+                     boost::python::handle<>(
+                         PyUnicode_FromUnicode(line.get(), array_size))));
+    }
+}
+
+
+template <typename T>
+void grid2utf2(T const& grid_type,
+                      boost::python::list& l,
+                      std::vector<typename T::lookup_type>& key_order,
+                      unsigned int resolution)
+{
+    using keys_type = std::map< typename T::lookup_type, typename T::value_type>;
+    using keys_iterator = typename keys_type::iterator;
+
+    typename T::data_type const& data = grid_type.data();
+    typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys();
+    typename T::feature_key_type::const_iterator feature_pos;
+
+    keys_type keys;
+    // start counting at utf8 codepoint 32, aka space character
+    uint16_t codepoint = 32;
+
+    mapnik::grid::data_type target(data.width()/resolution,data.height()/resolution);
+    mapnik::scale_grid(target,grid_type.data(),0.0,0.0);
+
+    unsigned array_size = target.width();
+    for (unsigned y = 0; y < target.height(); ++y)
+    {
+        uint16_t idx = 0;
+        const std::unique_ptr<Py_UNICODE[]> line(new Py_UNICODE[array_size]);
+        mapnik::grid::value_type * row = target.get_row(y);
+        unsigned x;
+        for (x = 0; x < target.width(); ++x)
+        {
+            feature_pos = feature_keys.find(row[x]);
+            if (feature_pos != feature_keys.end())
+            {
+                mapnik::grid::lookup_type val = feature_pos->second;
+                keys_iterator key_pos = keys.find(val);
+                if (key_pos == keys.end())
+                {
+                    // Create a new entry for this key. Skip the codepoints that
+                    // can't be encoded directly in JSON.
+                    if (codepoint == 34) ++codepoint;      // Skip "
+                    else if (codepoint == 92) ++codepoint; // Skip backslash
+                    keys[val] = codepoint;
+                    key_order.push_back(val);
+                    line[idx++] = static_cast<Py_UNICODE>(codepoint);
+                    ++codepoint;
+                }
+                else
+                {
+                    line[idx++] = static_cast<Py_UNICODE>(key_pos->second);
+                }
+            }
+            // else, shouldn't get here...
+        }
+        l.append(boost::python::object(
+                     boost::python::handle<>(
+                         PyUnicode_FromUnicode(line.get(), array_size))));
+    }
+}
+
+
+template <typename T>
+void write_features(T const& grid_type,
+                           boost::python::dict& feature_data,
+                           std::vector<typename T::lookup_type> const& key_order)
+{
+    typename T::feature_type const& g_features = grid_type.get_grid_features();
+    if (g_features.size() <= 0)
+    {
+        return;
+    }
+
+    std::set<std::string> const& attributes = grid_type.get_fields();
+    typename T::feature_type::const_iterator feat_end = g_features.end();
+    for ( std::string const& key_item :key_order )
+    {
+        if (key_item.empty())
+        {
+            continue;
+        }
+
+        typename T::feature_type::const_iterator feat_itr = g_features.find(key_item);
+        if (feat_itr == feat_end)
+        {
+            continue;
+        }
+
+        bool found = false;
+        boost::python::dict feat;
+        mapnik::feature_ptr feature = feat_itr->second;
+        for ( std::string const& attr : attributes )
+        {
+            if (attr == "__id__")
+            {
+                feat[attr.c_str()] = feature->id();
+            }
+            else if (feature->has_key(attr))
+            {
+                found = true;
+                feat[attr.c_str()] = feature->get(attr);
+            }
+        }
+
+        if (found)
+        {
+            feature_data[feat_itr->first] = feat;
+        }
+    }
+}
+
+template <typename T>
+void grid_encode_utf(T const& grid_type,
+                            boost::python::dict & json,
+                            bool add_features,
+                            unsigned int resolution)
+{
+    // convert buffer to utf and gather key order
+    boost::python::list l;
+    std::vector<typename T::lookup_type> key_order;
+
+    if (resolution != 1) {
+        // resample on the fly - faster, less accurate
+        mapnik::grid2utf<T>(grid_type,l,key_order,resolution);
+
+        // resample first - slower, more accurate
+        //mapnik::grid2utf2<T>(grid_type,l,key_order,resolution);
+    }
+    else
+    {
+        mapnik::grid2utf<T>(grid_type,l,key_order);
+    }
+
+    // convert key order to proper python list
+    boost::python::list keys_a;
+    for ( typename T::lookup_type const& key_id : key_order )
+    {
+        keys_a.append(key_id);
+    }
+
+    // gather feature data
+    boost::python::dict feature_data;
+    if (add_features) {
+        mapnik::write_features<T>(grid_type,feature_data,key_order);
+    }
+
+    json["grid"] = l;
+    json["keys"] = keys_a;
+    json["data"] = feature_data;
+
+}
+
+template <typename T>
+boost::python::dict grid_encode( T const& grid, std::string const& format, bool add_features, unsigned int resolution)
+{
+    if (format == "utf") {
+        boost::python::dict json;
+        grid_encode_utf<T>(grid,json,add_features,resolution);
+        return json;
+    }
+    else
+    {
+        std::stringstream s;
+        s << "'utf' is currently the only supported encoding format.";
+        throw mapnik::value_error(s.str());
+    }
+}
+
+template boost::python::dict grid_encode( mapnik::grid const& grid, std::string const& format, bool add_features, unsigned int resolution);
+template boost::python::dict grid_encode( mapnik::grid_view const& grid, std::string const& format, bool add_features, unsigned int resolution);
+
+void render_layer_for_grid(mapnik::Map const& map,
+                                  mapnik::grid & grid,
+                                  unsigned layer_idx,
+                                  boost::python::list const& fields,
+                                  double scale_factor,
+                                  unsigned offset_x,
+                                  unsigned offset_y)
+{
+    std::vector<mapnik::layer> const& layers = map.layers();
+    std::size_t layer_num = layers.size();
+    if (layer_idx >= layer_num) {
+        std::ostringstream s;
+        s << "Zero-based layer index '" << layer_idx << "' not valid, only '"
+          << layer_num << "' layers are in map\n";
+        throw std::runtime_error(s.str());
+    }
+
+    // convert python list to std::set
+    boost::python::ssize_t num_fields = boost::python::len(fields);
+    for(boost::python::ssize_t i=0; i<num_fields; i++) {
+        boost::python::extract<std::string> name(fields[i]);
+        if (name.check())
+        {
+            grid.add_field(name());
+        }
+        else
+        {
+            std::stringstream s;
+            s << "list of field names must be strings";
+            throw mapnik::value_error(s.str());
+        }
+    }
+
+    // copy field names
+    std::set<std::string> attributes = grid.get_fields();
+    // todo - make this a static constant
+    std::string known_id_key = "__id__";
+    if (attributes.find(known_id_key) != attributes.end())
+    {
+        attributes.erase(known_id_key);
+    }
+
+    std::string join_field = grid.get_key();
+    if (known_id_key != join_field &&
+        attributes.find(join_field) == attributes.end())
+    {
+        attributes.insert(join_field);
+    }
+
+    mapnik::grid_renderer<mapnik::grid> ren(map,grid,scale_factor,offset_x,offset_y);
+    mapnik::layer const& layer = layers[layer_idx];
+    ren.apply(layer,attributes);
+}
+
+}
+
+#endif
diff --git a/src/python_grid_utils.hpp b/src/python_grid_utils.hpp
new file mode 100644
index 0000000..a15a026
--- /dev/null
+++ b/src/python_grid_utils.hpp
@@ -0,0 +1,79 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_PYTHON_BINDING_GRID_UTILS_INCLUDED
+#define MAPNIK_PYTHON_BINDING_GRID_UTILS_INCLUDED
+
+// boost
+#include <boost/python.hpp>
+
+// mapnik
+#include <mapnik/map.hpp>
+#include <mapnik/grid/grid.hpp>
+
+namespace mapnik {
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+                     boost::python::list& l,
+                     std::vector<typename T::lookup_type>& key_order);
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+                     boost::python::list& l,
+                     std::vector<typename T::lookup_type>& key_order,
+                     unsigned int resolution);
+
+
+template <typename T>
+void grid2utf2(T const& grid_type,
+                      boost::python::list& l,
+                      std::vector<typename T::lookup_type>& key_order,
+                      unsigned int resolution);
+
+
+template <typename T>
+void write_features(T const& grid_type,
+                           boost::python::dict& feature_data,
+                           std::vector<typename T::lookup_type> const& key_order);
+
+template <typename T>
+void grid_encode_utf(T const& grid_type,
+                            boost::python::dict & json,
+                            bool add_features,
+                            unsigned int resolution);
+
+template <typename T>
+boost::python::dict grid_encode( T const& grid, std::string const& format, bool add_features, unsigned int resolution);
+
+void render_layer_for_grid(const mapnik::Map& map,
+                           mapnik::grid& grid,
+                           unsigned layer_idx, // TODO - layer by name or index
+                           boost::python::list const& fields,
+                           double scale_factor,
+                           unsigned offset_x,
+                           unsigned offset_y);
+
+}
+
+#endif // MAPNIK_PYTHON_BINDING_GRID_UTILS_INCLUDED
diff --git a/src/python_optional.hpp b/src/python_optional.hpp
new file mode 100644
index 0000000..45db528
--- /dev/null
+++ b/src/python_optional.hpp
@@ -0,0 +1,198 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+
+#include <boost/optional/optional.hpp>
+#include <boost/python.hpp>
+
+#include <mapnik/util/noncopyable.hpp>
+
+// boost::optional<T> to/from converter from John Wiegley
+
+template <typename T, typename TfromPy>
+struct object_from_python
+{
+    object_from_python() {
+        boost::python::converter::registry::push_back
+            (&TfromPy::convertible, &TfromPy::construct,
+             boost::python::type_id<T>());
+    }
+};
+
+template <typename T, typename TtoPy, typename TfromPy>
+struct register_python_conversion
+{
+    register_python_conversion() {
+        boost::python::to_python_converter<T, TtoPy>();
+        object_from_python<T, TfromPy>();
+    }
+};
+
+template <typename T>
+struct python_optional : public mapnik::util::noncopyable
+{
+    struct optional_to_python
+    {
+        static PyObject * convert(const boost::optional<T>& value)
+        {
+            return (value ? boost::python::to_python_value<T>()(*value) :
+                    boost::python::detail::none());
+        }
+    };
+
+    struct optional_from_python
+    {
+        static void * convertible(PyObject * source)
+        {
+            using namespace boost::python::converter;
+
+            if (source == Py_None)
+                return source;
+
+            const registration& converters(registered<T>::converters);
+
+            if (implicit_rvalue_convertible_from_python(source,
+                                                        converters)) {
+                rvalue_from_python_stage1_data data =
+                    rvalue_from_python_stage1(source, converters);
+                return rvalue_from_python_stage2(source, data, converters);
+            }
+            return 0;
+        }
+
+        static void construct(PyObject * source,
+                              boost::python::converter::rvalue_from_python_stage1_data * data)
+        {
+            using namespace boost::python::converter;
+
+            void * const storage = ((rvalue_from_python_storage<T> *)
+                                    data)->storage.bytes;
+
+            if (data->convertible == source)        // == None
+                new (storage) boost::optional<T>(); // A Boost uninitialized value
+            else
+                new (storage) boost::optional<T>(*static_cast<T *>(data->convertible));
+
+            data->convertible = storage;
+        }
+    };
+
+    explicit python_optional()
+    {
+        register_python_conversion<boost::optional<T>,
+            optional_to_python, optional_from_python>();
+    }
+};
+
+// to/from boost::optional<bool>
+template <>
+struct python_optional<float> : public mapnik::util::noncopyable
+{
+    struct optional_to_python
+    {
+        static PyObject * convert(const boost::optional<float>& value)
+        {
+            return (value ? PyFloat_FromDouble(*value) :
+                    boost::python::detail::none());
+        }
+    };
+
+    struct optional_from_python
+    {
+        static void * convertible(PyObject * source)
+        {
+            using namespace boost::python::converter;
+
+            if (source == Py_None || PyFloat_Check(source))
+                return source;
+            return 0;
+        }
+
+        static void construct(PyObject * source,
+                              boost::python::converter::rvalue_from_python_stage1_data * data)
+        {
+            using namespace boost::python::converter;
+            void * const storage = ((rvalue_from_python_storage<boost::optional<bool> > *)
+                                    data)->storage.bytes;
+            if (source == Py_None)  // == None
+                new (storage) boost::optional<float>(); // A Boost uninitialized value
+            else
+                new (storage) boost::optional<float>(PyFloat_AsDouble(source));
+            data->convertible = storage;
+        }
+    };
+
+    explicit python_optional()
+    {
+        register_python_conversion<boost::optional<float>,
+            optional_to_python, optional_from_python>();
+    }
+};
+
+// to/from boost::optional<float>
+template <>
+struct python_optional<bool> : public mapnik::util::noncopyable
+{
+    struct optional_to_python
+    {
+        static PyObject * convert(const boost::optional<bool>& value)
+        {
+            if (value)
+            {
+                if (*value) Py_RETURN_TRUE;
+                else Py_RETURN_FALSE;
+            }
+            else return boost::python::detail::none();
+        }
+    };
+    struct optional_from_python
+    {
+        static void * convertible(PyObject * source)
+        {
+            using namespace boost::python::converter;
+
+            if (source == Py_None || PyBool_Check(source))
+                return source;
+            return 0;
+        }
+
+        static void construct(PyObject * source,
+                              boost::python::converter::rvalue_from_python_stage1_data * data)
+        {
+            using namespace boost::python::converter;
+            void * const storage = ((rvalue_from_python_storage<boost::optional<bool> > *)
+                                    data)->storage.bytes;
+            if (source == Py_None)  // == None
+                new (storage) boost::optional<bool>(); // A Boost uninitialized value
+            else
+            {
+                new (storage) boost::optional<bool>(source == Py_True ? true : false);
+            }
+            data->convertible = storage;
+        }
+    };
+
+    explicit python_optional()
+    {
+        register_python_conversion<boost::optional<bool>,
+            optional_to_python, optional_from_python>();
+    }
+};
diff --git a/src/python_to_value.hpp b/src/python_to_value.hpp
new file mode 100644
index 0000000..6ad9250
--- /dev/null
+++ b/src/python_to_value.hpp
@@ -0,0 +1,122 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_PYTHON_BINDING_PYTHON_TO_VALUE
+#define MAPNIK_PYTHON_BINDING_PYTHON_TO_VALUE
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/value.hpp>
+#include <mapnik/unicode.hpp>
+#include <mapnik/attribute.hpp>
+
+namespace mapnik {
+
+    static mapnik::attributes dict2attr(boost::python::dict const& d)
+    {
+        using namespace boost::python;
+        mapnik::attributes vars;
+        mapnik::transcoder tr_("utf8");
+        boost::python::list keys=d.keys();
+        for (int i=0; i < len(keys); ++i)
+        {
+            std::string key;
+            object obj_key = keys[i];
+            if (PyUnicode_Check(obj_key.ptr()))
+            {
+                PyObject* temp = PyUnicode_AsUTF8String(obj_key.ptr());
+                if (temp)
+                {
+    #if PY_VERSION_HEX >= 0x03000000
+                    char* c_str = PyBytes_AsString(temp);
+    #else
+                    char* c_str = PyString_AsString(temp);
+    #endif
+                    key = c_str;
+                    Py_DecRef(temp);
+                }
+            }
+            else
+            {
+                key = extract<std::string>(keys[i]);
+            }
+            object obj = d[key];
+            if (PyUnicode_Check(obj.ptr()))
+            {
+                PyObject* temp = PyUnicode_AsUTF8String(obj.ptr());
+                if (temp)
+                {
+    #if PY_VERSION_HEX >= 0x03000000
+                    char* c_str = PyBytes_AsString(temp);
+    #else
+                    char* c_str = PyString_AsString(temp);
+    #endif
+                    vars[key] = tr_.transcode(c_str);
+                    Py_DecRef(temp);
+                }
+                continue;
+            }
+
+            if (PyBool_Check(obj.ptr()))
+            {
+                extract<mapnik::value_bool> ex(obj);
+                if (ex.check())
+                {
+                    vars[key] = ex();
+                }
+            }
+            else if (PyFloat_Check(obj.ptr()))
+            {
+                extract<mapnik::value_double> ex(obj);
+                if (ex.check())
+                {
+                    vars[key] = ex();
+                }
+            }
+            else
+            {
+                extract<mapnik::value_integer> ex(obj);
+                if (ex.check())
+                {
+                    vars[key] = ex();
+                }
+                else
+                {
+                    extract<std::string> ex0(obj);
+                    if (ex0.check())
+                    {
+                        vars[key] = tr_.transcode(ex0().c_str());
+                    }
+                }
+            }
+        }
+        return vars;
+    }
+}
+
+#endif // MAPNIK_PYTHON_BINDING_PYTHON_TO_VALUE
diff --git a/test/python_tests/__init__.py b/test/python_tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/python_tests/agg_rasterizer_integer_overflow_test.py b/test/python_tests/agg_rasterizer_integer_overflow_test.py
new file mode 100644
index 0000000..bfd8128
--- /dev/null
+++ b/test/python_tests/agg_rasterizer_integer_overflow_test.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+import json
+
+# geojson box of the world
+geojson  = { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -17963313.143242701888084, -6300857.11560364998877 ], [ -17963313.143242701888084, 13071343.332991421222687 ], [ 7396658.353099936619401, 13071343.332991421222687 ], [ 7396658.353099936619401, -6300857.11560364998877 ], [ -17963313.143242701888084, -6300857.11560364998877 ] ] ] } }
+
+def test_that_coordinates_do_not_overflow_and_polygon_is_rendered_memory():
+  expected_color = mapnik.Color('white')
+  projection = '+init=epsg:4326'
+  ds = mapnik.MemoryDatasource()
+  context = mapnik.Context()
+  feat = mapnik.Feature.from_geojson(json.dumps(geojson),context)
+  ds.add_feature(feat)
+  s = mapnik.Style()
+  r = mapnik.Rule()
+  sym = mapnik.PolygonSymbolizer()
+  sym.fill = expected_color
+  r.symbols.append(sym)
+  s.rules.append(r)
+  lyr = mapnik.Layer('Layer',projection)
+  lyr.datasource = ds
+  lyr.styles.append('style')
+  m = mapnik.Map(256,256,projection)
+  m.background_color = mapnik.Color('green')
+  m.append_style('style',s)
+  m.layers.append(lyr)
+  # 17/20864/45265.png
+  m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6197514.253362091,-13657768.213995293,6198125.749588372))
+  # works 15/5216/11316.png
+  #m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6195679.764683247,-13655933.72531645,6198125.749588372))
+  im = mapnik.Image(256,256)
+  mapnik.render(m,im)
+  eq_(im.get_pixel(128,128),expected_color.packed())
+
+def test_that_coordinates_do_not_overflow_and_polygon_is_rendered_csv():
+  expected_color = mapnik.Color('white')
+  projection = '+init=epsg:4326'
+  ds = mapnik.MemoryDatasource()
+  context = mapnik.Context()
+  feat = mapnik.Feature.from_geojson(json.dumps(geojson),context)
+  ds.add_feature(feat)
+  geojson_string = "geojson\n'%s'" % json.dumps(geojson['geometry'])
+  ds = mapnik.Datasource(**{'type':'csv','inline':geojson_string})
+  s = mapnik.Style()
+  r = mapnik.Rule()
+  sym = mapnik.PolygonSymbolizer()
+  sym.fill = expected_color
+  r.symbols.append(sym)
+  s.rules.append(r)
+  lyr = mapnik.Layer('Layer',projection)
+  lyr.datasource = ds
+  lyr.styles.append('style')
+  m = mapnik.Map(256,256,projection)
+  m.background_color = mapnik.Color('green')
+  m.append_style('style',s)
+  m.layers.append(lyr)
+  # 17/20864/45265.png
+  m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6197514.253362091,-13657768.213995293,6198125.749588372))
+  # works 15/5216/11316.png
+  #m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6195679.764683247,-13655933.72531645,6198125.749588372))
+  im = mapnik.Image(256,256)
+  mapnik.render(m,im)
+  eq_(im.get_pixel(128,128),expected_color.packed())
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/box2d_test.py b/test/python_tests/box2d_test.py
new file mode 100644
index 0000000..c441002
--- /dev/null
+++ b/test/python_tests/box2d_test.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_true,assert_almost_equal,assert_false
+from utilities import run_all
+import mapnik
+
+def test_coord_init():
+    c = mapnik.Coord(100, 100)
+
+    eq_(c.x, 100)
+    eq_(c.y, 100)
+
+def test_coord_multiplication():
+    c = mapnik.Coord(100, 100)
+    c *= 2
+
+    eq_(c.x, 200)
+    eq_(c.y, 200)
+
+def test_envelope_init():
+    e = mapnik.Box2d(100, 100, 200, 200)
+
+    assert_true(e.contains(100, 100))
+    assert_true(e.contains(100, 200))
+    assert_true(e.contains(200, 200))
+    assert_true(e.contains(200, 100))
+
+    assert_true(e.contains(e.center()))
+
+    assert_false(e.contains(99.9, 99.9))
+    assert_false(e.contains(99.9, 200.1))
+    assert_false(e.contains(200.1, 200.1))
+    assert_false(e.contains(200.1, 99.9))
+
+    eq_(e.width(), 100)
+    eq_(e.height(), 100)
+
+    eq_(e.minx, 100)
+    eq_(e.miny, 100)
+
+    eq_(e.maxx, 200)
+    eq_(e.maxy, 200)
+
+    eq_(e[0],100)
+    eq_(e[1],100)
+    eq_(e[2],200)
+    eq_(e[3],200)
+    eq_(e[0],e[-4])
+    eq_(e[1],e[-3])
+    eq_(e[2],e[-2])
+    eq_(e[3],e[-1])
+
+    c = e.center()
+
+    eq_(c.x, 150)
+    eq_(c.y, 150)
+
+def test_envelope_static_init():
+    e = mapnik.Box2d.from_string('100 100 200 200')
+    e2 = mapnik.Box2d.from_string('100,100,200,200')
+    e3 = mapnik.Box2d.from_string('100 , 100 , 200 , 200')
+    eq_(e,e2)
+    eq_(e,e3)
+
+    assert_true(e.contains(100, 100))
+    assert_true(e.contains(100, 200))
+    assert_true(e.contains(200, 200))
+    assert_true(e.contains(200, 100))
+
+    assert_true(e.contains(e.center()))
+
+    assert_false(e.contains(99.9, 99.9))
+    assert_false(e.contains(99.9, 200.1))
+    assert_false(e.contains(200.1, 200.1))
+    assert_false(e.contains(200.1, 99.9))
+
+    eq_(e.width(), 100)
+    eq_(e.height(), 100)
+
+    eq_(e.minx, 100)
+    eq_(e.miny, 100)
+
+    eq_(e.maxx, 200)
+    eq_(e.maxy, 200)
+
+    eq_(e[0],100)
+    eq_(e[1],100)
+    eq_(e[2],200)
+    eq_(e[3],200)
+    eq_(e[0],e[-4])
+    eq_(e[1],e[-3])
+    eq_(e[2],e[-2])
+    eq_(e[3],e[-1])
+
+    c = e.center()
+
+    eq_(c.x, 150)
+    eq_(c.y, 150)
+
+def test_envelope_multiplication():
+    # no width then no impact of multiplication
+    a = mapnik.Box2d(100, 100, 100, 100)
+    a *= 5
+    eq_(a.minx,100)
+    eq_(a.miny,100)
+    eq_(a.maxx,100)
+    eq_(a.maxy,100)
+
+    a = mapnik.Box2d(100.0, 100.0, 100.0, 100.0)
+    a *= 5
+    eq_(a.minx,100)
+    eq_(a.miny,100)
+    eq_(a.maxx,100)
+    eq_(a.maxy,100)
+
+    a = mapnik.Box2d(100.0, 100.0, 100.001, 100.001)
+    a *= 5
+    assert_almost_equal(a.minx, 99.9979, places=3)
+    assert_almost_equal(a.miny, 99.9979, places=3)
+    assert_almost_equal(a.maxx, 100.0030, places=3)
+    assert_almost_equal(a.maxy, 100.0030, places=3)
+
+    e = mapnik.Box2d(100, 100, 200, 200)
+    e *= 2
+    eq_(e.minx,50)
+    eq_(e.miny,50)
+    eq_(e.maxx,250)
+    eq_(e.maxy,250)
+
+    assert_true(e.contains(50, 50))
+    assert_true(e.contains(50, 250))
+    assert_true(e.contains(250, 250))
+    assert_true(e.contains(250, 50))
+
+    assert_false(e.contains(49.9, 49.9))
+    assert_false(e.contains(49.9, 250.1))
+    assert_false(e.contains(250.1, 250.1))
+    assert_false(e.contains(250.1, 49.9))
+
+    assert_true(e.contains(e.center()))
+
+    eq_(e.width(), 200)
+    eq_(e.height(), 200)
+
+    eq_(e.minx, 50)
+    eq_(e.miny, 50)
+
+    eq_(e.maxx, 250)
+    eq_(e.maxy, 250)
+
+    c = e.center()
+
+    eq_(c.x, 150)
+    eq_(c.y, 150)
+
+def test_envelope_clipping():
+    e1 = mapnik.Box2d(-180,-90,180,90)
+    e2 = mapnik.Box2d(-120,40,-110,48)
+    e1.clip(e2)
+    eq_(e1,e2)
+
+    # madagascar in merc
+    e1 = mapnik.Box2d(4772116.5490, -2744395.0631, 5765186.4203, -1609458.0673)
+    e2 = mapnik.Box2d(5124338.3753, -2240522.1727, 5207501.8621, -2130452.8520)
+    e1.clip(e2)
+    eq_(e1,e2)
+
+    # nz in lon/lat
+    e1 = mapnik.Box2d(163.8062, -47.1897, 179.3628, -33.9069)
+    e2 = mapnik.Box2d(173.7378, -39.6395, 174.4849, -38.9252)
+    e1.clip(e2)
+    eq_(e1,e2)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/buffer_clear_test.py b/test/python_tests/buffer_clear_test.py
new file mode 100644
index 0000000..b4b3bda
--- /dev/null
+++ b/test/python_tests/buffer_clear_test.py
@@ -0,0 +1,61 @@
+import os, mapnik
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_clearing_image_data():
+    im = mapnik.Image(256,256)
+    # make sure it equals itself
+    bytes = im.tostring()
+    eq_(im.tostring(),bytes)
+    # set background, then clear
+    im.fill(mapnik.Color('green'))
+    eq_(im.tostring()!=bytes,True)
+    # clear image, should now equal original
+    im.clear()
+    eq_(im.tostring(),bytes)
+
+def make_map():
+    ds = mapnik.MemoryDatasource()
+    context = mapnik.Context()
+    context.push('Name')
+    pixel_key = 1
+    f = mapnik.Feature(context,pixel_key)
+    f['Name'] = str(pixel_key)
+    f.geometry=mapnik.Geometry.from_wkt('POLYGON ((0 0, 0 256, 256 256, 256 0, 0 0))')
+    ds.add_feature(f)
+    s = mapnik.Style()
+    r = mapnik.Rule()
+    symb = mapnik.PolygonSymbolizer()
+    r.symbols.append(symb)
+    s.rules.append(r)
+    lyr = mapnik.Layer('Places')
+    lyr.datasource = ds
+    lyr.styles.append('places_labels')
+    width,height = 256,256
+    m = mapnik.Map(width,height)
+    m.append_style('places_labels',s)
+    m.layers.append(lyr)
+    m.zoom_all()
+    return m
+
+if mapnik.has_grid_renderer():
+    def test_clearing_grid_data():
+        g = mapnik.Grid(256,256)
+        utf = g.encode()
+        # make sure it equals itself
+        eq_(g.encode(),utf)
+        m = make_map()
+        mapnik.render_layer(m,g,layer=0,fields=['__id__','Name'])
+        eq_(g.encode()!=utf,True)
+        # clear grid, should now match original
+        g.clear()
+        eq_(g.encode(),utf)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/cairo_test.py b/test/python_tests/cairo_test.py
new file mode 100644
index 0000000..3c626d4
--- /dev/null
+++ b/test/python_tests/cairo_test.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+
+import os
+import shutil
+import mapnik
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def make_tmp_map():
+    m = mapnik.Map(512,512)
+    m.background_color = mapnik.Color('steelblue')
+    ds = mapnik.MemoryDatasource()
+    context = mapnik.Context()
+    context.push('Name')
+    f = mapnik.Feature(context,1)
+    f['Name'] = 'Hello'
+    f.geometry = mapnik.Geometry.from_wkt('POINT (0 0)')
+    ds.add_feature(f)
+    s = mapnik.Style()
+    r = mapnik.Rule()
+    sym = mapnik.MarkersSymbolizer()
+    sym.allow_overlap = True
+    r.symbols.append(sym)
+    s.rules.append(r)
+    lyr = mapnik.Layer('Layer')
+    lyr.datasource = ds
+    lyr.styles.append('style')
+    m.append_style('style',s)
+    m.layers.append(lyr)
+    return m
+
+def draw_title(m,ctx,text,size=10,color=mapnik.Color('black')):
+    """ Draw a Map Title near the top of a page."""
+    middle = m.width/2.0
+    ctx.set_source_rgba(*cairo_color(color))
+    ctx.select_font_face("DejaVu Sans Book", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+    ctx.set_font_size(size)
+    x_bearing, y_bearing, width, height = ctx.text_extents(text)[:4]
+    ctx.move_to(middle - width / 2 - x_bearing, 20.0 - height / 2 - y_bearing)
+    ctx.show_text(text)
+
+def draw_neatline(m,ctx):
+    w,h = m.width, m.height
+    ctx.set_source_rgba(*cairo_color(mapnik.Color('black')))
+    outline = [
+      [0,0],[w,0],[w,h],[0,h]
+    ]
+    ctx.set_line_width(1)
+    for idx,pt in enumerate(outline):
+        if (idx == 0):
+          ctx.move_to(*pt)
+        else:
+          ctx.line_to(*pt)
+    ctx.close_path()
+    inset = 6
+    inline = [
+      [inset,inset],[w-inset,inset],[w-inset,h-inset],[inset,h-inset]
+    ]
+    ctx.set_line_width(inset/2)
+    for idx,pt in enumerate(inline):
+        if (idx == 0):
+          ctx.move_to(*pt)
+        else:
+          ctx.line_to(*pt)
+    ctx.close_path()
+    ctx.stroke()
+
+def cairo_color(c):
+    """ Return a Cairo color tuple from a Mapnik Color."""
+    ctx_c = (c.r/255.0,c.g/255.0,c.b/255.0,c.a/255.0)
+    return ctx_c
+
+if mapnik.has_pycairo():
+    import cairo
+
+    def test_passing_pycairo_context_svg():
+        m = make_tmp_map()
+        m.zoom_to_box(mapnik.Box2d(-180,-90,180,90))
+        test_cairo_file = '/tmp/mapnik-cairo-context-test.svg'
+        surface = cairo.SVGSurface(test_cairo_file, m.width, m.height)
+        expected_cairo_file = './images/pycairo/cairo-cairo-expected.svg'
+        context = cairo.Context(surface)
+        mapnik.render(m,context)
+        draw_title(m,context,"Hello Map",size=20)
+        draw_neatline(m,context)
+        surface.finish()
+        if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+            print 'generated expected cairo surface file %s' % expected_cairo_file
+            shutil.copy(test_cairo_file,expected_cairo_file)
+        diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+        msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+        eq_( diff < 1500, True, msg)
+        os.remove(test_cairo_file)
+
+    def test_passing_pycairo_context_pdf():
+        m = make_tmp_map()
+        m.zoom_to_box(mapnik.Box2d(-180,-90,180,90))
+        test_cairo_file = '/tmp/mapnik-cairo-context-test.pdf'
+        surface = cairo.PDFSurface(test_cairo_file, m.width, m.height)
+        expected_cairo_file = './images/pycairo/cairo-cairo-expected.pdf'
+        context = cairo.Context(surface)
+        mapnik.render(m,context)
+        draw_title(m,context,"Hello Map",size=20)
+        draw_neatline(m,context)
+        surface.finish()
+        if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+            print 'generated expected cairo surface file %s' % expected_cairo_file
+            shutil.copy(test_cairo_file,expected_cairo_file)
+        diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+        msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+        eq_( diff < 1500, True, msg)
+        os.remove(test_cairo_file)
+
+    def test_passing_pycairo_context_png():
+        m = make_tmp_map()
+        m.zoom_to_box(mapnik.Box2d(-180,-90,180,90))
+        test_cairo_file = '/tmp/mapnik-cairo-context-test.png'
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, m.width, m.height)
+        expected_cairo_file = './images/pycairo/cairo-cairo-expected.png'
+        expected_cairo_file2 = './images/pycairo/cairo-cairo-expected-reduced.png'
+        context = cairo.Context(surface)
+        mapnik.render(m,context)
+        draw_title(m,context,"Hello Map",size=20)
+        draw_neatline(m,context)
+        surface.write_to_png(test_cairo_file)
+        reduced_color_image = test_cairo_file.replace('png','-mapnik.png')
+        im = mapnik.Image.from_cairo(surface)
+        im.save(reduced_color_image,'png8')
+        surface.finish()
+        if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+            print 'generated expected cairo surface file %s' % expected_cairo_file
+            shutil.copy(test_cairo_file,expected_cairo_file)
+        diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+        msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+        eq_( diff < 500, True, msg)
+        os.remove(test_cairo_file)
+        if not os.path.exists(expected_cairo_file2) or os.environ.get('UPDATE'):
+            print 'generated expected cairo surface file %s' % expected_cairo_file2
+            shutil.copy(reduced_color_image,expected_cairo_file2)
+        diff = abs(os.stat(expected_cairo_file2).st_size-os.stat(reduced_color_image).st_size)
+        msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,reduced_color_image,'tests/python_tests/'+ expected_cairo_file2)
+        eq_( diff < 500, True, msg)
+        os.remove(reduced_color_image)
+
+    if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+        def _pycairo_surface(type,sym):
+                test_cairo_file = '/tmp/mapnik-cairo-surface-test.%s.%s' % (sym,type)
+                expected_cairo_file = './images/pycairo/cairo-surface-expected.%s.%s' % (sym,type)
+                m = mapnik.Map(256,256)
+                mapnik.load_map(m,'../data/good_maps/%s_symbolizer.xml' % sym)
+                m.zoom_all()
+                if hasattr(cairo,'%sSurface' % type.upper()):
+                    surface = getattr(cairo,'%sSurface' % type.upper())(test_cairo_file, m.width,m.height)
+                    mapnik.render(m, surface)
+                    surface.finish()
+                    if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+                        print 'generated expected cairo surface file %s' % expected_cairo_file
+                        shutil.copy(test_cairo_file,expected_cairo_file)
+                    diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+                    msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+                    if os.uname()[0] == 'Darwin':
+                        eq_( diff < 2100, True, msg)
+                    else:
+                        eq_( diff < 23000, True, msg)
+                    os.remove(test_cairo_file)
+                    return True
+                else:
+                    print 'skipping cairo.%s test since surface is not available' % type.upper()
+                    return True
+
+        def test_pycairo_svg_surface1():
+            eq_(_pycairo_surface('svg','point'),True)
+
+        def test_pycairo_svg_surface2():
+            eq_(_pycairo_surface('svg','building'),True)
+
+        def test_pycairo_svg_surface3():
+            eq_(_pycairo_surface('svg','polygon'),True)
+
+        def test_pycairo_pdf_surface1():
+            eq_(_pycairo_surface('pdf','point'),True)
+
+        def test_pycairo_pdf_surface2():
+            eq_(_pycairo_surface('pdf','building'),True)
+
+        def test_pycairo_pdf_surface3():
+            eq_(_pycairo_surface('pdf','polygon'),True)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/color_test.py b/test/python_tests/color_test.py
new file mode 100644
index 0000000..900faf1
--- /dev/null
+++ b/test/python_tests/color_test.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import os, mapnik
+from timeit import Timer, time
+from nose.tools import *
+from utilities import execution_path, run_all, get_unique_colors
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_color_init():
+    c = mapnik.Color(12, 128, 255)
+    eq_(c.r, 12)
+    eq_(c.g, 128)
+    eq_(c.b, 255)
+    eq_(c.a, 255)
+    eq_(False, c.get_premultiplied())
+    c = mapnik.Color(16, 32, 64, 128)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(False, c.get_premultiplied())
+    c = mapnik.Color(16, 32, 64, 128,True)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(True, c.get_premultiplied())
+    c = mapnik.Color('rgba(16,32,64,0.5)')
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(False, c.get_premultiplied())
+    c = mapnik.Color('rgba(16,32,64,0.5)', True)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(True, c.get_premultiplied())
+    hex_str = '#10204080'
+    c = mapnik.Color(hex_str)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(hex_str, c.to_hex_string())
+    eq_(False, c.get_premultiplied())
+    c = mapnik.Color(hex_str, True)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(hex_str, c.to_hex_string())
+    eq_(True, c.get_premultiplied())
+    rgba_int = 2151686160
+    c = mapnik.Color(rgba_int)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(rgba_int, c.packed())
+    eq_(False, c.get_premultiplied())
+    c = mapnik.Color(rgba_int, True)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    eq_(rgba_int, c.packed())
+    eq_(True, c.get_premultiplied())
+
+def test_color_properties():
+    c = mapnik.Color(16, 32, 64, 128)
+    eq_(c.r, 16)
+    eq_(c.g, 32)
+    eq_(c.b, 64)
+    eq_(c.a, 128)
+    c.r = 17
+    eq_(c.r, 17)
+    c.g = 33
+    eq_(c.g, 33)
+    c.b = 65
+    eq_(c.b, 65)
+    c.a = 128
+    eq_(c.a, 128)
+
+def test_color_premultiply():
+    c = mapnik.Color(16, 33, 255, 128)
+    eq_(c.premultiply(), True)
+    eq_(c.r, 8)
+    eq_(c.g, 17)
+    eq_(c.b, 128)
+    eq_(c.a, 128)
+    # Repeating it again should do nothing
+    eq_(c.premultiply(), False)
+    eq_(c.r, 8)
+    eq_(c.g, 17)
+    eq_(c.b, 128)
+    eq_(c.a, 128)
+    c.demultiply()
+    c.demultiply()
+    # This will not return the same values as before but we expect that
+    eq_(c.r,15)
+    eq_(c.g,33)
+    eq_(c.b,255)
+    eq_(c.a,128)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/compare_test.py b/test/python_tests/compare_test.py
new file mode 100644
index 0000000..f4b6563
--- /dev/null
+++ b/test/python_tests/compare_test.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import *
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_another_compare():
+    im = mapnik.Image(5,5)
+    im2 = mapnik.Image(5,5)
+    im2.fill(mapnik.Color('rgba(255,255,255,0)'))
+    eq_(im.compare(im2,16), im.width() * im.height())
+
+def test_compare_rgba8():
+    im = mapnik.Image(5,5,mapnik.ImageType.rgba8)
+    im.fill(mapnik.Color(0,0,0,0))
+    eq_(im.compare(im), 0)
+    im2 = mapnik.Image(5,5,mapnik.ImageType.rgba8)
+    im2.fill(mapnik.Color(0,0,0,0))
+    eq_(im.compare(im2), 0)
+    eq_(im2.compare(im), 0)
+    im2.fill(mapnik.Color(0,0,0,12))
+    eq_(im.compare(im2), 25)
+    eq_(im.compare(im2, 0, False), 0)
+    im3 = mapnik.Image(5,5,mapnik.ImageType.rgba8)
+    im3.set_pixel(0,0, mapnik.Color(0,0,0,0))
+    im3.set_pixel(0,1, mapnik.Color(1,1,1,1))
+    im3.set_pixel(1,0, mapnik.Color(2,2,2,2))
+    im3.set_pixel(1,1, mapnik.Color(3,3,3,3))
+    eq_(im.compare(im3), 3)
+    eq_(im.compare(im3,1),2)
+    eq_(im.compare(im3,2),1)
+    eq_(im.compare(im3,3),0)
+
+def test_compare_2_image():
+    im = mapnik.Image(5,5)
+    im.set_pixel(0,0, mapnik.Color(254, 254, 254, 254))
+    im.set_pixel(4,4, mapnik.Color('white'))
+    im2 = mapnik.Image(5,5)
+    eq_(im2.compare(im,16), 2)
+
+def test_compare_dimensions():
+    im = mapnik.Image(2,2)
+    im2 = mapnik.Image(3,3)
+    eq_(im.compare(im2), 4)
+    eq_(im2.compare(im), 9)
+
+def test_compare_gray8():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray8)
+    im.fill(0)
+    eq_(im.compare(im), 0)
+    im2 = mapnik.Image(2,2,mapnik.ImageType.gray8)
+    im2.fill(0)
+    eq_(im.compare(im2), 0)
+    eq_(im2.compare(im), 0)
+    eq_(im.compare(im2, 0, False), 0)
+    im3 = mapnik.Image(2,2,mapnik.ImageType.gray8)
+    im3.set_pixel(0,0,0)
+    im3.set_pixel(0,1,1)
+    im3.set_pixel(1,0,2)
+    im3.set_pixel(1,1,3)
+    eq_(im.compare(im3),3)
+    eq_(im.compare(im3,1),2)
+    eq_(im.compare(im3,2),1)
+    eq_(im.compare(im3,3),0)
+
+def test_compare_gray16():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    im.fill(0)
+    eq_(im.compare(im), 0)
+    im2 = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    im2.fill(0)
+    eq_(im.compare(im2), 0)
+    eq_(im2.compare(im), 0)
+    eq_(im.compare(im2, 0, False), 0)
+    im3 = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    im3.set_pixel(0,0,0)
+    im3.set_pixel(0,1,1)
+    im3.set_pixel(1,0,2)
+    im3.set_pixel(1,1,3)
+    eq_(im.compare(im3),3)
+    eq_(im.compare(im3,1),2)
+    eq_(im.compare(im3,2),1)
+    eq_(im.compare(im3,3),0)
+
+def test_compare_gray32f():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+    im.fill(0.5)
+    eq_(im.compare(im), 0)
+    im2 = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+    im2.fill(0.5)
+    eq_(im.compare(im2), 0)
+    eq_(im2.compare(im), 0)
+    eq_(im.compare(im2, 0, False), 0)
+    im3 = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+    im3.set_pixel(0,0,0.5)
+    im3.set_pixel(0,1,1.5)
+    im3.set_pixel(1,0,2.5)
+    im3.set_pixel(1,1,3.5)
+    eq_(im.compare(im3),3)
+    eq_(im.compare(im3,1.0),2)
+    eq_(im.compare(im3,2.0),1)
+    eq_(im.compare(im3,3.0),0)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/compositing_test.py b/test/python_tests/compositing_test.py
new file mode 100644
index 0000000..a0c8255
--- /dev/null
+++ b/test/python_tests/compositing_test.py
@@ -0,0 +1,258 @@
+#encoding: utf8
+
+from nose.tools import eq_
+import os
+from utilities import execution_path, run_all
+from utilities import get_unique_colors, pixel2channels, side_by_side_image
+import mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def is_pre(color,alpha):
+    return (color*255.0/alpha) <= 255
+
+def debug_image(image,step=2):
+    for x in range(0,image.width(),step):
+        for y in range(0,image.height(),step):
+            pixel = image.get_pixel(x,y)
+            red,green,blue,alpha = pixel2channels(pixel)
+            print "rgba(%s,%s,%s,%s) at %s,%s" % (red,green,blue,alpha,x,y)
+
+def replace_style(m, name, style):
+    m.remove_style(name)
+    m.append_style(name, style)
+
+# note: it is impossible to know for all pixel colors
+# we can only detect likely cases of non premultiplied colors
+def validate_pixels_are_not_premultiplied(image):
+    over_alpha = False
+    transparent = True
+    fully_opaque = True
+    for x in range(0,image.width(),2):
+        for y in range(0,image.height(),2):
+            pixel = image.get_pixel(x,y)
+            red,green,blue,alpha = pixel2channels(pixel)
+            if alpha > 0:
+                transparent = False
+                if alpha < 255:
+                    fully_opaque = False
+                color_max = max(red,green,blue)
+                if color_max > alpha:
+                    over_alpha = True
+    return over_alpha or transparent or fully_opaque
+
+def validate_pixels_are_not_premultiplied2(image):
+    looks_not_multiplied = False
+    for x in range(0,image.width(),2):
+        for y in range(0,image.height(),2):
+            pixel = image.get_pixel(x,y)
+            red,green,blue,alpha = pixel2channels(pixel)
+            #each value of the color channels will never be bigger than that of the alpha channel.
+            if alpha > 0:
+                if red > 0 and red > alpha:
+                    print 'red: %s, a: %s' % (red,alpha)
+                    looks_not_multiplied = True
+    return looks_not_multiplied
+
+def validate_pixels_are_premultiplied(image):
+    bad_pixels = []
+    for x in range(0,image.width(),2):
+        for y in range(0,image.height(),2):
+            pixel = image.get_pixel(x,y)
+            red,green,blue,alpha = pixel2channels(pixel)
+            if alpha > 0:
+                pixel = image.get_pixel(x,y)
+                is_valid = ((0 <= red <= alpha) and is_pre(red,alpha)) \
+                        and ((0 <= green <= alpha) and is_pre(green,alpha)) \
+                        and ((0 <= blue <= alpha) and is_pre(blue,alpha)) \
+                        and (alpha >= 0 and alpha <= 255)
+                if not is_valid:
+                    bad_pixels.append("rgba(%s,%s,%s,%s) at %s,%s" % (red,green,blue,alpha,x,y))
+    num_bad = len(bad_pixels)
+    return (num_bad == 0,bad_pixels)
+
+def test_compare_images():
+    b = mapnik.Image.open('./images/support/b.png')
+    b.premultiply()
+    num_ops = len(mapnik.CompositeOp.names)
+    successes = []
+    fails = []
+    for name in mapnik.CompositeOp.names:
+        a = mapnik.Image.open('./images/support/a.png')
+        a.premultiply()
+        a.composite(b,getattr(mapnik.CompositeOp,name))
+        actual = '/tmp/mapnik-comp-op-test-' + name + '.png'
+        expected = 'images/composited/' + name + '.png'
+        valid = validate_pixels_are_premultiplied(a)
+        if not valid[0]:
+            fails.append('%s not validly premultiplied!:\n\t %s pixels (%s)' % (name,len(valid[1]),valid[1][0]))
+        a.demultiply()
+        if not validate_pixels_are_not_premultiplied(a):
+            fails.append('%s not validly demultiplied' % (name))
+        a.save(actual,'png32')
+        if not os.path.exists(expected) or os.environ.get('UPDATE'):
+            print 'generating expected test image: %s' % expected
+            a.save(expected,'png32')
+        expected_im = mapnik.Image.open(expected)
+        # compare them
+        if a.tostring('png32') == expected_im.tostring('png32'):
+            successes.append(name)
+        else:
+            fails.append('failed comparing actual (%s) and expected(%s)' % (actual,'tests/python_tests/'+ expected))
+            fail_im = side_by_side_image(expected_im, a)
+            fail_im.save('/tmp/mapnik-comp-op-test-' + name + '.fail.png','png32')
+    eq_(len(successes),num_ops,'\n'+'\n'.join(fails))
+    b.demultiply()
+    # b will be slightly modified by pre and then de multiplication rounding errors
+    # TODO - write test to ensure the image is 99% the same.
+    #expected_b = mapnik.Image.open('./images/support/b.png')
+    #b.save('/tmp/mapnik-comp-op-test-original-mask.png')
+    #eq_(b.tostring('png32'),expected_b.tostring('png32'), '/tmp/mapnik-comp-op-test-original-mask.png is no longer equivalent to original mask: ./images/support/b.png')
+
+def test_pre_multiply_status():
+    b = mapnik.Image.open('./images/support/b.png')
+    # not premultiplied yet, should appear that way
+    result = validate_pixels_are_not_premultiplied(b)
+    eq_(result,True)
+    # not yet premultiplied therefore should return false
+    result = validate_pixels_are_premultiplied(b)
+    eq_(result[0],False)
+    # now actually premultiply the pixels
+    b.premultiply()
+    # now checking if premultiplied should succeed
+    result = validate_pixels_are_premultiplied(b)
+    eq_(result[0],True)
+    # should now not appear to look not premultiplied
+    result = validate_pixels_are_not_premultiplied(b)
+    eq_(result,False)
+    # now actually demultiply the pixels
+    b.demultiply()
+    # should now appear demultiplied
+    result = validate_pixels_are_not_premultiplied(b)
+    eq_(result,True)
+
+def test_pre_multiply_status_of_map1():
+    m = mapnik.Map(256,256)
+    im = mapnik.Image(m.width,m.height)
+    eq_(validate_pixels_are_not_premultiplied(im),True)
+    mapnik.render(m,im)
+    eq_(validate_pixels_are_not_premultiplied(im),True)
+
+def test_pre_multiply_status_of_map2():
+    m = mapnik.Map(256,256)
+    m.background = mapnik.Color(1,1,1,255)
+    im = mapnik.Image(m.width,m.height)
+    eq_(validate_pixels_are_not_premultiplied(im),True)
+    mapnik.render(m,im)
+    eq_(validate_pixels_are_not_premultiplied(im),True)
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+    def test_style_level_comp_op():
+        m = mapnik.Map(256, 256)
+        mapnik.load_map(m, '../data/good_maps/style_level_comp_op.xml')
+        m.zoom_all()
+        successes = []
+        fails = []
+        for name in mapnik.CompositeOp.names:
+            # find_style returns a copy of the style object
+            style_markers = m.find_style("markers")
+            style_markers.comp_op = getattr(mapnik.CompositeOp, name)
+            # replace the original style with the modified one
+            replace_style(m, "markers", style_markers)
+            im = mapnik.Image(m.width, m.height)
+            mapnik.render(m, im)
+            actual = '/tmp/mapnik-style-comp-op-' + name + '.png'
+            expected = 'images/style-comp-op/' + name + '.png'
+            im.save(actual,'png32')
+            if not os.path.exists(expected) or os.environ.get('UPDATE'):
+                print 'generating expected test image: %s' % expected
+                im.save(expected,'png32')
+            expected_im = mapnik.Image.open(expected)
+            # compare them
+            if im.tostring('png32') == expected_im.tostring('png32'):
+                successes.append(name)
+            else:
+                fails.append('failed comparing actual (%s) and expected(%s)' % (actual,'tests/python_tests/'+ expected))
+                fail_im = side_by_side_image(expected_im, im)
+                fail_im.save('/tmp/mapnik-style-comp-op-' + name + '.fail.png','png32')
+        eq_(len(fails), 0, '\n'+'\n'.join(fails))
+
+    def test_style_level_opacity():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/style_level_opacity_and_blur.xml')
+        m.zoom_all()
+        im = mapnik.Image(512,512)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-style-level-opacity.png'
+        expected = 'images/support/mapnik-style-level-opacity.png'
+        im.save(actual,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+def test_rounding_and_color_expectations():
+    m = mapnik.Map(1,1)
+    m.background = mapnik.Color('rgba(255,255,255,.4999999)')
+    im = mapnik.Image(m.width,m.height)
+    mapnik.render(m,im)
+    eq_(get_unique_colors(im),['rgba(255,255,255,127)'])
+    m = mapnik.Map(1,1)
+    m.background = mapnik.Color('rgba(255,255,255,.5)')
+    im = mapnik.Image(m.width,m.height)
+    mapnik.render(m,im)
+    eq_(get_unique_colors(im),['rgba(255,255,255,128)'])
+    im_file = mapnik.Image.open('../data/images/stripes_pattern.png')
+    eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(74,74,74,255)'])
+    # should have no effect
+    im_file.premultiply()
+    eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(74,74,74,255)'])
+    im_file.apply_opacity(.5)
+    # should have effect now that image has transparency
+    im_file.premultiply()
+    eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(37,37,37,127)'])
+    # should restore to original nonpremultiplied colors
+    im_file.demultiply()
+    eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(74,74,74,127)'])
+
+
+def test_background_image_and_background_color():
+    m = mapnik.Map(8,8)
+    m.background = mapnik.Color('rgba(255,255,255,.5)')
+    m.background_image = '../data/images/stripes_pattern.png'
+    im = mapnik.Image(m.width,m.height)
+    mapnik.render(m,im)
+    eq_(get_unique_colors(im),['rgba(255,255,255,128)', 'rgba(74,74,74,255)'])
+
+def test_background_image_with_alpha_and_background_color():
+    m = mapnik.Map(10,10)
+    m.background = mapnik.Color('rgba(255,255,255,.5)')
+    m.background_image = '../data/images/yellow_half_trans.png'
+    im = mapnik.Image(m.width,m.height)
+    mapnik.render(m,im)
+    eq_(get_unique_colors(im),['rgba(255,255,85,191)'])
+
+def test_background_image_with_alpha_and_background_color_against_composited_control():
+    m = mapnik.Map(10,10)
+    m.background = mapnik.Color('rgba(255,255,255,.5)')
+    m.background_image = '../data/images/yellow_half_trans.png'
+    im = mapnik.Image(m.width,m.height)
+    mapnik.render(m,im)
+    # create and composite the expected result
+    im1 = mapnik.Image(10,10)
+    im1.fill(mapnik.Color('rgba(255,255,255,.5)'))
+    im1.premultiply()
+    im2 = mapnik.Image(10,10)
+    im2.fill(mapnik.Color('rgba(255,255,0,.5)'))
+    im2.premultiply()
+    im1.composite(im2)
+    im1.demultiply()
+    # compare image rendered (compositing in `agg_renderer<T>::setup`)
+    # vs image composited via python bindings
+    #raise Todo("looks like we need to investigate PNG color rounding when saving")
+    #eq_(get_unique_colors(im),get_unique_colors(im1))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/copy_test.py b/test/python_tests/copy_test.py
new file mode 100644
index 0000000..d3cf9b1
--- /dev/null
+++ b/test/python_tests/copy_test.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import *
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_image_16_8_simple():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    im.set_pixel(0,0, 256)
+    im.set_pixel(0,1, 999)
+    im.set_pixel(1,0, 5)
+    im.set_pixel(1,1, 2)
+    im2 = im.copy(mapnik.ImageType.gray8)
+    eq_(im2.get_pixel(0,0), 255)
+    eq_(im2.get_pixel(0,1), 255)
+    eq_(im2.get_pixel(1,0), 5)
+    eq_(im2.get_pixel(1,1), 2)
+    # Cast back!
+    im = im2.copy(mapnik.ImageType.gray16)
+    eq_(im.get_pixel(0,0), 255)
+    eq_(im.get_pixel(0,1), 255)
+    eq_(im.get_pixel(1,0), 5)
+    eq_(im.get_pixel(1,1), 2)
+
+def test_image_32f_8_simple():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+    im.set_pixel(0,0, 120.1234)
+    im.set_pixel(0,1, -23.4)
+    im.set_pixel(1,0, 120.6)
+    im.set_pixel(1,1, 360.2)
+    im2 = im.copy(mapnik.ImageType.gray8)
+    eq_(im2.get_pixel(0,0), 120)
+    eq_(im2.get_pixel(0,1), 0)
+    eq_(im2.get_pixel(1,0), 120) # Notice this is truncated!
+    eq_(im2.get_pixel(1,1), 255)
+
+def test_image_offset_and_scale():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    eq_(im.offset, 0.0)
+    eq_(im.scaling, 1.0)
+    im.offset = 1.0
+    im.scaling = 2.0
+    eq_(im.offset, 1.0)
+    eq_(im.scaling, 2.0)
+
+def test_image_16_8_scale_and_offset():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    im.set_pixel(0,0, 256)
+    im.set_pixel(0,1, 258)
+    im.set_pixel(1,0, 99999)
+    im.set_pixel(1,1, 615)
+    offset = 255
+    scaling = 3
+    im2 = im.copy(mapnik.ImageType.gray8, offset, scaling)
+    eq_(im2.get_pixel(0,0), 0)
+    eq_(im2.get_pixel(0,1), 1)
+    eq_(im2.get_pixel(1,0), 255)
+    eq_(im2.get_pixel(1,1), 120)
+    # pixels will be a little off due to offsets in reverting!
+    im3 = im2.copy(mapnik.ImageType.gray16)
+    eq_(im3.get_pixel(0,0), 255) # Rounding error with ints
+    eq_(im3.get_pixel(0,1), 258) # same
+    eq_(im3.get_pixel(1,0), 1020) # The other one was way out of range for our scale/offset
+    eq_(im3.get_pixel(1,1), 615) # same 
+
+def test_image_16_32f_scale_and_offset():
+    im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+    im.set_pixel(0,0, 256)
+    im.set_pixel(0,1, 258)
+    im.set_pixel(1,0, 0)
+    im.set_pixel(1,1, 615)
+    offset = 255
+    scaling = 3.2
+    im2 = im.copy(mapnik.ImageType.gray32f, offset, scaling)
+    eq_(im2.get_pixel(0,0), 0.3125)
+    eq_(im2.get_pixel(0,1), 0.9375)
+    eq_(im2.get_pixel(1,0), -79.6875)
+    eq_(im2.get_pixel(1,1), 112.5)
+    im3 = im2.copy(mapnik.ImageType.gray16)
+    eq_(im3.get_pixel(0,0), 256) 
+    eq_(im3.get_pixel(0,1), 258)
+    eq_(im3.get_pixel(1,0), 0) 
+    eq_(im3.get_pixel(1,1), 615) 
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/csv_test.py b/test/python_tests/csv_test.py
new file mode 100644
index 0000000..5011f57
--- /dev/null
+++ b/test/python_tests/csv_test.py
@@ -0,0 +1,604 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import glob
+from nose.tools import eq_,raises
+from utilities import execution_path
+
+import os, mapnik
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+    # make the tests silent since we intentially test error conditions that are noisy
+    mapnik.logger.set_severity(mapnik.severity_type.None)
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def teardown():
+    mapnik.logger.set_severity(default_logging_severity)
+
+if 'csv' in mapnik.DatasourceCache.plugin_names():
+
+    def get_csv_ds(filename):
+        return mapnik.Datasource(type='csv',file=os.path.join('../data/csv/',filename))
+
+    def test_broken_files(visual=False):
+        broken = glob.glob("../data/csv/fails/*.*")
+        broken.extend(glob.glob("../data/csv/warns/*.*"))
+
+        # Add a filename that doesn't exist
+        broken.append("../data/csv/fails/does_not_exist.csv")
+
+        for csv in broken:
+            if visual:
+                try:
+                    mapnik.Datasource(type='csv',file=csv,strict=True)
+                    print '\x1b[33mfailed: should have thrown\x1b[0m',csv
+                except Exception:
+                    print '\x1b[1;32m✓ \x1b[0m', csv
+
+    def test_good_files(visual=False):
+        good_files = glob.glob("../data/csv/*.*")
+        good_files.extend(glob.glob("../data/csv/warns/*.*"))
+        ignorable = os.path.join('..','data','csv','long_lat.vrt')
+        good_files.remove(ignorable)
+
+        for csv in good_files:
+            if visual:
+                try:
+                    mapnik.Datasource(type='csv',file=csv)
+                    print '\x1b[1;32m✓ \x1b[0m', csv
+                except Exception, e:
+                    print '\x1b[33mfailed: should not have thrown\x1b[0m',csv,str(e)
+
+    def test_lon_lat_detection(**kwargs):
+        ds = get_csv_ds('lon_lat.csv')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['lon','lat'])
+        eq_(ds.field_types(),['int','int'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        fs = ds.features(query)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        feat = fs.next()
+        attr = {'lon': 0, 'lat': 0}
+        eq_(feat.attributes,attr)
+
+    def test_lng_lat_detection(**kwargs):
+        ds = get_csv_ds('lng_lat.csv')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['lng','lat'])
+        eq_(ds.field_types(),['int','int'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        fs = ds.features(query)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        feat = fs.next()
+        attr = {'lng': 0, 'lat': 0}
+        eq_(feat.attributes,attr)
+
+    def test_type_detection(**kwargs):
+        ds = get_csv_ds('nypd.csv')
+        eq_(ds.fields(),['Precinct','Phone','Address','City','geo_longitude','geo_latitude','geo_accuracy'])
+        eq_(ds.field_types(),['str','str','str','str','float','float','str'])
+        feat = ds.featureset().next()
+        attr = {'City': u'New York, NY', 'geo_accuracy': u'house', 'Phone': u'(212) 334-0711', 'Address': u'19 Elizabeth Street', 'Precinct': u'5th Precinct', 'geo_longitude': -70, 'geo_latitude': 40}
+        eq_(feat.attributes,attr)
+        eq_(len(ds.all_features()),2)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_skipping_blank_rows(**kwargs):
+        ds = get_csv_ds('blank_rows.csv')
+        eq_(ds.fields(),['x','y','name'])
+        eq_(ds.field_types(),['int','int','str'])
+        eq_(len(ds.all_features()),2)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_empty_rows(**kwargs):
+        ds = get_csv_ds('empty_rows.csv')
+        eq_(len(ds.fields()),10)
+        eq_(len(ds.field_types()),10)
+        eq_(ds.fields(),['x', 'y', 'text', 'date', 'integer', 'boolean', 'float', 'time', 'datetime', 'empty_column'])
+        eq_(ds.field_types(),['int', 'int', 'str', 'str', 'int', 'bool', 'float', 'str', 'str', 'str'])
+        fs = ds.featureset()
+        attr = {'x': 0, 'empty_column': u'', 'text': u'a b', 'float': 1.0, 'datetime': u'1971-01-01T04:14:00', 'y': 0, 'boolean': True, 'time': u'04:14:00', 'date': u'1971-01-01', 'integer': 40}
+        first = True
+        for feat in fs:
+            if first:
+                first=False
+                eq_(feat.attributes,attr)
+            eq_(len(feat),10)
+            eq_(feat['empty_column'],u'')
+
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_slashes(**kwargs):
+        ds = get_csv_ds('has_attributes_with_slashes.csv')
+        eq_(len(ds.fields()),3)
+        fs = ds.all_features()
+        eq_(fs[0].attributes,{'x':0,'y':0,'name':u'a/a'})
+        eq_(fs[1].attributes,{'x':1,'y':4,'name':u'b/b'})
+        eq_(fs[2].attributes,{'x':10,'y':2.5,'name':u'c/c'})
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_wkt_field(**kwargs):
+        ds = get_csv_ds('wkt.csv')
+        eq_(len(ds.fields()),1)
+        eq_(ds.fields(),['type'])
+        eq_(ds.field_types(),['str'])
+        fs = ds.all_features()
+        #eq_(len(fs[0].geometries()),1)
+        eq_(fs[0].geometry.type(),mapnik.GeometryType.Point)
+        #eq_(len(fs[1].geometries()),1)
+        eq_(fs[1].geometry.type(),mapnik.GeometryType.LineString)
+        #eq_(len(fs[2].geometries()),1)
+        eq_(fs[2].geometry.type(),mapnik.GeometryType.Polygon)
+        #eq_(len(fs[3].geometries()),1) # one geometry, two parts
+        eq_(fs[3].geometry.type(),mapnik.GeometryType.Polygon)
+        #eq_(len(fs[4].geometries()),4)
+        eq_(fs[4].geometry.type(),mapnik.GeometryType.MultiPoint)
+        #eq_(len(fs[5].geometries()),2)
+        eq_(fs[5].geometry.type(),mapnik.GeometryType.MultiLineString)
+        #eq_(len(fs[6].geometries()),2)
+        eq_(fs[6].geometry.type(),mapnik.GeometryType.MultiPolygon)
+        #eq_(len(fs[7].geometries()),2)
+        eq_(fs[7].geometry.type(),mapnik.GeometryType.MultiPolygon)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Collection)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_handling_of_missing_header(**kwargs):
+        ds = get_csv_ds('missing_header.csv')
+        eq_(len(ds.fields()),6)
+        eq_(ds.fields(),['one','two','x','y','_4','aftermissing'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['_4'],'missing')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_handling_of_headers_that_are_numbers(**kwargs):
+        ds = get_csv_ds('numbers_for_headers.csv')
+        eq_(len(ds.fields()),5)
+        eq_(ds.fields(),['x','y','1990','1991','1992'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['1990'],1)
+        eq_(feat['1991'],2)
+        eq_(feat['1992'],3)
+        eq_(mapnik.Expression("[1991]=2").evaluate(feat),True)
+
+    def test_quoted_numbers(**kwargs):
+        ds = get_csv_ds('points.csv')
+        eq_(len(ds.fields()),6)
+        eq_(ds.fields(),['lat','long','name','nr','color','placements'])
+        fs = ds.all_features()
+        eq_(fs[0]['placements'],"N,S,E,W,SW,10,5")
+        eq_(fs[1]['placements'],"N,S,E,W,SW,10,5")
+        eq_(fs[2]['placements'],"N,S,E,W,SW,10,5")
+        eq_(fs[3]['placements'],"N,S,E,W,SW,10,5")
+        eq_(fs[4]['placements'],"N,S,E,W,SW,10,5")
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_reading_windows_newlines(**kwargs):
+        ds = get_csv_ds('windows_newlines.csv')
+        eq_(len(ds.fields()),3)
+        feats = ds.all_features()
+        eq_(len(feats),1)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],1)
+        eq_(feat['y'],10)
+        eq_(feat['z'],9999.9999)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_reading_mac_newlines(**kwargs):
+        ds = get_csv_ds('mac_newlines.csv')
+        eq_(len(ds.fields()),3)
+        feats = ds.all_features()
+        eq_(len(feats),1)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],1)
+        eq_(feat['y'],10)
+        eq_(feat['z'],9999.9999)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def check_newlines(filename):
+        ds = get_csv_ds(filename)
+        eq_(len(ds.fields()),3)
+        feats = ds.all_features()
+        eq_(len(feats),1)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['line'],'many\n  lines\n  of text\n  with unix newlines')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_mixed_mac_unix_newlines(**kwargs):
+        check_newlines('mac_newlines_with_unix_inline.csv')
+
+    def test_mixed_mac_unix_newlines_escaped(**kwargs):
+        check_newlines('mac_newlines_with_unix_inline_escaped.csv')
+
+    # To hard to support this case
+    #def test_mixed_unix_windows_newlines(**kwargs):
+    #    check_newlines('unix_newlines_with_windows_inline.csv')
+
+    # To hard to support this case
+    #def test_mixed_unix_windows_newlines_escaped(**kwargs):
+    #    check_newlines('unix_newlines_with_windows_inline_escaped.csv')
+
+    def test_mixed_windows_unix_newlines(**kwargs):
+        check_newlines('windows_newlines_with_unix_inline.csv')
+
+    def test_mixed_windows_unix_newlines_escaped(**kwargs):
+        check_newlines('windows_newlines_with_unix_inline_escaped.csv')
+
+    def test_tabs(**kwargs):
+        ds = get_csv_ds('tabs_in_csv.csv')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','z'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],-122)
+        eq_(feat['y'],48)
+        eq_(feat['z'],0)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_separator_pipes(**kwargs):
+        ds = get_csv_ds('pipe_delimiters.csv')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','z'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['z'],'hello')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_separator_semicolon(**kwargs):
+        ds = get_csv_ds('semicolon_delimiters.csv')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','z'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['z'],'hello')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_that_null_and_bool_keywords_are_empty_strings(**kwargs):
+        ds = get_csv_ds('nulls_and_booleans_as_strings.csv')
+        eq_(len(ds.fields()),4)
+        eq_(ds.fields(),['x','y','null','boolean'])
+        eq_(ds.field_types(),['int', 'int', 'str', 'bool'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['null'],'null')
+        eq_(feat['boolean'],True)
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['null'],'')
+        eq_(feat['boolean'],False)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = get_csv_ds('lon_lat.csv')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['lon','lat'])
+        eq_(ds.field_types(),['int','int'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        # also add an invalid one, triggering throw
+        query.add_property_name('bogus')
+        ds.features(query)
+
+    def test_that_leading_zeros_mean_strings(**kwargs):
+        ds = get_csv_ds('leading_zeros.csv')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','fips'])
+        eq_(ds.field_types(),['int','int','str'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['fips'],'001')
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['fips'],'003')
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['fips'],'005')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_advanced_geometry_detection(**kwargs):
+        ds = get_csv_ds('point_wkt.csv')
+        eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+        ds = get_csv_ds('poly_wkt.csv')
+        eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Polygon)
+        ds = get_csv_ds('multi_poly_wkt.csv')
+        eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Polygon)
+        ds = get_csv_ds('line_wkt.csv')
+        eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.LineString)
+
+    def test_creation_of_csv_from_in_memory_string(**kwargs):
+        csv_string = '''
+           wkt,Name
+          "POINT (120.15 48.47)","Winthrop, WA"
+          ''' # csv plugin will test lines <= 10 chars for being fully blank
+        ds = mapnik.Datasource(**{"type":"csv","inline":csv_string})
+        eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['Name'],u"Winthrop, WA")
+
+    def test_creation_of_csv_from_in_memory_string_with_uft8(**kwargs):
+        csv_string = '''
+           wkt,Name
+          "POINT (120.15 48.47)","Québec"
+          ''' # csv plugin will test lines <= 10 chars for being fully blank
+        ds = mapnik.Datasource(**{"type":"csv","inline":csv_string})
+        eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['Name'],u"Québec")
+
+    def validate_geojson_datasource(ds):
+        eq_(len(ds.fields()),1)
+        eq_(ds.fields(),['type'])
+        eq_(ds.field_types(),['str'])
+        fs = ds.all_features()
+        #eq_(len(fs[0].geometries()),1)
+        eq_(fs[0].geometry.type(),mapnik.GeometryType.Point)
+        #eq_(len(fs[1].geometries()),1)
+        eq_(fs[1].geometry.type(),mapnik.GeometryType.LineString)
+        #eq_(len(fs[2].geometries()),1)
+        eq_(fs[2].geometry.type(), mapnik.GeometryType.Polygon)
+        #eq_(len(fs[3].geometries()),1) # one geometry, two parts
+        eq_(fs[3].geometry.type(),mapnik.GeometryType.Polygon)
+        #eq_(len(fs[4].geometries()),4)
+        eq_(fs[4].geometry.type(),mapnik.GeometryType.MultiPoint)
+        #eq_(len(fs[5].geometries()),2)
+        eq_(fs[5].geometry.type(),mapnik.GeometryType.MultiLineString)
+        #eq_(len(fs[6].geometries()),2)
+        eq_(fs[6].geometry.type(),mapnik.GeometryType.MultiPolygon)
+        #eq_(len(fs[7].geometries()),2)
+        eq_(fs[7].geometry.type(),mapnik.GeometryType.MultiPolygon)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Collection)
+        eq_(desc['name'],'csv')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+    def test_json_field1(**kwargs):
+        ds = get_csv_ds('geojson_double_quote_escape.csv')
+        validate_geojson_datasource(ds)
+
+    def test_json_field2(**kwargs):
+        ds = get_csv_ds('geojson_single_quote.csv')
+        validate_geojson_datasource(ds)
+
+    def test_json_field3(**kwargs):
+        ds = get_csv_ds('geojson_2x_double_quote_filebakery_style.csv')
+        validate_geojson_datasource(ds)
+
+    def test_that_blank_undelimited_rows_are_still_parsed(**kwargs):
+        ds = get_csv_ds('more_headers_than_column_values.csv')
+        eq_(len(ds.fields()),5)
+        eq_(ds.fields(),['x','y','one', 'two','three'])
+        eq_(ds.field_types(),['int','int','str','str','str'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['one'],'')
+        eq_(feat['two'],'')
+        eq_(feat['three'],'')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+    @raises(RuntimeError)
+    def test_that_fewer_headers_than_rows_throws(**kwargs):
+        # this has invalid header # so throw
+        get_csv_ds('more_column_values_than_headers.csv')
+
+    def test_that_feature_id_only_incremented_for_valid_rows(**kwargs):
+        ds = mapnik.Datasource(type='csv',
+                               file=os.path.join('../data/csv/warns','feature_id_counting.csv'))
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','id'])
+        eq_(ds.field_types(),['int','int','int'])
+        fs = ds.featureset()
+        # first
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['id'],1)
+        # second, should have skipped bogus one
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['id'],2)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(len(ds.all_features()),2)
+
+    def test_dynamically_defining_headers1(**kwargs):
+        ds = mapnik.Datasource(type='csv',
+                               file=os.path.join('../data/csv/fails','needs_headers_two_lines.csv'),
+                               headers='x,y,name')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','name'])
+        eq_(ds.field_types(),['int','int','str'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['name'],'data_name')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(len(ds.all_features()),2)
+
+    def test_dynamically_defining_headers2(**kwargs):
+        ds = mapnik.Datasource(type='csv',
+                               file=os.path.join('../data/csv/fails','needs_headers_one_line.csv'),
+                               headers='x,y,name')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','name'])
+        eq_(ds.field_types(),['int','int','str'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['name'],'data_name')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(len(ds.all_features()),1)
+
+    def test_dynamically_defining_headers3(**kwargs):
+        ds = mapnik.Datasource(type='csv',
+                               file=os.path.join('../data/csv/fails','needs_headers_one_line_no_newline.csv'),
+                               headers='x,y,name')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','name'])
+        eq_(ds.field_types(),['int','int','str'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['x'],0)
+        eq_(feat['y'],0)
+        eq_(feat['name'],'data_name')
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(len(ds.all_features()),1)
+
+    def test_that_64bit_int_fields_work(**kwargs):
+        ds = get_csv_ds('64bit_int.csv')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','bigint'])
+        eq_(ds.field_types(),['int','int','int'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['bigint'],2147483648)
+        feat = fs.next()
+        eq_(feat['bigint'],9223372036854775807)
+        eq_(feat['bigint'],0x7FFFFFFFFFFFFFFF)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(len(ds.all_features()),2)
+
+    def test_various_number_types(**kwargs):
+        ds = get_csv_ds('number_types.csv')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['x','y','floats'])
+        eq_(ds.field_types(),['int','int','float'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['floats'],.0)
+        feat = fs.next()
+        eq_(feat['floats'],+.0)
+        feat = fs.next()
+        eq_(feat['floats'],1e-06)
+        feat = fs.next()
+        eq_(feat['floats'],-1e-06)
+        feat = fs.next()
+        eq_(feat['floats'],0.000001)
+        feat = fs.next()
+        eq_(feat['floats'],1.234e+16)
+        feat = fs.next()
+        eq_(feat['floats'],1.234e+16)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(len(ds.all_features()),8)
+
+    def test_manually_supplied_extent(**kwargs):
+        csv_string = '''
+           wkt,Name
+          '''
+        ds = mapnik.Datasource(**{"type":"csv","extent":"-180,-90,180,90","inline":csv_string})
+        b = ds.envelope()
+        eq_(b.minx,-180)
+        eq_(b.miny,-90)
+        eq_(b.maxx,180)
+        eq_(b.maxy,90)
+
+    def test_inline_geojson(**kwargs):
+        csv_string = "geojson\n'{\"coordinates\":[-92.22568,38.59553],\"type\":\"Point\"}'"
+        ds = mapnik.Datasource(**{"type":"csv","inline":csv_string})
+        eq_(len(ds.fields()),0)
+        eq_(ds.fields(),[])
+        # FIXME - re-enable after https://github.com/mapnik/mapnik/issues/2319 is fixed
+        #fs = ds.featureset()
+        #feat = fs.next()
+        #eq_(feat.num_geometries(),1)
+
+if __name__ == "__main__":
+    setup()
+    [eval(run)(visual=True) for run in dir() if 'test_' in run]
diff --git a/test/python_tests/datasource_test.py b/test/python_tests/datasource_test.py
new file mode 100644
index 0000000..4ada3dc
--- /dev/null
+++ b/test/python_tests/datasource_test.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_, raises
+from utilities import execution_path, run_all
+import os, mapnik
+from itertools import groupby
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_that_datasources_exist():
+    if len(mapnik.DatasourceCache.plugin_names()) == 0:
+        print '***NOTICE*** - no datasource plugins have been loaded'
+
+# adapted from raster_symboliser_test#test_dataraster_query_point
+ at raises(RuntimeError)
+def test_vrt_referring_to_missing_files():
+    srs = '+init=epsg:32630'
+    if 'gdal' in mapnik.DatasourceCache.plugin_names():
+        lyr = mapnik.Layer('dataraster')
+        lyr.datasource = mapnik.Gdal(
+            file = '../data/raster/missing_raster.vrt',
+            band = 1,
+            )
+        lyr.srs = srs
+        _map = mapnik.Map(256, 256, srs)
+        _map.layers.append(lyr)
+
+        # center of extent of raster
+        x, y = 556113.0,4381428.0 # center of extent of raster
+
+        _map.zoom_all()
+
+        # Fancy stuff to supress output of error
+        # open 2 fds
+        null_fds = [os.open(os.devnull, os.O_RDWR) for x in xrange(2)]
+        # save the current file descriptors to a tuple
+        save = os.dup(1), os.dup(2)
+        # put /dev/null fds on 1 and 2
+        os.dup2(null_fds[0], 1)
+        os.dup2(null_fds[1], 2)
+
+        # *** run the function ***
+        try:
+            # Should RuntimeError here
+            _map.query_point(0, x, y).features
+        finally:
+            # restore file descriptors so I can print the results
+            os.dup2(save[0], 1)
+            os.dup2(save[1], 2)
+            # close the temporary fds
+            os.close(null_fds[0])
+            os.close(null_fds[1])
+
+
+def test_field_listing():
+    if 'shape' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+        fields = ds.fields()
+        eq_(fields, ['AREA', 'EAS_ID', 'PRFEDEA'])
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Polygon)
+        eq_(desc['name'],'shape')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+
+def test_total_feature_count_shp():
+    if 'shape' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+        features = ds.all_features()
+        num_feats = len(features)
+        eq_(num_feats, 10)
+
+def test_total_feature_count_json():
+    if 'ogr' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Ogr(file='../data/json/points.geojson',layer_by_index=0)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(desc['name'],'ogr')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+        features = ds.all_features()
+        num_feats = len(features)
+        eq_(num_feats, 5)
+
+def test_sqlite_reading():
+    if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',table_by_index=0)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Polygon)
+        eq_(desc['name'],'sqlite')
+        eq_(desc['type'],mapnik.DataType.Vector)
+        eq_(desc['encoding'],'utf-8')
+        features = ds.all_features()
+        num_feats = len(features)
+        eq_(num_feats, 245)
+
+def test_reading_json_from_string():
+    json = open('../data/json/points.geojson','r').read()
+    if 'ogr' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Ogr(file=json,layer_by_index=0)
+        features = ds.all_features()
+        num_feats = len(features)
+        eq_(num_feats, 5)
+
+def test_feature_envelope():
+    if 'shape' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+        features = ds.all_features()
+        for feat in features:
+            env = feat.envelope()
+            contains = ds.envelope().contains(env)
+            eq_(contains, True)
+            intersects = ds.envelope().contains(env)
+            eq_(intersects, True)
+
+def test_feature_attributes():
+    if 'shape' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+        features = ds.all_features()
+        feat = features[0]
+        attrs = {'PRFEDEA': u'35043411', 'EAS_ID': 168, 'AREA': 215229.266}
+        eq_(feat.attributes, attrs)
+        eq_(ds.fields(),['AREA', 'EAS_ID', 'PRFEDEA'])
+        eq_(ds.field_types(),['float','int','str'])
+
+def test_ogr_layer_by_sql():
+    if 'ogr' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Ogr(file='../data/shp/poly.shp', layer_by_sql='SELECT * FROM poly WHERE EAS_ID = 168')
+        features = ds.all_features()
+        num_feats = len(features)
+        eq_(num_feats, 1)
+
+def test_hit_grid():
+
+    def rle_encode(l):
+        """ encode a list of strings with run-length compression """
+        return ["%d:%s" % (len(list(group)), name) for name, group in groupby(l)]
+
+    m = mapnik.Map(256,256);
+    try:
+        mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml');
+        m.zoom_all()
+        join_field = 'NAME'
+        fg = [] # feature grid
+        for y in range(0, 256, 4):
+            for x in range(0, 256, 4):
+                featureset = m.query_map_point(0,x,y)
+                added = False
+                for feature in featureset.features:
+                    fg.append(feature[join_field])
+                    added = True
+                if not added:
+                    fg.append('')
+        hit_list = '|'.join(rle_encode(fg))
+        eq_(hit_list[:16],'730:|2:Greenland')
+        eq_(hit_list[-12:],'1:Chile|812:')
+    except RuntimeError, e:
+        # only test datasources that we have installed
+        if not 'Could not create datasource' in str(e):
+            raise RuntimeError(str(e))
+
+
+if __name__ == '__main__':
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/datasource_xml_template_test.py b/test/python_tests/datasource_xml_template_test.py
new file mode 100644
index 0000000..38a73a3
--- /dev/null
+++ b/test/python_tests/datasource_xml_template_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from utilities import execution_path, run_all
+import mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_datasource_template_is_working():
+    m = mapnik.Map(256,256)
+    try:
+        mapnik.load_map(m,'../data/good_maps/datasource.xml')
+    except RuntimeError, e:
+        if "Required parameter 'type'" in str(e):
+            raise RuntimeError(e)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/extra_map_props_test.py b/test/python_tests/extra_map_props_test.py
new file mode 100644
index 0000000..045cddb
--- /dev/null
+++ b/test/python_tests/extra_map_props_test.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_arbitrary_parameters_attached_to_map():
+    m = mapnik.Map(256,256)
+    mapnik.load_map(m,'../data/good_maps/extra_arbitary_map_parameters.xml')
+    eq_(len(m.parameters),5)
+    eq_(m.parameters['key'],'value2')
+    eq_(m.parameters['key3'],'value3')
+    eq_(m.parameters['unicode'],u'iván')
+    eq_(m.parameters['integer'],10)
+    eq_(m.parameters['decimal'],.999)
+    m2 = mapnik.Map(256,256)
+    for k,v in m.parameters:
+        m2.parameters.append(mapnik.Parameter(k,v))
+    eq_(len(m2.parameters),5)
+    eq_(m2.parameters['key'],'value2')
+    eq_(m2.parameters['key3'],'value3')
+    eq_(m2.parameters['unicode'],u'iván')
+    eq_(m2.parameters['integer'],10)
+    eq_(m2.parameters['decimal'],.999)
+    map_string = mapnik.save_map_to_string(m)
+    m3 = mapnik.Map(256,256)
+    mapnik.load_map_from_string(m3,map_string)
+    eq_(len(m3.parameters),5)
+    eq_(m3.parameters['key'],'value2')
+    eq_(m3.parameters['key3'],'value3')
+    eq_(m3.parameters['unicode'],u'iván')
+    eq_(m3.parameters['integer'],10)
+    eq_(m3.parameters['decimal'],.999)
+
+
+def test_serializing_arbitrary_parameters():
+    m = mapnik.Map(256,256)
+    m.parameters.append(mapnik.Parameter('width',m.width))
+    m.parameters.append(mapnik.Parameter('height',m.height))
+
+    m2 = mapnik.Map(1,1)
+    mapnik.load_map_from_string(m2,mapnik.save_map_to_string(m))
+    eq_(m2.parameters['width'],m.width)
+    eq_(m2.parameters['height'],m.height)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/feature_id_test.py b/test/python_tests/feature_id_test.py
new file mode 100644
index 0000000..66c20cc
--- /dev/null
+++ b/test/python_tests/feature_id_test.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+import itertools
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def compare_shape_between_mapnik_and_ogr(shapefile,query=None):
+    plugins = mapnik.DatasourceCache.plugin_names()
+    if 'shape' in plugins and 'ogr' in plugins:
+        ds1 = mapnik.Ogr(file=shapefile,layer_by_index=0)
+        ds2 = mapnik.Shapefile(file=shapefile)
+        if query:
+            fs1 = ds1.features(query)
+            fs2 = ds2.features(query)
+        else:
+            fs1 = ds1.featureset()
+            fs2 = ds2.featureset()
+        count = 0;
+        for feat1,feat2 in itertools.izip(fs1,fs2):
+            count += 1
+            eq_(feat1.id(),feat2.id(),
+                '%s : ogr feature id %s "%s" does not equal shapefile feature id %s "%s"'
+                  % (count,feat1.id(),str(feat1.attributes), feat2.id(),str(feat2.attributes)))
+    return True
+
+
+def test_shapefile_line_featureset_id():
+    compare_shape_between_mapnik_and_ogr('../data/shp/polylines.shp')
+
+def test_shapefile_polygon_featureset_id():
+    compare_shape_between_mapnik_and_ogr('../data/shp/poly.shp')
+
+def test_shapefile_polygon_feature_query_id():
+    bbox = (15523428.2632, 4110477.6323, -11218494.8310, 7495720.7404)
+    query = mapnik.Query(mapnik.Box2d(*bbox))
+    if 'ogr' in mapnik.DatasourceCache.plugin_names():
+        ds = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0)
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        compare_shape_between_mapnik_and_ogr('../data/shp/world_merc.shp',query)
+
+def test_feature_hit_count():
+    pass
+    #raise Todo("need to optimize multigeom bbox handling in shapeindex: https://github.com/mapnik/mapnik/issues/783")
+    # results in different results between shp and ogr!
+    #bbox = (-14284551.8434, 2074195.1992, -7474929.8687, 8140237.7628)
+    #bbox = (1113194.91,4512803.085,2226389.82,6739192.905)
+    #query = mapnik.Query(mapnik.Box2d(*bbox))
+    #if 'ogr' in mapnik.DatasourceCache.plugin_names():
+    #    ds1 = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0)
+    #    for fld in ds1.fields():
+    #        query.add_property_name(fld)
+    #    ds2 = mapnik.Shapefile(file='../data/shp/world_merc.shp')
+    #    count1 = len(ds1.features(query).features)
+    #    count2 = len(ds2.features(query).features)
+    #    eq_(count1,count2,"Feature count differs between OGR driver (%s features) and Shapefile Driver (%s features) when querying the same bbox" % (count1,count2))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/feature_test.py b/test/python_tests/feature_test.py
new file mode 100644
index 0000000..5574cc7
--- /dev/null
+++ b/test/python_tests/feature_test.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+from utilities import run_all
+
+import mapnik
+from binascii import unhexlify
+
+def test_default_constructor():
+    f = mapnik.Feature(mapnik.Context(),1)
+    eq_(f is not None,True)
+
+def test_feature_geo_interface():
+    ctx = mapnik.Context()
+    feat = mapnik.Feature(ctx,1)
+    feat.geometry = mapnik.Geometry.from_wkt('Point (0 0)')
+    eq_(feat.__geo_interface__['geometry'],{u'type': u'Point', u'coordinates': [0, 0]})
+
+def test_python_extended_constructor():
+    context = mapnik.Context()
+    context.push('foo')
+    context.push('foo')
+    f = mapnik.Feature(context,1)
+    wkt = 'POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))'
+    f.geometry = mapnik.Geometry.from_wkt(wkt)
+    f['foo'] = 'bar'
+    eq_(f['foo'], 'bar')
+    eq_(f.envelope(),mapnik.Box2d(10.0,10.0,45.0,45.0))
+    # reset
+    f['foo'] = u"avión"
+    eq_(f['foo'], u"avión")
+    f['foo'] = 1.4
+    eq_(f['foo'], 1.4)
+    f['foo'] = True
+    eq_(f['foo'], True)
+
+def test_add_geom_wkb():
+# POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))
+    wkb = '010300000001000000050000000000000000003e4000000000000024400000000000002440000000000000344000000000000034400000000000004440000000000000444000000000000044400000000000003e400000000000002440'
+    geometry = mapnik.Geometry.from_wkb(unhexlify(wkb))
+    eq_(geometry.is_valid(), True)
+    eq_(geometry.is_simple(), True)
+    eq_(geometry.envelope(), mapnik.Box2d(10.0,10.0,40.0,40.0))
+    geometry.correct()
+    # valid after calling correct
+    eq_(geometry.is_valid(), True)
+
+def test_feature_expression_evaluation():
+    context = mapnik.Context()
+    context.push('name')
+    f = mapnik.Feature(context,1)
+    f['name'] = 'a'
+    eq_(f['name'],u'a')
+    expr = mapnik.Expression("[name]='a'")
+    evaluated = expr.evaluate(f)
+    eq_(evaluated,True)
+    num_attributes = len(f)
+    eq_(num_attributes,1)
+    eq_(f.id(),1)
+
+# https://github.com/mapnik/mapnik/issues/933
+def test_feature_expression_evaluation_missing_attr():
+    context = mapnik.Context()
+    context.push('name')
+    f = mapnik.Feature(context,1)
+    f['name'] = u'a'
+    eq_(f['name'],u'a')
+    expr = mapnik.Expression("[fielddoesnotexist]='a'")
+    eq_(f.has_key('fielddoesnotexist'),False)
+    try:
+        expr.evaluate(f)
+    except Exception, e:
+        eq_("Key does not exist" in str(e),True)
+    num_attributes = len(f)
+    eq_(num_attributes,1)
+    eq_(f.id(),1)
+
+# https://github.com/mapnik/mapnik/issues/934
+def test_feature_expression_evaluation_attr_with_spaces():
+    context = mapnik.Context()
+    context.push('name with space')
+    f = mapnik.Feature(context,1)
+    f['name with space'] = u'a'
+    eq_(f['name with space'],u'a')
+    expr = mapnik.Expression("[name with space]='a'")
+    eq_(str(expr),"([name with space]='a')")
+    eq_(expr.evaluate(f),True)
+
+# https://github.com/mapnik/mapnik/issues/2390
+ at raises(RuntimeError)
+def test_feature_from_geojson():
+    ctx = mapnik.Context()
+    inline_string = """
+    {
+         "geometry" : {
+            "coordinates" : [ 0,0 ]
+            "type" : "Point"
+         },
+         "type" : "Feature",
+         "properties" : {
+            "this":"that"
+            "known":"nope because missing comma"
+         }
+    }
+    """
+    mapnik.Feature.from_geojson(inline_string,ctx)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/filter_test.py b/test/python_tests/filter_test.py
new file mode 100644
index 0000000..34845ce
--- /dev/null
+++ b/test/python_tests/filter_test.py
@@ -0,0 +1,451 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+from utilities import run_all
+import mapnik
+
+if hasattr(mapnik,'Expression'):
+    mapnik.Filter = mapnik.Expression
+
+map_ = '''<Map>
+    <Style name="s">
+        <Rule>
+            <Filter><![CDATA[(([region]>=0) and ([region]<=50))]]></Filter>
+        </Rule>
+        <Rule>
+            <Filter><![CDATA[([region]>=0) and ([region]<=50)]]></Filter>
+        </Rule>
+        <Rule>
+            <Filter>
+
+            <![CDATA[
+
+            ([region] >= 0)
+
+            and
+
+            ([region] <= 50)
+            ]]>
+
+            </Filter>
+        </Rule>
+        <Rule>
+            <Filter>([region]>=0) and ([region]<=50)</Filter>
+        </Rule>
+        <Rule>
+            <Filter>
+            ([region] >= 0)
+             and
+            ([region] <= 50)
+            </Filter>
+        </Rule>
+
+    </Style>
+    <Style name="s2" filter-mode="first">
+        <Rule>
+        </Rule>
+        <Rule>
+        </Rule>
+    </Style>
+</Map>'''
+
+def test_filter_init():
+    m = mapnik.Map(1,1)
+    mapnik.load_map_from_string(m,map_)
+    filters = []
+    filters.append(mapnik.Filter("([region]>=0) and ([region]<=50)"))
+    filters.append(mapnik.Filter("(([region]>=0) and ([region]<=50))"))
+    filters.append(mapnik.Filter("((([region]>=0) and ([region]<=50)))"))
+    filters.append(mapnik.Filter('((([region]>=0) and ([region]<=50)))'))
+    filters.append(mapnik.Filter('''((([region]>=0) and ([region]<=50)))'''))
+    filters.append(mapnik.Filter('''
+    ((([region]>=0)
+    and
+    ([region]<=50)))
+    '''))
+    filters.append(mapnik.Filter('''
+    ([region]>=0)
+    and
+    ([region]<=50)
+    '''))
+    filters.append(mapnik.Filter('''
+    ([region]
+    >=
+    0)
+    and
+    ([region]
+    <=
+    50)
+    '''))
+
+    s = m.find_style('s')
+
+    for r in s.rules:
+        filters.append(r.filter)
+
+    first = filters[0]
+    for f in filters:
+        eq_(str(first),str(f))
+
+    s = m.find_style('s2')
+
+    eq_(s.filter_mode,mapnik.filter_mode.FIRST)
+
+
+def test_geometry_type_eval():
+    # clashing field called 'mapnik::geometry'
+    context2 = mapnik.Context()
+    context2.push('mapnik::geometry_type')
+    f = mapnik.Feature(context2,0)
+    f["mapnik::geometry_type"] = 'sneaky'
+    expr = mapnik.Expression("[mapnik::geometry_type]")
+    eq_(expr.evaluate(f),0)
+
+    expr = mapnik.Expression("[mapnik::geometry_type]")
+    context = mapnik.Context()
+
+    # no geometry
+    f = mapnik.Feature(context,0)
+    eq_(expr.evaluate(f),0)
+    eq_(mapnik.Expression("[mapnik::geometry_type]=0").evaluate(f),True)
+
+    # POINT = 1
+    f = mapnik.Feature(context,0)
+    f.geometry = mapnik.Geometry.from_wkt('POINT(10 40)')
+    eq_(expr.evaluate(f),1)
+    eq_(mapnik.Expression("[mapnik::geometry_type]=point").evaluate(f),True)
+
+    # LINESTRING = 2
+    f = mapnik.Feature(context,0)
+    f.geometry = mapnik.Geometry.from_wkt('LINESTRING (30 10, 10 30, 40 40)')
+    eq_(expr.evaluate(f),2)
+    eq_(mapnik.Expression("[mapnik::geometry_type] = linestring").evaluate(f),True)
+
+    # POLYGON = 3
+    f = mapnik.Feature(context,0)
+    f.geometry = mapnik.Geometry.from_wkt('POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))')
+    eq_(expr.evaluate(f),3)
+    eq_(mapnik.Expression("[mapnik::geometry_type] = polygon").evaluate(f),True)
+
+    # COLLECTION = 4
+    f = mapnik.Feature(context,0)
+    geom = mapnik.Geometry.from_wkt('GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))')
+    f.geometry = geom;
+    eq_(expr.evaluate(f),4)
+    eq_(mapnik.Expression("[mapnik::geometry_type] = collection").evaluate(f),True)
+
+def test_regex_match():
+    context = mapnik.Context()
+    context.push('name')
+    f = mapnik.Feature(context,0)
+    f["name"] = 'test'
+    expr = mapnik.Expression("[name].match('test')")
+    eq_(expr.evaluate(f),True) # 1 == True
+
+def test_unicode_regex_match():
+    context = mapnik.Context()
+    context.push('name')
+    f = mapnik.Feature(context,0)
+    f["name"] = 'Québec'
+    expr = mapnik.Expression("[name].match('Québec')")
+    eq_(expr.evaluate(f),True) # 1 == True
+
+def test_regex_replace():
+    context = mapnik.Context()
+    context.push('name')
+    f = mapnik.Feature(context,0)
+    f["name"] = 'test'
+    expr = mapnik.Expression("[name].replace('(\B)|( )','$1 ')")
+    eq_(expr.evaluate(f),'t e s t')
+
+def test_unicode_regex_replace_to_str():
+    expr = mapnik.Expression("[name].replace('(\B)|( )','$1 ')")
+    eq_(str(expr),"[name].replace('(\B)|( )','$1 ')")
+
+def test_unicode_regex_replace():
+    context = mapnik.Context()
+    context.push('name')
+    f = mapnik.Feature(context,0)
+    f["name"] = 'Québec'
+    expr = mapnik.Expression("[name].replace('(\B)|( )','$1 ')")
+    # will fail if -DBOOST_REGEX_HAS_ICU is not defined
+    eq_(expr.evaluate(f), u'Q u é b e c')
+
+def test_float_precision():
+    context = mapnik.Context()
+    context.push('num')
+    f = mapnik.Feature(context,0)
+    f["num1"] = 1.0000
+    f["num2"] = 1.0001
+    eq_(f["num1"],1.0000)
+    eq_(f["num2"],1.0001)
+    expr = mapnik.Expression("[num1] = 1.0000")
+    eq_(expr.evaluate(f),True)
+    expr = mapnik.Expression("[num1].match('1')")
+    eq_(expr.evaluate(f),True)
+    expr = mapnik.Expression("[num2] = 1.0001")
+    eq_(expr.evaluate(f),True)
+    expr = mapnik.Expression("[num2].match('1.0001')")
+    eq_(expr.evaluate(f),True)
+
+def test_string_matching_on_precision():
+    context = mapnik.Context()
+    context.push('num')
+    f = mapnik.Feature(context,0)
+    f["num"] = "1.0000"
+    eq_(f["num"],"1.0000")
+    expr = mapnik.Expression("[num].match('.*(^0|00)$')")
+    eq_(expr.evaluate(f),True)
+
+def test_creation_of_null_value():
+    context = mapnik.Context()
+    context.push('nv')
+    f = mapnik.Feature(context,0)
+    f["nv"] = None
+    eq_(f["nv"],None)
+    eq_(f["nv"] is None,True)
+    # test boolean
+    f["nv"] = 0
+    eq_(f["nv"],0)
+    eq_(f["nv"] is not None,True)
+
+def test_creation_of_bool():
+    context = mapnik.Context()
+    context.push('bool')
+    f = mapnik.Feature(context,0)
+    f["bool"] = True
+    eq_(f["bool"],True)
+    # TODO - will become int of 1 do to built in boost python conversion
+    # https://github.com/mapnik/mapnik/issues/1873
+    eq_(isinstance(f["bool"],bool) or isinstance(f["bool"],long),True)
+    f["bool"] = False
+    eq_(f["bool"],False)
+    eq_(isinstance(f["bool"],bool) or isinstance(f["bool"],long),True)
+    # test NoneType
+    f["bool"] = None
+    eq_(f["bool"],None)
+    eq_(isinstance(f["bool"],bool) or isinstance(f["bool"],long),False)
+    # test integer
+    f["bool"] = 0
+    eq_(f["bool"],0)
+    # https://github.com/mapnik/mapnik/issues/1873
+    # ugh, boost_python's built into converter does not work right
+    #eq_(isinstance(f["bool"],bool),False)
+
+null_equality = [
+  ['hello',False,unicode],
+  [u'',False,unicode],
+  [0,False,long],
+  [123,False,long],
+  [0.0,False,float],
+  [123.123,False,float],
+  [.1,False,float],
+  [False,False,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+  [True,False,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+  [None,True,None],
+  [2147483648,False,long],
+  [922337203685477580,False,long]
+]
+
+def test_expressions_with_null_equality():
+    for eq in null_equality:
+        context = mapnik.Context()
+        f = mapnik.Feature(context,0)
+        f["prop"] = eq[0]
+        eq_(f["prop"],eq[0])
+        if eq[0] is None:
+            eq_(f["prop"] is None, True)
+        else:
+            eq_(isinstance(f['prop'],eq[2]),True,'%s is not an instance of %s' % (f['prop'],eq[2]))
+        expr = mapnik.Expression("[prop] = null")
+        eq_(expr.evaluate(f),eq[1])
+        expr = mapnik.Expression("[prop] is null")
+        eq_(expr.evaluate(f),eq[1])
+
+def test_expressions_with_null_equality2():
+    for eq in null_equality:
+        context = mapnik.Context()
+        f = mapnik.Feature(context,0)
+        f["prop"] = eq[0]
+        eq_(f["prop"],eq[0])
+        if eq[0] is None:
+            eq_(f["prop"] is None, True)
+        else:
+            eq_(isinstance(f['prop'],eq[2]),True,'%s is not an instance of %s' % (f['prop'],eq[2]))
+        # TODO - support `is not` syntax:
+        # https://github.com/mapnik/mapnik/issues/796
+        expr = mapnik.Expression("not [prop] is null")
+        eq_(expr.evaluate(f),not eq[1])
+        # https://github.com/mapnik/mapnik/issues/1642
+        expr = mapnik.Expression("[prop] != null")
+        eq_(expr.evaluate(f),not eq[1])
+
+truthyness = [
+  [u'hello',True,unicode],
+  [u'',False,unicode],
+  [0,False,long],
+  [123,True,long],
+  [0.0,False,float],
+  [123.123,True,float],
+  [.1,True,float],
+  [False,False,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+  [True,True,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+  [None,False,None],
+  [2147483648,True,long],
+  [922337203685477580,True,long]
+]
+
+def test_expressions_for_thruthyness():
+    context = mapnik.Context()
+    for eq in truthyness:
+        f = mapnik.Feature(context,0)
+        f["prop"] = eq[0]
+        eq_(f["prop"],eq[0])
+        if eq[0] is None:
+            eq_(f["prop"] is None, True)
+        else:
+            eq_(isinstance(f['prop'],eq[2]),True,'%s is not an instance of %s' % (f['prop'],eq[2]))
+        expr = mapnik.Expression("[prop]")
+        eq_(expr.to_bool(f),eq[1])
+        expr = mapnik.Expression("not [prop]")
+        eq_(expr.to_bool(f),not eq[1])
+        expr = mapnik.Expression("! [prop]")
+        eq_(expr.to_bool(f),not eq[1])
+    # also test if feature does not have property at all
+    f2 = mapnik.Feature(context,1)
+    # no property existing will return value_null since
+    # https://github.com/mapnik/mapnik/commit/562fada9d0f680f59b2d9f396c95320a0d753479#include/mapnik/feature.hpp
+    eq_(f2["prop"] is None,True)
+    expr = mapnik.Expression("[prop]")
+    eq_(expr.evaluate(f2),None)
+    eq_(expr.to_bool(f2),False)
+
+# https://github.com/mapnik/mapnik/issues/1859
+def test_if_null_and_empty_string_are_equal():
+    context = mapnik.Context()
+    f = mapnik.Feature(context,0)
+    f["empty"] = u""
+    f["null"] = None
+    # ensure base assumptions are good
+    eq_(mapnik.Expression("[empty] = ''").to_bool(f),True)
+    eq_(mapnik.Expression("[null] = null").to_bool(f),True)
+    eq_(mapnik.Expression("[empty] != ''").to_bool(f),False)
+    eq_(mapnik.Expression("[null] != null").to_bool(f),False)
+    # now test expected behavior
+    eq_(mapnik.Expression("[null] = ''").to_bool(f),False)
+    eq_(mapnik.Expression("[empty] = null").to_bool(f),False)
+    eq_(mapnik.Expression("[empty] != null").to_bool(f),True)
+    # this one is the back compatibility shim
+    eq_(mapnik.Expression("[null] != ''").to_bool(f),False)
+
+def test_filtering_nulls_and_empty_strings():
+    context = mapnik.Context()
+    f = mapnik.Feature(context,0)
+    f["prop"] = u"hello"
+    eq_(f["prop"],u"hello")
+    eq_(mapnik.Expression("[prop]").to_bool(f),True)
+    eq_(mapnik.Expression("! [prop]").to_bool(f),False)
+    eq_(mapnik.Expression("[prop] != null").to_bool(f),True)
+    eq_(mapnik.Expression("[prop] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop] != null and [prop] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop] != null or [prop] != ''").to_bool(f),True)
+    f["prop2"] = u""
+    eq_(f["prop2"],u"")
+    eq_(mapnik.Expression("[prop2]").to_bool(f),False)
+    eq_(mapnik.Expression("! [prop2]").to_bool(f),True)
+    eq_(mapnik.Expression("[prop2] != null").to_bool(f),True)
+    eq_(mapnik.Expression("[prop2] != ''").to_bool(f),False)
+    eq_(mapnik.Expression("[prop2] = ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop2] != null or [prop2] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop2] != null and [prop2] != ''").to_bool(f),False)
+    f["prop3"] = None
+    eq_(f["prop3"],None)
+    eq_(mapnik.Expression("[prop3]").to_bool(f),False)
+    eq_(mapnik.Expression("! [prop3]").to_bool(f),True)
+    eq_(mapnik.Expression("[prop3] != null").to_bool(f),False)
+    eq_(mapnik.Expression("[prop3] = null").to_bool(f),True)
+
+    # https://github.com/mapnik/mapnik/issues/1859
+    #eq_(mapnik.Expression("[prop3] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop3] != ''").to_bool(f),False)
+
+    eq_(mapnik.Expression("[prop3] = ''").to_bool(f),False)
+
+    # https://github.com/mapnik/mapnik/issues/1859
+    #eq_(mapnik.Expression("[prop3] != null or [prop3] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop3] != null or [prop3] != ''").to_bool(f),False)
+
+    eq_(mapnik.Expression("[prop3] != null and [prop3] != ''").to_bool(f),False)
+    # attr not existing should behave the same as prop3
+    eq_(mapnik.Expression("[prop4]").to_bool(f),False)
+    eq_(mapnik.Expression("! [prop4]").to_bool(f),True)
+    eq_(mapnik.Expression("[prop4] != null").to_bool(f),False)
+    eq_(mapnik.Expression("[prop4] = null").to_bool(f),True)
+
+    # https://github.com/mapnik/mapnik/issues/1859
+    ##eq_(mapnik.Expression("[prop4] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop4] != ''").to_bool(f),False)
+
+    eq_(mapnik.Expression("[prop4] = ''").to_bool(f),False)
+
+    # https://github.com/mapnik/mapnik/issues/1859
+    ##eq_(mapnik.Expression("[prop4] != null or [prop4] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop4] != null or [prop4] != ''").to_bool(f),False)
+
+    eq_(mapnik.Expression("[prop4] != null and [prop4] != ''").to_bool(f),False)
+    f["prop5"] = False
+    eq_(f["prop5"],False)
+    eq_(mapnik.Expression("[prop5]").to_bool(f),False)
+    eq_(mapnik.Expression("! [prop5]").to_bool(f),True)
+    eq_(mapnik.Expression("[prop5] != null").to_bool(f),True)
+    eq_(mapnik.Expression("[prop5] = null").to_bool(f),False)
+    eq_(mapnik.Expression("[prop5] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop5] = ''").to_bool(f),False)
+    eq_(mapnik.Expression("[prop5] != null or [prop5] != ''").to_bool(f),True)
+    eq_(mapnik.Expression("[prop5] != null and [prop5] != ''").to_bool(f),True)
+    # note, we need to do [prop5] != 0 here instead of false due to this bug:
+    # https://github.com/mapnik/mapnik/issues/1873
+    eq_(mapnik.Expression("[prop5] != null and [prop5] != '' and [prop5] != 0").to_bool(f),False)
+
+# https://github.com/mapnik/mapnik/issues/1872
+def test_falseyness_comparision():
+    context = mapnik.Context()
+    f = mapnik.Feature(context,0)
+    f["prop"] = 0
+    eq_(mapnik.Expression("[prop]").to_bool(f),False)
+    eq_(mapnik.Expression("[prop] = false").to_bool(f),True)
+    eq_(mapnik.Expression("not [prop] != false").to_bool(f),True)
+    eq_(mapnik.Expression("not [prop] = true").to_bool(f),True)
+    eq_(mapnik.Expression("[prop] = true").to_bool(f),False)
+    eq_(mapnik.Expression("[prop] != true").to_bool(f),True)
+
+# https://github.com/mapnik/mapnik/issues/1806, fixed by https://github.com/mapnik/mapnik/issues/1872
+def test_truthyness_comparision():
+    context = mapnik.Context()
+    f = mapnik.Feature(context,0)
+    f["prop"] = 1
+    eq_(mapnik.Expression("[prop]").to_bool(f),True)
+    eq_(mapnik.Expression("[prop] = false").to_bool(f),False)
+    eq_(mapnik.Expression("not [prop] != false").to_bool(f),False)
+    eq_(mapnik.Expression("not [prop] = true").to_bool(f),False)
+    eq_(mapnik.Expression("[prop] = true").to_bool(f),True)
+    eq_(mapnik.Expression("[prop] != true").to_bool(f),False)
+
+def test_division_by_zero():
+    expr = mapnik.Expression('[a]/[b]')
+    c = mapnik.Context()
+    c.push('a')
+    c.push('b')
+    f = mapnik.Feature(c,0);
+    f['a'] = 1
+    f['b'] = 0
+    eq_(expr.evaluate(f),None)
+
+ at raises(RuntimeError)
+def test_invalid_syntax1():
+    mapnik.Expression('abs()')
+
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/fontset_test.py b/test/python_tests/fontset_test.py
new file mode 100644
index 0000000..ee8fd7d
--- /dev/null
+++ b/test/python_tests/fontset_test.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_loading_fontset_from_map():
+    m = mapnik.Map(256,256)
+    mapnik.load_map(m,'../data/good_maps/fontset.xml',True)
+    fs = m.find_fontset('book-fonts')
+    eq_(len(fs.names),2)
+    eq_(list(fs.names),['DejaVu Sans Book','DejaVu Sans Oblique'])
+
+# def test_loading_fontset_from_python():
+#     m = mapnik.Map(256,256)
+#     fset = mapnik.FontSet('foo')
+#     fset.add_face_name('Comic Sans')
+#     fset.add_face_name('Papyrus')
+#     eq_(fset.name,'foo')
+#     fset.name = 'my-set'
+#     eq_(fset.name,'my-set')
+#     m.append_fontset('my-set', fset)
+#     sty = mapnik.Style()
+#     rule = mapnik.Rule()
+#     tsym = mapnik.TextSymbolizer()
+#     eq_(tsym.fontset,None)
+#     tsym.fontset = fset
+#     rule.symbols.append(tsym)
+#     sty.rules.append(rule)
+#     m.append_style('Style',sty)
+#     serialized_map = mapnik.save_map_to_string(m)
+#     eq_('fontset-name="my-set"' in serialized_map,True)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/geojson_plugin_test.py b/test/python_tests/geojson_plugin_test.py
new file mode 100644
index 0000000..ef7c74a
--- /dev/null
+++ b/test/python_tests/geojson_plugin_test.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal
+from utilities import execution_path, run_all
+import os, mapnik
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'geojson' in mapnik.DatasourceCache.plugin_names():
+
+    def test_geojson_init():
+        ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+        e = ds.envelope()
+        assert_almost_equal(e.minx, -81.705583, places=7)
+        assert_almost_equal(e.miny, 41.480573, places=6)
+        assert_almost_equal(e.maxx, -81.705583, places=5)
+        assert_almost_equal(e.maxy, 41.480573, places=3)
+
+    def test_geojson_properties():
+        ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+        f = ds.features_at_point(ds.envelope().center()).features[0]
+        eq_(len(ds.fields()),7)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+
+        ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+        f = ds.all_features()[0]
+        eq_(len(ds.fields()),7)
+
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+    def test_large_geojson_properties():
+        ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson',cache_features = False)
+        f = ds.features_at_point(ds.envelope().center()).features[0]
+        eq_(len(ds.fields()),7)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+
+        ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+        f = ds.all_features()[0]
+        eq_(len(ds.fields()),7)
+
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+
+    def test_geojson_from_in_memory_string():
+        # will silently fail since it is a geometry and needs to be a featurecollection.
+        #ds = mapnik.Datasource(type='geojson',inline='{"type":"LineString","coordinates":[[0,0],[10,10]]}')
+        # works since it is a featurecollection
+        ds = mapnik.Datasource(type='geojson',inline='{ "type":"FeatureCollection", "features": [ { "type":"Feature", "properties":{"name":"test"}, "geometry": { "type":"LineString","coordinates":[[0,0],[10,10]] } } ]}')
+        eq_(len(ds.fields()),1)
+        f = ds.all_features()[0]
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.LineString)
+        eq_(f['name'], u'test')
+
+#    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+        eq_(len(ds.fields()),7)
+        # TODO - this sorting is messed up
+        #eq_(ds.fields(),['name', 'int', 'double', 'description', 'boolean', 'NOM_FR'])
+        #eq_(ds.field_types(),['str', 'int', 'float', 'str', 'bool', 'str'])
+# TODO - should geojson plugin throw like others?
+#        query = mapnik.Query(ds.envelope())
+#        for fld in ds.fields():
+#            query.add_property_name(fld)
+#        # also add an invalid one, triggering throw
+#        query.add_property_name('bogus')
+#        fs = ds.features(query)
+
+    def test_parsing_feature_collection_with_top_level_properties():
+        ds = mapnik.Datasource(type='geojson',file='../data/json/feature_collection_level_properties.json')
+        f = ds.all_features()[0]
+
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_(f['feat_name'], u'feat_value')
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/geometry_io_test.py b/test/python_tests/geometry_io_test.py
new file mode 100644
index 0000000..58e4f36
--- /dev/null
+++ b/test/python_tests/geometry_io_test.py
@@ -0,0 +1,273 @@
+#encoding: utf8
+
+from nose.tools import eq_,raises
+import os
+from utilities import execution_path, run_all
+import mapnik
+from binascii import unhexlify
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+wkts = [
+    [mapnik.GeometryType.Point,"POINT(30 10)", "01010000000000000000003e400000000000002440"],
+    [mapnik.GeometryType.Point,"POINT(30.0 10.0)", "01010000000000000000003e400000000000002440"],
+    [mapnik.GeometryType.Point,"POINT(30.1 10.1)", "01010000009a99999999193e403333333333332440"],
+    [mapnik.GeometryType.LineString,"LINESTRING(30 10,10 30,40 40)", "0102000000030000000000000000003e40000000000000244000000000000024400000000000003e4000000000000044400000000000004440"],
+    [mapnik.GeometryType.Polygon,"POLYGON((30 10,10 20,20 40,40 40,30 10))", "010300000001000000050000000000000000003e4000000000000024400000000000002440000000000000344000000000000034400000000000004440000000000000444000000000000044400000000000003e400000000000002440"],
+    [mapnik.GeometryType.Polygon,"POLYGON((35 10,10 20,15 40,45 45,35 10),(20 30,35 35,30 20,20 30))","0103000000020000000500000000000000008041400000000000002440000000000000244000000000000034400000000000002e40000000000000444000000000008046400000000000804640000000000080414000000000000024400400000000000000000034400000000000003e40000000000080414000000000008041400000000000003e40000000000000344000000000000034400000000000003e40"],
+    [mapnik.GeometryType.MultiPoint,"MULTIPOINT((10 40),(40 30),(20 20),(30 10))","010400000004000000010100000000000000000024400000000000004440010100000000000000000044400000000000003e4001010000000000000000003440000000000000344001010000000000000000003e400000000000002440"],
+    [mapnik.GeometryType.MultiLineString,"MULTILINESTRING((10 10,20 20,10 40),(40 40,30 30,40 20,30 10))","010500000002000000010200000003000000000000000000244000000000000024400000000000003440000000000000344000000000000024400000000000004440010200000004000000000000000000444000000000000044400000000000003e400000000000003e40000000000000444000000000000034400000000000003e400000000000002440"],
+    [mapnik.GeometryType.MultiPolygon,"MULTIPOLYGON(((30 20,10 40,45 40,30 20)),((15 5,40 10,10 20,5 10,15 5)))","010600000002000000010300000001000000040000000000000000003e40000000000000344000000000000024400000000000004440000000000080464000000000000044400000000000003e400000000000003440010300000001000000050000000000000000002e4000000000000014400000000000004440000000000000244000000000000024400000000000003440000000000000144000000000000024400000000000002e400000000000001440"],
+    [mapnik.GeometryType.MultiPolygon,"MULTIPOLYGON(((40 40,20 45,45 30,40 40)),((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20)))","01060000000200000001030000000100000004000000000000000000444000000000000044400000000000003440000000000080464000000000008046400000000000003e40000000000000444000000000000044400103000000020000000600000000000000000034400000000000804140000000000080464000000000000034400000000000003e40000000000000144000000000000024400000000000002440000000000000244000 [...]
+    [mapnik.GeometryType.GeometryCollection,"GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))","01070000000300000001030000000100000005000000000000000000f03f000000000000f03f0000000000000040000000000000f03f00000000000000400000000000000040000000000000f03f0000000000000040000000000000f03f000000000000f03f0101000000000000000000004000000000000008400102000000020000000000000000000040000000000000084000000000000008400000000000001040"],
+    [mapnik.GeometryType.Polygon,"POLYGON((-178.32319 71.518365,-178.321586 71.518439,-178.259635 71.510688,-178.304862 71.513129,-178.32319 71.518365),(-178.32319 71.518365,-178.341544 71.517524,-178.32244 71.505439,-178.215323 71.478034,-178.193473 71.47663,-178.147757 71.485175,-178.124442 71.481879,-178.005729 71.448615,-178.017203 71.441413,-178.054191 71.428778,-178.047049 71.425727,-178.033439 71.417792,-178.026236 71.415107,-178.030082 71.413459,-178.039908 71.40766,-177.970878 7 [...]
+    [mapnik.GeometryType.MultiPolygon,"MULTIPOLYGON(((-178.32319 71.518365,-178.321586 71.518439,-178.259635 71.510688,-178.304862 71.513129,-178.32319 71.518365)),((-178.32319 71.518365,-178.341544 71.517524,-178.32244 71.505439,-178.215323 71.478034,-178.193473 71.47663,-178.147757 71.485175,-178.124442 71.481879,-178.005729 71.448615,-178.017203 71.441413,-178.054191 71.428778,-178.047049 71.425727,-178.033439 71.417792,-178.026236 71.415107,-178.030082 71.413459,-178.039908 71.40766, [...]
+]
+
+
+geojson = [
+    [mapnik.GeometryType.Point,'{"type":"Point","coordinates":[30,10]}'],
+    [mapnik.GeometryType.Point,'{"type":"Point","coordinates":[30.0,10.0]}'],
+    [mapnik.GeometryType.Point,'{"type":"Point","coordinates":[30.1,10.1]}'],
+    [mapnik.GeometryType.LineString,'{"type":"LineString","coordinates":[[30.0,10.0],[10.0,30.0],[40.0,40.0]]}'],
+    [mapnik.GeometryType.Polygon,'{"type":"Polygon","coordinates":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]]}'],
+    [mapnik.GeometryType.Polygon,'{"type":"Polygon","coordinates":[[[35.0,10.0],[10.0,20.0],[15.0,40.0],[45.0,45.0],[35.0,10.0]],[[20.0,30.0],[35.0,35.0],[30.0,20.0],[20.0,30.0]]]}'],
+    [mapnik.GeometryType.MultiPoint,'{"type":"MultiPoint","coordinates":[[10.0,40.0],[40.0,30.0],[20.0,20.0],[30.0,10.0]]}'],
+    [mapnik.GeometryType.MultiLineString,'{"type":"MultiLineString","coordinates":[[[10.0,10.0],[20.0,20.0],[10.0,40.0]],[[40.0,40.0],[30.0,30.0],[40.0,20.0],[30.0,10.0]]]}'],
+    [mapnik.GeometryType.MultiPolygon,'{"type":"MultiPolygon","coordinates":[[[[30.0,20.0],[10.0,40.0],[45.0,40.0],[30.0,20.0]]],[[[15.0,5.0],[40.0,10.0],[10.0,20.0],[5.0,10.0],[15.0,5.0]]]]}'],
+    [mapnik.GeometryType.MultiPolygon,'{"type":"MultiPolygon","coordinates":[[[[40.0,40.0],[20.0,45.0],[45.0,30.0],[40.0,40.0]]],[[[20.0,35.0],[45.0,20.0],[30.0,5.0],[10.0,10.0],[10.0,30.0],[20.0,35.0]],[[30.0,20.0],[20.0,25.0],[20.0,15.0],[30.0,20.0]]]]}'],
+    [mapnik.GeometryType.GeometryCollection,'{"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[1.0,1.0],[2.0,1.0],[2.0,2.0],[1.0,2.0],[1.0,1.0]]]},{"type":"Point","coordinates":[2.0,3.0]},{"type":"LineString","coordinates":[[2.0,3.0],[3.0,4.0]]}]}'],
+    [mapnik.GeometryType.Polygon,'{"type":"Polygon","coordinates":[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]],[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178.026236,71.415 [...]
+    [mapnik.GeometryType.MultiPolygon,'{"type":"MultiPolygon","coordinates":[[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]]],[[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178. [...]
+]
+
+geojson_reversed = [
+    '{"coordinates":[30,10],"type":"Point"}',
+    '{"coordinates":[30.0,10.0],"type":"Point"}',
+    '{"coordinates":[30.1,10.1],"type":"Point"}',
+    '{"coordinates":[[30.0,10.0],[10.0,30.0],[40.0,40.0]],"type":"LineString"}',
+    '{"coordinates":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]],"type":"Polygon"}',
+    '{"coordinates":[[[35.0,10.0],[10.0,20.0],[15.0,40.0],[45.0,45.0],[35.0,10.0]],[[20.0,30.0],[35.0,35.0],[30.0,20.0],[20.0,30.0]]],"type":"Polygon"}',
+    '{"coordinates":[[10.0,40.0],[40.0,30.0],[20.0,20.0],[30.0,10.0]],"type":"MultiPoint"}',
+    '{"coordinates":[[[10.0,10.0],[20.0,20.0],[10.0,40.0]],[[40.0,40.0],[30.0,30.0],[40.0,20.0],[30.0,10.0]]],"type":"MultiLineString"}',
+    '{"coordinates":[[[[30.0,20.0],[10.0,40.0],[45.0,40.0],[30.0,20.0]]],[[[15.0,5.0],[40.0,10.0],[10.0,20.0],[5.0,10.0],[15.0,5.0]]]],"type":"MultiPolygon"}',
+    '{"coordinates":[[[[40.0,40.0],[20.0,45.0],[45.0,30.0],[40.0,40.0]]],[[[20.0,35.0],[45.0,20.0],[30.0,5.0],[10.0,10.0],[10.0,30.0],[20.0,35.0]],[[30.0,20.0],[20.0,25.0],[20.0,15.0],[30.0,20.0]]]],"type":"MultiPolygon"}',
+    '{"geometries":[{"coordinates":[[[1.0,1.0],[2.0,1.0],[2.0,2.0],[1.0,2.0],[1.0,1.0]]],"type":"Polygon"},{"coordinates":[2.0,3.0],"type":"Point"},{"coordinates":[[2.0,3.0],[3.0,4.0]],"type":"LineString"}],"type":"GeometryCollection"}',
+    '{"coordinates":[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]],[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178.026236,71.415107],[-178.030082,71.413459],[-178.039908,71.4 [...]
+    '{"coordinates":[[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]]],[[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178.026236,71.415107],[-178.030082,71.413459],[-178.039908,7 [...]
+]
+
+geojson_nulls = [
+  '{ "type": "Feature", "properties": { }, "geometry": null }',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "Point", "coordinates": [] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [] ] ] } }',
+  '{ "type": "Feature", "properties": { }, "geometry": { "coordinates": [], "type": "Point" }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "coordinates": [ [] ], "type": "LineString" }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "coordinates": [ [ [] ] ], "type": "Polygon" } }',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPoint", "coordinates": [ [] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPoint", "coordinates": [ [],[] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiLineString", "coordinates": [ [] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [] ] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPolygon", "coordinates": [ [] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [] ] ] }}',
+  '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [] ] ] ] }}',
+]
+
+# valid, but empty wkb's (http://trac.osgeo.org/postgis/wiki/DevWikiEmptyGeometry)
+empty_wkbs = [
+    # TODO - this is messed up: round trips as MULTIPOINT EMPTY
+    # template_postgis=# select ST_AsText(ST_GeomFromEWKB(decode(encode(ST_GeomFromText('POINT EMPTY'),'hex'),'hex')));
+    #    st_astext
+    #------------------
+    # MULTIPOINT EMPTY
+    #(1 row)
+    #[ mapnik.GeometryType.Point,              "Point EMPTY", '010400000000000000'],
+    [ mapnik.GeometryType.MultiPoint,         "MULTIPOINT EMPTY", '010400000000000000'],
+    [ mapnik.GeometryType.LineString,         "LINESTRING EMPTY", '010200000000000000'],
+    [ mapnik.GeometryType.LineString,         "LINESTRING EMPTY", '010200000000000000' ],
+    [ mapnik.GeometryType.MultiLineString,    "MULTILINESTRING EMPTY", '010500000000000000'],
+    [ mapnik.GeometryType.Polygon,            "Polygon EMPTY", '010300000000000000'],
+    [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION EMPTY", '010700000000000000'],
+    [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(LINESTRING EMPTY,LINESTRING EMPTY)", '010700000000000000'],
+    [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(POINT EMPTY,POINT EMPTY)", '010700000000000000'],
+]
+
+partially_empty_wkb = [
+    # TODO - currently this is not considered empty
+    # even though one part is
+    [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(MULTILINESTRING((10 10,20 20,10 40),(40 40,30 30,40 20,30 10)),LINESTRING EMPTY)", '010700000002000000010500000002000000010200000003000000000000000000244000000000000024400000000000003440000000000000344000000000000024400000000000004440010200000004000000000000000000444000000000000044400000000000003e400000000000003e40000000000000444000000000000034400000000000003e400000000000002440010200000000000000'],
+    [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(POINT EMPTY,POINT(0 0))", '010700000002000000010400000000000000010100000000000000000000000000000000000000'],
+    [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(POINT EMPTY,MULTIPOINT(0 0))", '010700000002000000010400000000000000010400000001000000010100000000000000000000000000000000000000'],
+]
+
+# unsupported types
+unsupported_wkb = [
+    [ "MULTIPOLYGON EMPTY", '010600000000000000'],
+    [ "TRIANGLE EMPTY", '011100000000000000'],
+    [ "CircularString EMPTY", '010800000000000000'],
+    [ "CurvePolygon EMPTY", '010A00000000000000'],
+    [ "CompoundCurve EMPTY", '010900000000000000'],
+    [ "MultiCurve EMPTY", '010B00000000000000'],
+    [ "MultiSurface EMPTY", '010C00000000000000'],
+    [ "PolyhedralSurface EMPTY", '010F00000000000000'],
+    [ "TIN EMPTY", '011000000000000000'],
+    # TODO - a few bogus inputs
+    # enable if we start range checking to avoid crashing on invalid input?
+    # https://github.com/mapnik/mapnik/issues/2236
+    #[ "", '' ],
+    #[ "00", '01' ],
+    #[ "0000", '0104' ],
+]
+
+def test_path_geo_interface():
+    geom = mapnik.Geometry.from_wkt('POINT(0 0)')
+    eq_(geom.__geo_interface__,{u'type': u'Point', u'coordinates': [0, 0]})
+
+def test_valid_wkb_parsing():
+    count = 0
+    for wkb in empty_wkbs:
+        geom = mapnik.Geometry.from_wkb(unhexlify(wkb[2]))
+        eq_(geom.is_empty(),True)
+        eq_(geom.type(),wkb[0])
+
+    for wkb in wkts:
+        geom = mapnik.Geometry.from_wkb(unhexlify(wkb[2]))
+        eq_(geom.is_empty(),False)
+        eq_(geom.type(),wkb[0])
+
+def test_wkb_parsing_error():
+    count = 0
+    for wkb in unsupported_wkb:
+        try:
+            geom = mapnik.Geometry.from_wkb(unhexlify(wkb))
+            # should not get here
+            eq_(True,False)
+        except:
+            pass
+    assert True
+
+# for partially empty wkbs don't currently look empty right now
+# since the enclosing container has objects
+def test_empty_wkb_parsing():
+    count = 0
+    for wkb in partially_empty_wkb:
+        geom = mapnik.Geometry.from_wkb(unhexlify(wkb[2]))
+        eq_(geom.type(),wkb[0])
+        eq_(geom.is_empty(),False)
+
+def test_geojson_parsing():
+    geometries = []
+    count = 0
+    for j in geojson:
+        count += 1
+        geometries.append(mapnik.Geometry.from_geojson(j[1]))
+    eq_(count,len(geometries))
+
+def test_geojson_parsing_reversed():
+    for idx,j in enumerate(geojson_reversed):
+        g1 = mapnik.Geometry.from_geojson(j)
+        g2 = mapnik.Geometry.from_geojson(geojson[idx][1])
+        eq_(g1.to_geojson(), g2.to_geojson())
+
+# http://geojson.org/geojson-spec.html#positions
+def test_geojson_point_positions():
+    input_json = '{"type":"Point","coordinates":[30,10]}'
+    geom = mapnik.Geometry.from_geojson(input_json)
+    eq_(geom.to_geojson(),input_json)
+    # should ignore all but the first two
+    geom = mapnik.Geometry.from_geojson('{"type":"Point","coordinates":[30,10,50,50,50,50]}')
+    eq_(geom.to_geojson(),input_json)
+
+def test_geojson_point_positions2():
+    input_json = '{"type":"LineString","coordinates":[[30,10],[10,30],[40,40]]}'
+    geom = mapnik.Geometry.from_geojson(input_json)
+    eq_(geom.to_geojson(),input_json)
+
+    # should ignore all but the first two
+    geom = mapnik.Geometry.from_geojson('{"type":"LineString","coordinates":[[30.0,10.0,0,0,0],[10.0,30.0,0,0,0],[40.0,40.0,0,0,0]]}')
+    eq_(geom.to_geojson(),input_json)
+
+def compare_wkb_from_wkt(wkt,type):
+    geom = mapnik.Geometry.from_wkt(wkt)
+    eq_(geom.type(),type)
+
+def compare_wkt_to_geojson(idx,wkt,num=None):
+    geom = mapnik.Geometry.from_wkt(wkt)
+    # ensure both have same result
+    gj = geom.to_geojson()
+    eq_(len(gj) > 1,True)
+    a = json.loads(gj)
+    e = json.loads(geojson[idx][1])
+    eq_(a,e)
+
+def test_wkt_simple():
+    for wkt in wkts:
+        try:
+            geom = mapnik.Geometry.from_wkt(wkt[1])
+            eq_(geom.type(),wkt[0])
+        except RuntimeError, e:
+            raise RuntimeError('%s %s' % (e, wkt))
+
+def test_wkb_simple():
+    for wkt in wkts:
+        try:
+            compare_wkb_from_wkt(wkt[1],wkt[0])
+        except RuntimeError, e:
+            raise RuntimeError('%s %s' % (e, wkt))
+
+def test_wkt_to_geojson():
+    idx = -1
+    for wkt in wkts:
+        try:
+            idx += 1
+            compare_wkt_to_geojson(idx,wkt[1],wkt[0])
+        except RuntimeError, e:
+            raise RuntimeError('%s %s' % (e, wkt))
+
+def test_wkt_rounding():
+    # currently fails because we use output precision of 6 - should we make configurable? https://github.com/mapnik/mapnik/issues/1009
+    # if precision is set to 15 still fails due to very subtle rounding issues
+    wkt = "POLYGON((7.904185 54.180426,7.89918 54.178168,7.897715 54.182318,7.893565 54.183111,7.890391 54.187567,7.885874 54.19068,7.879893 54.193915,7.894541 54.194647,7.900645 54.19068,7.904185 54.180426))"
+    geom = mapnik.Geometry.from_wkt(wkt)
+    eq_(geom.type(),mapnik.GeometryType.Polygon)
+
+def test_wkt_collection_flattening():
+    wkt = 'GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POLYGON((40 40,20 45,45 30,40 40)),POLYGON((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20)),LINESTRING(2 3,3 4))'
+    # currently fails as the MULTIPOLYGON inside will be returned as multiple polygons - not a huge deal - should we worry?
+    #wkt = "GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),MULTIPOLYGON(((40 40,20 45,45 30,40 40)),((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20))),LINESTRING(2 3,3 4))"
+    geom = mapnik.Geometry.from_wkt(wkt)
+    eq_(geom.type(),mapnik.GeometryType.GeometryCollection)
+
+def test_creating_feature_from_geojson():
+    json_feat = {
+      "type": "Feature",
+      "geometry": {"type": "Point", "coordinates": [-122,48]},
+      "properties": {"name": "value"}
+    }
+    ctx = mapnik.Context()
+    feat = mapnik.Feature.from_geojson(json.dumps(json_feat),ctx)
+    eq_(feat.id(),1)
+    eq_(feat['name'],u'value')
+
+def test_handling_geojson_null_geoms():
+    for j in geojson_nulls:
+        ctx = mapnik.Context()
+        out_json = mapnik.Feature.from_geojson(j,ctx).to_geojson()
+        expected = '{"type":"Feature","id":1,"geometry":null,"properties":{}}'
+        eq_(out_json,expected)
+        # ensure it round trips
+        eq_(mapnik.Feature.from_geojson(out_json,ctx).to_geojson(),expected)
+
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/grayscale_test.py b/test/python_tests/grayscale_test.py
new file mode 100644
index 0000000..2bcf836
--- /dev/null
+++ b/test/python_tests/grayscale_test.py
@@ -0,0 +1,13 @@
+import mapnik
+from nose.tools import eq_
+from utilities import run_all
+
+def test_grayscale_conversion():
+    im = mapnik.Image(2,2)
+    im.fill(mapnik.Color('white'))
+    im.set_grayscale_to_alpha()
+    pixel = im.get_pixel(0,0)
+    eq_((pixel >> 24) & 0xff,255);
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_encoding_speed_test.py b/test/python_tests/image_encoding_speed_test.py
new file mode 100644
index 0000000..75bbc85
--- /dev/null
+++ b/test/python_tests/image_encoding_speed_test.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from timeit import Timer, time
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+combinations = ['png',
+                'png8',
+                'png8:m=o',
+                'png8:m=h',
+                'png8:m=o:t=0',
+                'png8:m=o:t=1',
+                'png8:m=o:t=2',
+                'png8:m=h:t=0',
+                'png8:m=h:t=1',
+                'png8:m=h:t=2',
+                'png:z=1',
+                'png:z=1:t=0', # forces rbg, no a
+                'png8:z=1',
+                'png8:z=1:m=o',
+                'png8:z=1:m=h',
+                'png8:z=1:c=1',
+                'png8:z=1:c=24',
+                'png8:z=1:c=64',
+                'png8:z=1:c=128',
+                'png8:z=1:c=200',
+                'png8:z=1:c=255',
+                'png8:z=9:c=64',
+                'png8:z=9:c=128',
+                'png8:z=9:c=200',
+                'png8:z=1:c=50:m=h',
+                'png8:z=1:c=1:m=o',
+                'png8:z=1:c=1:m=o:s=filtered',
+                'png:z=1:s=filtered',
+                'png:z=1:s=huff',
+                'png:z=1:s=rle',
+                'png8:m=h:g=2.0',
+                'png8:m=h:g=1.0',
+                'png:e=miniz',
+                'png8:e=miniz'
+               ]
+
+tiles = [
+'blank',
+'solid',
+'many_colors',
+'aerial_24'
+]
+
+iterations = 10
+
+def do_encoding():
+
+    global image
+
+    results = {}
+    sortable = {}
+
+    def run(func, im, format, t):
+        global image
+        image = im
+        start = time.time()
+        set = t.repeat(iterations,1)
+        elapsed = (time.time() - start)
+        min_ = min(set)*1000
+        avg = (sum(set)/len(set))*1000
+        name = func.__name__ + ' ' + format
+        results[name] = [min_,avg,elapsed*1000,name,len(func())]
+        sortable[name] = [min_]
+
+    if 'blank' in tiles:
+        def blank():
+            return eval('image.tostring("%s")' % c)
+        blank_im = mapnik.Image(512,512)
+        for c in combinations:
+            t = Timer(blank)
+            run(blank,blank_im,c,t)
+
+    if 'solid' in tiles:
+        def solid():
+            return eval('image.tostring("%s")' % c)
+        solid_im = mapnik.Image(512,512)
+        solid_im.fill(mapnik.Color("#f2efe9"))
+        for c in combinations:
+            t = Timer(solid)
+            run(solid,solid_im,c,t)
+
+    if 'many_colors' in tiles:
+        def many_colors():
+            return eval('image.tostring("%s")' % c)
+        # lots of colors: http://tile.osm.org/13/4194/2747.png
+        many_colors_im = mapnik.Image.open('../data/images/13_4194_2747.png')
+        for c in combinations:
+            t = Timer(many_colors)
+            run(many_colors,many_colors_im,c,t)
+
+    if 'aerial_24' in tiles:
+        def aerial_24():
+            return eval('image.tostring("%s")' % c)
+        aerial_24_im = mapnik.Image.open('../data/images/12_654_1580.png')
+        for c in combinations:
+            t = Timer(aerial_24)
+            run(aerial_24,aerial_24_im,c,t)
+
+    for key, value in sorted(sortable.iteritems(), key=lambda (k,v): (v,k)):
+        s = results[key]
+        min_ = str(s[0])[:6]
+        avg = str(s[1])[:6]
+        elapsed = str(s[2])[:6]
+        name = s[3]
+        size = s[4]
+        print 'min: %sms | avg: %sms | total: %sms | len: %s <-- %s' % (min_,avg,elapsed,size,name)
+
+
+if __name__ == "__main__":
+    setup()
+    do_encoding()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_filters_test.py b/test/python_tests/image_filters_test.py
new file mode 100644
index 0000000..269d64c
--- /dev/null
+++ b/test/python_tests/image_filters_test.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+from utilities import side_by_side_image
+import os, mapnik
+import re
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def replace_style(m, name, style):
+    m.remove_style(name)
+    m.append_style(name, style)
+
+def test_append():
+    s = mapnik.Style()
+    eq_(s.image_filters,'')
+    s.image_filters = 'gray'
+    eq_(s.image_filters,'gray')
+    s.image_filters = 'sharpen'
+    eq_(s.image_filters,'sharpen')
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+    def test_style_level_image_filter():
+        m = mapnik.Map(256, 256)
+        mapnik.load_map(m, '../data/good_maps/style_level_image_filter.xml')
+        m.zoom_all()
+        successes = []
+        fails = []
+        for name in ("", "agg-stack-blur(2,2)", "blur",
+                     "edge-detect", "emboss", "gray", "invert",
+                     "sharpen", "sobel", "x-gradient", "y-gradient"):
+            if name == "":
+                filename = "none"
+            else:
+                filename = re.sub(r"[^-_a-z.0-9]", "", name)
+            # find_style returns a copy of the style object
+            style_markers = m.find_style("markers")
+            style_markers.image_filters = name
+            style_labels = m.find_style("labels")
+            style_labels.image_filters = name
+            # replace the original style with the modified one
+            replace_style(m, "markers", style_markers)
+            replace_style(m, "labels", style_labels)
+            im = mapnik.Image(m.width, m.height)
+            mapnik.render(m, im)
+            actual = '/tmp/mapnik-style-image-filter-' + filename + '.png'
+            expected = 'images/style-image-filter/' + filename + '.png'
+            im.save(actual,"png32")
+            if not os.path.exists(expected) or os.environ.get('UPDATE'):
+                print 'generating expected test image: %s' % expected
+                im.save(expected,'png32')
+            expected_im = mapnik.Image.open(expected)
+            # compare them
+            if im.tostring('png32') == expected_im.tostring('png32'):
+                successes.append(name)
+            else:
+                fails.append('failed comparing actual (%s) and expected(%s)' % (actual,'tests/python_tests/'+ expected))
+                fail_im = side_by_side_image(expected_im, im)
+                fail_im.save('/tmp/mapnik-style-image-filter-' + filename + '.fail.png','png32')
+        eq_(len(fails), 0, '\n'+'\n'.join(fails))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_test.py b/test/python_tests/image_test.py
new file mode 100644
index 0000000..189f8be
--- /dev/null
+++ b/test/python_tests/image_test.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import eq_,raises, assert_almost_equal
+from utilities import execution_path, run_all, get_unique_colors
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_type():
+    im = mapnik.Image(256, 256)
+    eq_(im.get_type(), mapnik.ImageType.rgba8)
+    im = mapnik.Image(256, 256, mapnik.ImageType.gray8)
+    eq_(im.get_type(), mapnik.ImageType.gray8)
+
+def test_image_premultiply():
+    im = mapnik.Image(256,256)
+    eq_(im.premultiplied(),False)
+    # Premultiply should return true that it worked
+    eq_(im.premultiply(), True)
+    eq_(im.premultiplied(),True)
+    # Premultipling again should return false as nothing should happen
+    eq_(im.premultiply(), False)
+    eq_(im.premultiplied(),True)
+    # Demultiply should return true that it worked
+    eq_(im.demultiply(), True)
+    eq_(im.premultiplied(),False)
+    # Demultiply again should not work and return false as it did nothing
+    eq_(im.demultiply(), False)
+    eq_(im.premultiplied(),False)
+
+def test_image_premultiply_values():
+    im = mapnik.Image(256,256)
+    im.fill(mapnik.Color(16, 33, 255, 128))
+    im.premultiply()
+    c = im.get_pixel(0,0, True)
+    eq_(c.r, 8)
+    eq_(c.g, 17)
+    eq_(c.b, 128)
+    eq_(c.a, 128)
+    im.demultiply()
+    # Do to the nature of this operation the result will not be exactly the same
+    c = im.get_pixel(0,0,True)
+    eq_(c.r,15)
+    eq_(c.g,33)
+    eq_(c.b,255)
+    eq_(c.a,128)
+
+def test_apply_opacity():
+    im = mapnik.Image(4,4)
+    im.fill(mapnik.Color(128,128,128,128))
+    im.apply_opacity(0.75);
+    c = im.get_pixel(0,0,True)
+    eq_(c.r,128)
+    eq_(c.g,128)
+    eq_(c.b,128)
+    eq_(c.a,96)
+
+def test_background():
+    im = mapnik.Image(256,256)
+    eq_(im.premultiplied(), False)
+    im.fill(mapnik.Color(32,64,125,128))
+    eq_(im.premultiplied(), False)
+    c = im.get_pixel(0,0,True)
+    eq_(c.get_premultiplied(), False)
+    eq_(c.r,32)
+    eq_(c.g,64)
+    eq_(c.b,125)
+    eq_(c.a,128)
+    # Now again with a premultiplied alpha
+    im.fill(mapnik.Color(32,64,125,128,True))
+    eq_(im.premultiplied(), True)
+    c = im.get_pixel(0,0,True)
+    eq_(c.get_premultiplied(), True)
+    eq_(c.r,32)
+    eq_(c.g,64)
+    eq_(c.b,125)
+    eq_(c.a,128)
+
+def test_set_and_get_pixel():
+    # Create an image that is not premultiplied
+    im = mapnik.Image(256,256)
+    c0 = mapnik.Color(16,33,255,128)
+    c0_pre = mapnik.Color(16,33,255,128, True)
+    im.set_pixel(0,0,c0)
+    im.set_pixel(1,1,c0_pre)
+    # No differences for non premultiplied pixels
+    c1_int = mapnik.Color(im.get_pixel(0,0))
+    eq_(c0.r, c1_int.r)
+    eq_(c0.g, c1_int.g)
+    eq_(c0.b, c1_int.b)
+    eq_(c0.a, c1_int.a)
+    c1 = im.get_pixel(0,0,True)
+    eq_(c0.r, c1.r)
+    eq_(c0.g, c1.g)
+    eq_(c0.b, c1.b)
+    eq_(c0.a, c1.a)
+    # The premultiplied Color should be demultiplied before being applied.
+    c0_pre.demultiply()
+    c1_int = mapnik.Color(im.get_pixel(1,1))
+    eq_(c0_pre.r, c1_int.r)
+    eq_(c0_pre.g, c1_int.g)
+    eq_(c0_pre.b, c1_int.b)
+    eq_(c0_pre.a, c1_int.a)
+    c1 = im.get_pixel(1,1,True)
+    eq_(c0_pre.r, c1.r)
+    eq_(c0_pre.g, c1.g)
+    eq_(c0_pre.b, c1.b)
+    eq_(c0_pre.a, c1.a)
+    
+    # Now create a new image that is premultiplied
+    im = mapnik.Image(256,256, mapnik.ImageType.rgba8, True, True)
+    c0 = mapnik.Color(16,33,255,128)
+    c0_pre = mapnik.Color(16,33,255,128, True)
+    im.set_pixel(0,0,c0)
+    im.set_pixel(1,1,c0_pre)
+    # It should have put pixels that are the same as premultiplied so premultiply c0
+    c0.premultiply()
+    c1_int = mapnik.Color(im.get_pixel(0,0))
+    eq_(c0.r, c1_int.r)
+    eq_(c0.g, c1_int.g)
+    eq_(c0.b, c1_int.b)
+    eq_(c0.a, c1_int.a)
+    c1 = im.get_pixel(0,0,True)
+    eq_(c0.r, c1.r)
+    eq_(c0.g, c1.g)
+    eq_(c0.b, c1.b)
+    eq_(c0.a, c1.a)
+    # The premultiplied Color should be the same though
+    c1_int = mapnik.Color(im.get_pixel(1,1))
+    eq_(c0_pre.r, c1_int.r)
+    eq_(c0_pre.g, c1_int.g)
+    eq_(c0_pre.b, c1_int.b)
+    eq_(c0_pre.a, c1_int.a)
+    c1 = im.get_pixel(1,1,True)
+    eq_(c0_pre.r, c1.r)
+    eq_(c0_pre.g, c1.g)
+    eq_(c0_pre.b, c1.b)
+    eq_(c0_pre.a, c1.a)
+
+def test_pixel_gray8():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray8)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray8s():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray8s)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_gray16():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray16)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray16s():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray16s)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_gray32():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray32)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray32s():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray32s)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_gray64():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray64)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray64s():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray64s)
+    val_list = range(20)
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        eq_(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_floats():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray32f)
+    val_list = [0.9, 0.99, 0.999, 0.9999, 0.99999, 1, 1.0001, 1.001, 1.01, 1.1]
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        assert_almost_equal(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        assert_almost_equal(im.get_pixel(0,0), -v)
+
+def test_pixel_doubles():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray64f)
+    val_list = [0.9, 0.99, 0.999, 0.9999, 0.99999, 1, 1.0001, 1.001, 1.01, 1.1]
+    for v in val_list:
+        im.set_pixel(0,0, v)
+        assert_almost_equal(im.get_pixel(0,0), v)
+        im.set_pixel(0,0, -v)
+        assert_almost_equal(im.get_pixel(0,0), -v)
+
+def test_pixel_overflow():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray8)
+    im.set_pixel(0,0,256)
+    eq_(im.get_pixel(0,0),255)
+
+def test_pixel_underflow():
+    im = mapnik.Image(4,4,mapnik.ImageType.gray8)
+    im.set_pixel(0,0,-1)
+    eq_(im.get_pixel(0,0),0)
+    im = mapnik.Image(4,4,mapnik.ImageType.gray16)
+    im.set_pixel(0,0,-1)
+    eq_(im.get_pixel(0,0),0)
+
+ at raises(IndexError)
+def test_set_pixel_out_of_range_1():
+    im = mapnik.Image(4,4)
+    c = mapnik.Color('blue')
+    im.set_pixel(5,5,c)
+
+ at raises(OverflowError)
+def test_set_pixel_out_of_range_2():
+    im = mapnik.Image(4,4)
+    c = mapnik.Color('blue')
+    im.set_pixel(-1,1,c)
+
+ at raises(IndexError)
+def test_get_pixel_out_of_range_1():
+    im = mapnik.Image(4,4)
+    c = im.get_pixel(5,5)
+
+ at raises(OverflowError)
+def test_get_pixel_out_of_range_2():
+    im = mapnik.Image(4,4)
+    c = im.get_pixel(-1,1)
+
+ at raises(IndexError)
+def test_get_pixel_color_out_of_range_1():
+    im = mapnik.Image(4,4)
+    c = im.get_pixel(5,5,True)
+
+ at raises(OverflowError)
+def test_get_pixel_color_out_of_range_2():
+    im = mapnik.Image(4,4)
+    c = im.get_pixel(-1,1,True)
+    
+def test_set_color_to_alpha():
+    im = mapnik.Image(256,256)
+    im.fill(mapnik.Color('rgba(12,12,12,255)'))
+    eq_(get_unique_colors(im), ['rgba(12,12,12,255)'])
+    im.set_color_to_alpha(mapnik.Color('rgba(12,12,12,0)'))
+    eq_(get_unique_colors(im), ['rgba(0,0,0,0)'])
+
+ at raises(RuntimeError)
+def test_negative_image_dimensions():
+    # TODO - this may have regressed in https://github.com/mapnik/mapnik/commit/4f3521ac24b61fc8ae8fd344a16dc3a5fdf15af7
+    im = mapnik.Image(-40,40)
+    # should not get here
+    eq_(im.width(),0)
+    eq_(im.height(),0)
+
+def test_jpeg_round_trip():
+    filepath = '/tmp/mapnik-jpeg-io.jpeg'
+    im = mapnik.Image(255,267)
+    im.fill(mapnik.Color('rgba(1,2,3,.5)'))
+    im.save(filepath,'jpeg')
+    im2 = mapnik.Image.open(filepath)
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    eq_(len(im.tostring()),len(im2.tostring()))
+    eq_(len(im.tostring('jpeg')),len(im2.tostring('jpeg')))
+    eq_(len(im.tostring()),len(im3.tostring()))
+    eq_(len(im.tostring('jpeg')),len(im3.tostring('jpeg')))
+
+def test_png_round_trip():
+    filepath = '/tmp/mapnik-png-io.png'
+    im = mapnik.Image(255,267)
+    im.fill(mapnik.Color('rgba(1,2,3,.5)'))
+    im.save(filepath,'png')
+    im2 = mapnik.Image.open(filepath)
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    eq_(len(im.tostring()),len(im2.tostring()))
+    eq_(len(im.tostring('png')),len(im2.tostring('png')))
+    eq_(len(im.tostring('png8')),len(im2.tostring('png8')))
+    eq_(len(im.tostring()),len(im3.tostring()))
+    eq_(len(im.tostring('png')),len(im3.tostring('png')))
+    eq_(len(im.tostring('png8')),len(im3.tostring('png8')))
+
+def test_image_open_from_string():
+    filepath = '../data/images/dummy.png'
+    im1 = mapnik.Image.open(filepath)
+    im2 = mapnik.Image.fromstring(open(filepath,'rb').read())
+    eq_(im1.width(),im2.width())
+    length = len(im1.tostring())
+    eq_(length,len(im2.tostring()))
+    eq_(len(mapnik.Image.fromstring(im1.tostring('png')).tostring()),length)
+    eq_(len(mapnik.Image.fromstring(im1.tostring('jpeg')).tostring()),length)
+    eq_(len(mapnik.Image.frombuffer(buffer(im1.tostring('png'))).tostring()),length)
+    eq_(len(mapnik.Image.frombuffer(buffer(im1.tostring('jpeg'))).tostring()),length)
+
+    # TODO - https://github.com/mapnik/mapnik/issues/1831
+    eq_(len(mapnik.Image.fromstring(im1.tostring('tiff')).tostring()),length)
+    eq_(len(mapnik.Image.frombuffer(buffer(im1.tostring('tiff'))).tostring()),length)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_tiff_test.py b/test/python_tests/image_tiff_test.py
new file mode 100644
index 0000000..e0535d0
--- /dev/null
+++ b/test/python_tests/image_tiff_test.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+import hashlib
+from nose.tools import eq_, assert_not_equal
+from utilities import execution_path, run_all
+
+def hashstr(var):
+    return hashlib.md5(var).hexdigest()
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_tiff_round_trip_scanline():
+    filepath = '/tmp/mapnik-tiff-io-scanline.tiff'
+    im = mapnik.Image(255,267)
+    im.fill(mapnik.Color('rgba(12,255,128,.5)'))
+    org_str = hashstr(im.tostring())
+    im.save(filepath,'tiff:method=scanline')
+    im2 = mapnik.Image.open(filepath)
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    eq_(hashstr(im.tostring()), org_str)
+    # This won't be the same the first time around because the im is not premultiplied and im2 is
+    assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+    assert_not_equal(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+    # Now premultiply
+    im.premultiply()
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+    eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+    eq_(hashstr(im2.tostring('tiff:method=scanline')),hashstr(im3.tostring('tiff:method=scanline')))
+
+def test_tiff_round_trip_stripped():
+    filepath = '/tmp/mapnik-tiff-io-stripped.tiff'
+    im = mapnik.Image(255,267)
+    im.fill(mapnik.Color('rgba(12,255,128,.5)'))
+    org_str = hashstr(im.tostring())
+    im.save(filepath,'tiff:method=stripped')
+    im2 = mapnik.Image.open(filepath)
+    im2.save('/tmp/mapnik-tiff-io-stripped2.tiff','tiff:method=stripped')
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+    # difference in tags.
+    assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+    assert_not_equal(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+    # Now if we premultiply they will be exactly the same
+    im.premultiply()
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+    eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+    # Both of these started out premultiplied, so this round trip should be exactly the same!
+    eq_(hashstr(im2.tostring('tiff:method=stripped')),hashstr(im3.tostring('tiff:method=stripped')))
+
+def test_tiff_round_trip_rows_stripped():
+    filepath = '/tmp/mapnik-tiff-io-rows_stripped.tiff'
+    filepath2 = '/tmp/mapnik-tiff-io-rows_stripped2.tiff'
+    im = mapnik.Image(255,267)
+    im.fill(mapnik.Color('rgba(12,255,128,.5)'))
+    c = im.get_pixel(0,0,True)
+    eq_(c.r, 12)
+    eq_(c.g, 255)
+    eq_(c.b, 128)
+    eq_(c.a, 128)
+    eq_(c.get_premultiplied(), False)
+    im.save(filepath,'tiff:method=stripped:rows_per_strip=8')
+    im2 = mapnik.Image.open(filepath)
+    c2 = im2.get_pixel(0,0,True)
+    eq_(c2.r, 6)
+    eq_(c2.g, 128)
+    eq_(c2.b, 64)
+    eq_(c2.a, 128)
+    eq_(c2.get_premultiplied(), True)
+    im2.save(filepath2,'tiff:method=stripped:rows_per_strip=8')
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+    # difference in tags. 
+    assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+    assert_not_equal(hashstr(im.tostring('tiff:method=stripped:rows_per_strip=8')),hashstr(im2.tostring('tiff:method=stripped:rows_per_strip=8')))
+    # Now premultiply the first image and they will be the same!
+    im.premultiply()
+    eq_(hashstr(im.tostring('tiff:method=stripped:rows_per_strip=8')),hashstr(im2.tostring('tiff:method=stripped:rows_per_strip=8')))
+    eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+    # Both of these started out premultiplied, so this round trip should be exactly the same!
+    eq_(hashstr(im2.tostring('tiff:method=stripped:rows_per_strip=8')),hashstr(im3.tostring('tiff:method=stripped:rows_per_strip=8')))
+
+def test_tiff_round_trip_buffered_tiled():
+    filepath = '/tmp/mapnik-tiff-io-buffered-tiled.tiff'
+    filepath2 = '/tmp/mapnik-tiff-io-buffered-tiled2.tiff'
+    filepath3 = '/tmp/mapnik-tiff-io-buffered-tiled3.tiff'
+    im = mapnik.Image(255,267)
+    im.fill(mapnik.Color('rgba(33,255,128,.5)'))
+    c = im.get_pixel(0,0,True)
+    eq_(c.r, 33)
+    eq_(c.g, 255)
+    eq_(c.b, 128)
+    eq_(c.a, 128)
+    eq_(c.get_premultiplied(), False)
+    im.save(filepath,'tiff:method=tiled:tile_width=32:tile_height=32')
+    im2 = mapnik.Image.open(filepath)
+    c2 = im2.get_pixel(0,0,True)
+    eq_(c2.r, 17)
+    eq_(c2.g, 128)
+    eq_(c2.b, 64)
+    eq_(c2.a, 128)
+    eq_(c2.get_premultiplied(), True)
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    im2.save(filepath2, 'tiff:method=tiled:tile_width=32:tile_height=32')
+    im3.save(filepath3, 'tiff:method=tiled:tile_width=32:tile_height=32')
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+    # difference in tags.
+    assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+    assert_not_equal(hashstr(im.tostring('tiff:method=tiled:tile_width=32:tile_height=32')),hashstr(im2.tostring('tiff:method=tiled:tile_width=32:tile_height=32')))
+    # Now premultiply the first image and they should be the same
+    im.premultiply()
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=tiled:tile_width=32:tile_height=32')),hashstr(im2.tostring('tiff:method=tiled:tile_width=32:tile_height=32')))
+    eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+    # Both of these started out premultiplied, so this round trip should be exactly the same!
+    eq_(hashstr(im2.tostring('tiff:method=tiled:tile_width=32:tile_height=32')),hashstr(im3.tostring('tiff:method=tiled:tile_width=32:tile_height=32')))
+
+def test_tiff_round_trip_tiled():
+    filepath = '/tmp/mapnik-tiff-io-tiled.tiff'
+    im = mapnik.Image(256,256)
+    im.fill(mapnik.Color('rgba(1,255,128,.5)'))
+    im.save(filepath,'tiff:method=tiled')
+    im2 = mapnik.Image.open(filepath)
+    im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(im.width(),im3.width())
+    eq_(im.height(),im3.height())
+    # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+    # difference in tags.
+    assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+    assert_not_equal(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+    # Now premultiply the first image and they will be exactly the same.
+    im.premultiply()
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+    eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+    # Both of these started out premultiplied, so this round trip should be exactly the same!
+    eq_(hashstr(im2.tostring('tiff:method=tiled')),hashstr(im3.tostring('tiff:method=tiled')))
+
+
+def test_tiff_rgb8_compare():
+    filepath1 = '../data/tiff/ndvi_256x256_rgb8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-rgb8.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff')),hashstr(im2.tostring('tiff')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_rgba8_compare_scanline():
+    filepath1 = '../data/tiff/ndvi_256x256_rgba8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-rgba8-scanline.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=scanline')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_rgba8_compare_stripped():
+    filepath1 = '../data/tiff/ndvi_256x256_rgba8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-rgba8-stripped.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=stripped')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_rgba8_compare_tiled():
+    filepath1 = '../data/tiff/ndvi_256x256_rgba8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-rgba8-stripped.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=tiled')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_gray8_compare_scanline():
+    filepath1 = '../data/tiff/ndvi_256x256_gray8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray8-scanline.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=scanline')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray8).tostring("tiff")),True)
+
+def test_tiff_gray8_compare_stripped():
+    filepath1 = '../data/tiff/ndvi_256x256_gray8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray8-stripped.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=stripped')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray8).tostring("tiff")),True)
+
+def test_tiff_gray8_compare_tiled():
+    filepath1 = '../data/tiff/ndvi_256x256_gray8_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray8-tiled.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=tiled')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray8).tostring("tiff")),True)
+
+def test_tiff_gray16_compare_scanline():
+    filepath1 = '../data/tiff/ndvi_256x256_gray16_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray16-scanline.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=scanline')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray16).tostring("tiff")),True)
+
+def test_tiff_gray16_compare_stripped():
+    filepath1 = '../data/tiff/ndvi_256x256_gray16_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray16-stripped.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=stripped')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray16).tostring("tiff")),True)
+
+def test_tiff_gray16_compare_tiled():
+    filepath1 = '../data/tiff/ndvi_256x256_gray16_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray16-tiled.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=tiled')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray16).tostring("tiff")),True)
+
+def test_tiff_gray32f_compare_scanline():
+    filepath1 = '../data/tiff/ndvi_256x256_gray32f_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray32f-scanline.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=scanline')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray32f).tostring("tiff")),True)
+
+def test_tiff_gray32f_compare_stripped():
+    filepath1 = '../data/tiff/ndvi_256x256_gray32f_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray32f-stripped.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=stripped')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray32f).tostring("tiff")),True)
+
+def test_tiff_gray32f_compare_tiled():
+    filepath1 = '../data/tiff/ndvi_256x256_gray32f_striped.tif'
+    filepath2 = '/tmp/mapnik-tiff-gray32f-tiled.tiff'
+    im = mapnik.Image.open(filepath1)
+    im.save(filepath2,'tiff:method=tiled')
+    im2 = mapnik.Image.open(filepath2)
+    eq_(im.width(),im2.width())
+    eq_(im.height(),im2.height())
+    eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+    eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+    # should not be a blank image
+    eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray32f).tostring("tiff")),True)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/images/actual.png b/test/python_tests/images/actual.png
new file mode 100644
index 0000000..adfa856
Binary files /dev/null and b/test/python_tests/images/actual.png differ
diff --git a/test/python_tests/images/composited/clear.png b/test/python_tests/images/composited/clear.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/composited/clear.png differ
diff --git a/test/python_tests/images/composited/color.png b/test/python_tests/images/composited/color.png
new file mode 100644
index 0000000..f9a6879
Binary files /dev/null and b/test/python_tests/images/composited/color.png differ
diff --git a/test/python_tests/images/composited/color_burn.png b/test/python_tests/images/composited/color_burn.png
new file mode 100644
index 0000000..af985bd
Binary files /dev/null and b/test/python_tests/images/composited/color_burn.png differ
diff --git a/test/python_tests/images/composited/color_dodge.png b/test/python_tests/images/composited/color_dodge.png
new file mode 100644
index 0000000..6c45b85
Binary files /dev/null and b/test/python_tests/images/composited/color_dodge.png differ
diff --git a/test/python_tests/images/composited/contrast.png b/test/python_tests/images/composited/contrast.png
new file mode 100644
index 0000000..54ea219
Binary files /dev/null and b/test/python_tests/images/composited/contrast.png differ
diff --git a/test/python_tests/images/composited/darken.png b/test/python_tests/images/composited/darken.png
new file mode 100644
index 0000000..4324c0a
Binary files /dev/null and b/test/python_tests/images/composited/darken.png differ
diff --git a/test/python_tests/images/composited/difference.png b/test/python_tests/images/composited/difference.png
new file mode 100644
index 0000000..312bded
Binary files /dev/null and b/test/python_tests/images/composited/difference.png differ
diff --git a/test/python_tests/images/composited/divide.png b/test/python_tests/images/composited/divide.png
new file mode 100644
index 0000000..0a4b24f
Binary files /dev/null and b/test/python_tests/images/composited/divide.png differ
diff --git a/test/python_tests/images/composited/dst.png b/test/python_tests/images/composited/dst.png
new file mode 100644
index 0000000..14be353
Binary files /dev/null and b/test/python_tests/images/composited/dst.png differ
diff --git a/test/python_tests/images/composited/dst_atop.png b/test/python_tests/images/composited/dst_atop.png
new file mode 100644
index 0000000..845c370
Binary files /dev/null and b/test/python_tests/images/composited/dst_atop.png differ
diff --git a/test/python_tests/images/composited/dst_in.png b/test/python_tests/images/composited/dst_in.png
new file mode 100644
index 0000000..1664be0
Binary files /dev/null and b/test/python_tests/images/composited/dst_in.png differ
diff --git a/test/python_tests/images/composited/dst_out.png b/test/python_tests/images/composited/dst_out.png
new file mode 100644
index 0000000..eb943bc
Binary files /dev/null and b/test/python_tests/images/composited/dst_out.png differ
diff --git a/test/python_tests/images/composited/dst_over.png b/test/python_tests/images/composited/dst_over.png
new file mode 100644
index 0000000..51fe08e
Binary files /dev/null and b/test/python_tests/images/composited/dst_over.png differ
diff --git a/test/python_tests/images/composited/exclusion.png b/test/python_tests/images/composited/exclusion.png
new file mode 100644
index 0000000..6cf4fa7
Binary files /dev/null and b/test/python_tests/images/composited/exclusion.png differ
diff --git a/test/python_tests/images/composited/grain_extract.png b/test/python_tests/images/composited/grain_extract.png
new file mode 100644
index 0000000..cfa03e1
Binary files /dev/null and b/test/python_tests/images/composited/grain_extract.png differ
diff --git a/test/python_tests/images/composited/grain_merge.png b/test/python_tests/images/composited/grain_merge.png
new file mode 100644
index 0000000..78de8b5
Binary files /dev/null and b/test/python_tests/images/composited/grain_merge.png differ
diff --git a/test/python_tests/images/composited/hard_light.png b/test/python_tests/images/composited/hard_light.png
new file mode 100644
index 0000000..9d878de
Binary files /dev/null and b/test/python_tests/images/composited/hard_light.png differ
diff --git a/test/python_tests/images/composited/hue.png b/test/python_tests/images/composited/hue.png
new file mode 100644
index 0000000..96ed7a6
Binary files /dev/null and b/test/python_tests/images/composited/hue.png differ
diff --git a/test/python_tests/images/composited/invert.png b/test/python_tests/images/composited/invert.png
new file mode 100644
index 0000000..03e8e94
Binary files /dev/null and b/test/python_tests/images/composited/invert.png differ
diff --git a/test/python_tests/images/composited/invert_rgb.png b/test/python_tests/images/composited/invert_rgb.png
new file mode 100644
index 0000000..5a8904f
Binary files /dev/null and b/test/python_tests/images/composited/invert_rgb.png differ
diff --git a/test/python_tests/images/composited/lighten.png b/test/python_tests/images/composited/lighten.png
new file mode 100644
index 0000000..3b8a860
Binary files /dev/null and b/test/python_tests/images/composited/lighten.png differ
diff --git a/test/python_tests/images/composited/linear_burn.png b/test/python_tests/images/composited/linear_burn.png
new file mode 100644
index 0000000..37ec4b7
Binary files /dev/null and b/test/python_tests/images/composited/linear_burn.png differ
diff --git a/test/python_tests/images/composited/linear_dodge.png b/test/python_tests/images/composited/linear_dodge.png
new file mode 100644
index 0000000..848ddca
Binary files /dev/null and b/test/python_tests/images/composited/linear_dodge.png differ
diff --git a/test/python_tests/images/composited/minus.png b/test/python_tests/images/composited/minus.png
new file mode 100644
index 0000000..46a7647
Binary files /dev/null and b/test/python_tests/images/composited/minus.png differ
diff --git a/test/python_tests/images/composited/multiply.png b/test/python_tests/images/composited/multiply.png
new file mode 100644
index 0000000..0c6880f
Binary files /dev/null and b/test/python_tests/images/composited/multiply.png differ
diff --git a/test/python_tests/images/composited/overlay.png b/test/python_tests/images/composited/overlay.png
new file mode 100644
index 0000000..77df0d3
Binary files /dev/null and b/test/python_tests/images/composited/overlay.png differ
diff --git a/test/python_tests/images/composited/plus.png b/test/python_tests/images/composited/plus.png
new file mode 100644
index 0000000..6656c63
Binary files /dev/null and b/test/python_tests/images/composited/plus.png differ
diff --git a/test/python_tests/images/composited/saturation.png b/test/python_tests/images/composited/saturation.png
new file mode 100644
index 0000000..52e9d6c
Binary files /dev/null and b/test/python_tests/images/composited/saturation.png differ
diff --git a/test/python_tests/images/composited/screen.png b/test/python_tests/images/composited/screen.png
new file mode 100644
index 0000000..df69486
Binary files /dev/null and b/test/python_tests/images/composited/screen.png differ
diff --git a/test/python_tests/images/composited/soft_light.png b/test/python_tests/images/composited/soft_light.png
new file mode 100644
index 0000000..954bef3
Binary files /dev/null and b/test/python_tests/images/composited/soft_light.png differ
diff --git a/test/python_tests/images/composited/src.png b/test/python_tests/images/composited/src.png
new file mode 100644
index 0000000..70aa18f
Binary files /dev/null and b/test/python_tests/images/composited/src.png differ
diff --git a/test/python_tests/images/composited/src_atop.png b/test/python_tests/images/composited/src_atop.png
new file mode 100644
index 0000000..5621a09
Binary files /dev/null and b/test/python_tests/images/composited/src_atop.png differ
diff --git a/test/python_tests/images/composited/src_in.png b/test/python_tests/images/composited/src_in.png
new file mode 100644
index 0000000..c2dbc51
Binary files /dev/null and b/test/python_tests/images/composited/src_in.png differ
diff --git a/test/python_tests/images/composited/src_out.png b/test/python_tests/images/composited/src_out.png
new file mode 100644
index 0000000..4df0d0a
Binary files /dev/null and b/test/python_tests/images/composited/src_out.png differ
diff --git a/test/python_tests/images/composited/src_over.png b/test/python_tests/images/composited/src_over.png
new file mode 100644
index 0000000..fcba78a
Binary files /dev/null and b/test/python_tests/images/composited/src_over.png differ
diff --git a/test/python_tests/images/composited/value.png b/test/python_tests/images/composited/value.png
new file mode 100644
index 0000000..70bcf4e
Binary files /dev/null and b/test/python_tests/images/composited/value.png differ
diff --git a/test/python_tests/images/composited/xor.png b/test/python_tests/images/composited/xor.png
new file mode 100644
index 0000000..b6f2f2f
Binary files /dev/null and b/test/python_tests/images/composited/xor.png differ
diff --git a/test/python_tests/images/expected.png b/test/python_tests/images/expected.png
new file mode 100644
index 0000000..5a27b46
Binary files /dev/null and b/test/python_tests/images/expected.png differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected-reduced.png b/test/python_tests/images/pycairo/cairo-cairo-expected-reduced.png
new file mode 100644
index 0000000..b99dc91
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-cairo-expected-reduced.png differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected.pdf b/test/python_tests/images/pycairo/cairo-cairo-expected.pdf
new file mode 100644
index 0000000..220a9b2
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-cairo-expected.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected.png b/test/python_tests/images/pycairo/cairo-cairo-expected.png
new file mode 100644
index 0000000..3a99f5e
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-cairo-expected.png differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected.svg b/test/python_tests/images/pycairo/cairo-cairo-expected.svg
new file mode 100644
index 0000000..18d7343
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-cairo-expected.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 9.371094 -12.722656 L 9.464844 -12.722656 L 9.5625 -12.703125 L 9.273438 -12.703125 Z M 1.59375 -12.722656 L 1.6875 -12.722656 L 1.785156 -12.703125 L 1.496094 -12.703125 Z M 9.273438 -12.703125 L 9.5625 -12.703125 L 9.652344 -12.667969 L 9.183594 -12.667969 Z M 1.496094 -12.703125 L 1.785156 -12.703125 L 1.875 -12.667969 L 1.40625 -12.667969 Z M 9.183594 -12.667969 L 9.652344 -12.667969 L 9.734375 -12.617188 L 9.101562 -12.617188 Z M 1.40625 -12.667969 L  [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d="M 5.25 -8.832031 L 5.347656 -8.828125 L 4.941406 -8.828125 Z M 4.941406 -8.828125 L 5.347656 -8.828125 L 5.832031 -8.808594 L 4.726562 -8.808594 Z M 4.726562 -8.808594 L 5.832031 -8.808594 L 5.882812 -8.800781 L 4.644531 -8.800781 Z M 4.644531 -8.800781 L 5.882812 -8.800781 L 6.363281 -8.726562 L 4.191406 -8.726562 Z M 4.191406 -8.726562 L 6.363281 -8.726562 L 6.421875 -8.710938 L 4.097656 -8.710938 Z M 4.097656 -8.710938 L 6.421875 -8.710938 L 6.832031 -8.6 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 1.59375 -12.722656 L 1.6875 -12.722656 L 1.785156 -12.703125 L 1.496094 -12.703125 Z M 1.496094 -12.703125 L 1.785156 -12.703125 L 1.875 -12.667969 L 1.40625 -12.667969 Z M 1.40625 -12.667969 L 1.875 -12.667969 L 1.957031 -12.617188 L 1.324219 -12.617188 Z M 1.324219 -12.617188 L 1.957031 -12.617188 L 2.03125 -12.550781 L 1.25 -12.550781 Z M 1.25 -12.550781 L 2.03125 -12.550781 L 2.089844 -12.472656 L 1.191406 -12.472656 Z M 1.191406 -12.472656 L 2.089844  [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 5.25 -8.832031 L 5.546875 -8.828125 L 4.941406 -8.828125 Z M 4.941406 -8.828125 L 5.546875 -8.828125 L 5.855469 -8.800781 L 4.644531 -8.800781 Z M 4.644531 -8.800781 L 5.855469 -8.800781 L 6.390625 -8.710938 L 4.097656 -8.710938 Z M 4.097656 -8.710938 L 6.390625 -8.710938 L 6.878906 -8.570312 L 3.609375 -8.570312 Z M 3.609375 -8.570312 L 6.878906 -8.570312 L 7.117188 -8.476562 L 3.367188 -8.476562 Z M 3.367188 -8.476562 L 7.117188 -8.476562 L 7.140625 -8.4 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d="M 10.480469 -12.722656 L 10.574219 -12.722656 L 10.671875 -12.703125 L 10.382812 -12.703125 Z M 1.59375 -12.722656 L 1.6875 -12.722656 L 1.785156 -12.703125 L 1.496094 -12.703125 Z M 10.382812 -12.703125 L 10.671875 -12.703125 L 10.761719 -12.667969 L 10.292969 -12.667969 Z M 1.496094 -12.703125 L 1.785156 -12.703125 L 1.875 -12.667969 L 1.40625 -12.667969 Z M 10.292969 -12.667969 L 10.761719 -12.667969 L 10.84375 -12.617188 L 10.210938 -12.617188 Z M 1.4062 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 5.25 -8.832031 L 5.425781 -8.828125 L 4.941406 -8.828125 Z M 4.941406 -8.828125 L 5.425781 -8.828125 L 5.769531 -8.820312 L 4.855469 -8.820312 Z M 8.257812 -8.832031 L 8.351562 -8.832031 L 8.449219 -8.8125 L 8.160156 -8.8125 Z M 4.855469 -8.820312 L 5.769531 -8.820312 L 5.941406 -8.800781 L 4.644531 -8.800781 Z M 8.160156 -8.8125 L 8.449219 -8.8125 L 8.539062 -8.777344 L 8.070312 -8.777344 Z M 4.644531 -8.800781 L 5.941406 -8.800781 L 6.28125 -8.761719 L 4 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 4.695312 -8.832031 L 4.992188 -8.828125 L 4.613281 -8.828125 Z M 1.59375 -8.832031 L 1.6875 -8.832031 L 1.785156 -8.8125 L 1.496094 -8.8125 Z M 4.613281 -8.828125 L 4.992188 -8.828125 L 5.25 -8.804688 L 4.140625 -8.804688 Z M 4.140625 -8.804688 L 5.25 -8.804688 L 5.289062 -8.800781 L 4.105469 -8.800781 Z M 1.496094 -8.8125 L 1.785156 -8.8125 L 1.875 -8.777344 L 1.40625 -8.777344 Z M 4.105469 -8.800781 L 5.289062 -8.800781 L 5.457031 -8.773438 L 3.878906 -8 [...]
+</symbol>
+</g>
+</defs>
+<g id="surface1">
+<rect x="0" y="0" width="512" height="512" style="fill:rgb(27.45098%,50.980392%,70.588235%);fill-opacity:1;stroke:none;"/>
+<path style="fill-rule:nonzero;fill:rgb(0%,0%,100%);fill-opacity:1;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 5 0 L 4.503906 2.167969 L 3.117188 3.910156 L 1.113281 4.875 L -1.113281 4.875 L -3.117188 3.910156 L -4.503906 2.167969 L -5 0 L -4.503906 -2.167969 L -3.117188 -3.910156 L -1.113281 -4.875 L 1.113281 -4.875 L 3.117188 -3.910156 L 4.503906 -2.167969 Z " transform="matrix(1,0,0,1,256,256)"/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-0" x="218.722656" y="24.5"/>
+  <use xlink:href="#glyph0-1" x="229.778212" y="24.5"/>
+  <use xlink:href="#glyph0-2" x="239.722656" y="24.5"/>
+  <use xlink:href="#glyph0-2" x="243.000434" y="24.5"/>
+  <use xlink:href="#glyph0-3" x="246.278212" y="24.5"/>
+  <use xlink:href="#glyph0-4" x="256.778212" y="24.5"/>
+  <use xlink:href="#glyph0-5" x="261.167101" y="24.5"/>
+  <use xlink:href="#glyph0-6" x="273.333767" y="24.5"/>
+  <use xlink:href="#glyph0-7" x="283.278212" y="24.5"/>
+</g>
+<path style="fill:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 0 0 L 512 0 L 512 512 L 0 512 Z M 6 6 L 506 6 L 506 506 L 6 506 Z "/>
+</g>
+</svg>
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.building.pdf b/test/python_tests/images/pycairo/cairo-surface-expected.building.pdf
new file mode 100644
index 0000000..11559bb
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-surface-expected.building.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.building.svg b/test/python_tests/images/pycairo/cairo-surface-expected.building.svg
new file mode 100644
index 0000000..78bc15e
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-surface-expected.building.svg
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256pt" height="256pt" viewBox="0 0 256 256" version="1.1">
+<g id="surface90">
+<rect x="0" y="0" width="256" height="256" style="fill:rgb(27.45098%,50.980392%,70.588235%);fill-opacity:1;stroke:none;"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.222656 27.054688 L 141.789062 23.054688 L 141.789062 20.75 L 132.222656 24.746094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.507812 28.515625 L 132.222656 27.054688 L 132.222656 24.746094 L 132.507812 26.207031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 141.789062 23.054688 L 145.058594 32.933594 L 145.058594 30.628906 L 141.789062 20.75 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 108.777344 39.203125 L 132.507812 28.515625 L 132.507812 26.207031 L 108.777344 36.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.984375 38.507812 L 122.578125 41.546875 L 122.578125 39.238281 L 127.984375 36.203125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.367188 41.585938 L 108.777344 39.203125 L 108.777344 36.894531 L 102.367188 39.277344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 131.019531 45.429688 L 127.984375 38.507812 L 127.984375 36.203125 L 131.019531 43.121094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 122.578125 41.546875 L 118.730469 49.234375 L 118.730469 46.929688 L 122.578125 39.238281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.6875 50.042969 L 102.367188 41.585938 L 102.367188 39.277344 L 105.6875 47.734375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 134.109375 51.578125 L 131.019531 45.429688 L 131.019531 43.121094 L 134.109375 49.273438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 118.730469 49.234375 L 115.65625 56.117188 L 115.65625 53.808594 L 118.730469 46.929688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 115.65625 56.117188 L 105.6875 50.042969 L 105.6875 47.734375 L 115.65625 53.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.332031 59.265625 L 134.109375 51.578125 L 134.109375 49.273438 L 136.332031 56.960938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.882812 66.1875 L 136.332031 59.265625 L 136.332031 56.960938 L 135.882812 63.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 145.058594 32.933594 L 157.566406 68.800781 L 157.566406 66.496094 L 145.058594 30.628906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.675781 71.570312 L 135.882812 66.1875 L 135.882812 63.878906 L 135.675781 69.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 157.566406 68.800781 L 159.464844 73.835938 L 159.464844 71.53125 L 157.566406 66.496094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 139.804688 81.023438 L 135.675781 71.570312 L 135.675781 69.261719 L 139.804688 78.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 159.464844 73.835938 L 139.804688 81.023438 L 139.804688 78.71875 L 159.464844 71.53125 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 115.65625 56.117188 L 105.6875 50.042969 L 102.367188 41.585938 L 108.777344 39.203125 L 132.507812 28.515625 L 132.222656 27.054688 L 141.789062 23.054688 L 145.058594 32.933594 L 157.566406 68.800781 L 159.464844 73.835938 L 139.804688 81.023438 L 135.675781 71.570312 L 135.882812 66.1875 L 136.332031 59.265625 L 134.109375 51.578125 L 13 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 115.65625 53.808594 L 105.6875 47.734375 L 102.367188 39.277344 L 108.777344 36.894531 L 132.507812 26.207031 L 132.222656 24.746094 L 141.789062 20.75 L 145.058594 30.628906 L 157.566406 66.496094 L 159.464844 71.53125 L 139.804688 78.71875 L 135.675781 69.261719 L 135.882812 63.878906 L 136.332031 56.960938 L 134.109375 49.273438 L 131.019531 43.121094 L 127.984375 36.203125 L 122.578125 39.23828 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.222656 27.054688 L 132.507812 28.515625 L 132.507812 26.207031 L 132.222656 24.746094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.507812 28.515625 L 108.777344 39.203125 L 108.777344 36.894531 L 132.507812 26.207031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.800781 39.277344 L 132.222656 27.054688 L 132.222656 24.746094 L 101.800781 36.972656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 108.777344 39.203125 L 102.367188 41.585938 L 102.367188 39.277344 L 108.777344 36.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.367188 41.585938 L 105.6875 50.042969 L 105.6875 47.734375 L 102.367188 39.277344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 64.152344 54.578125 L 101.800781 39.277344 L 101.800781 36.972656 L 64.152344 52.269531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.6875 50.042969 L 115.65625 56.117188 L 115.65625 53.808594 L 105.6875 47.734375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 53.773438 58.652344 L 64.152344 54.578125 L 64.152344 52.269531 L 53.773438 56.347656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 56.199219 60.996094 L 54.90625 61.496094 L 54.90625 59.191406 L 56.199219 58.691406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.90625 61.496094 L 53.773438 58.652344 L 53.773438 56.347656 L 54.90625 59.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 115.65625 56.117188 L 112.578125 61.574219 L 112.578125 59.265625 L 115.65625 53.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 62.101562 68.839844 L 56.199219 60.996094 L 56.199219 58.691406 L 62.101562 66.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 88.605469 74.414062 L 84.253906 74.566406 L 84.253906 72.261719 L 88.605469 72.105469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.039062 74.605469 L 88.605469 74.414062 L 88.605469 72.105469 L 105.039062 72.300781 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 112.578125 61.574219 L 105.039062 74.605469 L 105.039062 72.300781 L 112.578125 59.265625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 84.253906 74.566406 L 79.761719 74.644531 L 79.761719 72.335938 L 84.253906 72.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 79.761719 74.644531 L 68.664062 79.027344 L 68.664062 76.71875 L 79.761719 72.335938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 67.753906 79.296875 L 62.101562 68.839844 L 62.101562 66.53125 L 67.753906 76.988281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 68.664062 79.027344 L 67.753906 79.296875 L 67.753906 76.988281 L 68.664062 76.71875 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 132.222656 27.054688 L 132.507812 28.515625 L 108.777344 39.203125 L 102.367188 41.585938 L 105.6875 50.042969 L 115.65625 56.117188 L 112.578125 61.574219 L 105.039062 74.605469 L 88.605469 74.414062 L 84.253906 74.566406 L 79.761719 74.644531 L 68.664062 79.027344 L 67.753906 79.296875 L 62.101562 68.839844 L 56.199219 60.996094 L 54.9062 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 132.222656 24.746094 L 132.507812 26.207031 L 108.777344 36.894531 L 102.367188 39.277344 L 105.6875 47.734375 L 115.65625 53.808594 L 112.578125 59.265625 L 105.039062 72.300781 L 88.605469 72.105469 L 84.253906 72.261719 L 79.761719 72.335938 L 68.664062 76.71875 L 67.753906 76.988281 L 62.101562 66.53125 L 56.199219 58.691406 L 54.90625 59.191406 L 53.773438 56.347656 L 64.152344 52.269531 L 101 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 122.578125 41.546875 L 127.984375 38.507812 L 127.984375 36.203125 L 122.578125 39.238281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.984375 38.507812 L 131.019531 45.429688 L 131.019531 43.121094 L 127.984375 36.203125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 118.730469 49.234375 L 122.578125 41.546875 L 122.578125 39.238281 L 118.730469 46.929688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 131.019531 45.429688 L 134.109375 51.578125 L 134.109375 49.273438 L 131.019531 43.121094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 115.65625 56.117188 L 118.730469 49.234375 L 118.730469 46.929688 L 115.65625 53.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 134.109375 51.578125 L 136.332031 59.265625 L 136.332031 56.960938 L 134.109375 49.273438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 112.578125 61.574219 L 115.65625 56.117188 L 115.65625 53.808594 L 112.578125 59.265625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.332031 59.265625 L 135.882812 66.1875 L 135.882812 63.878906 L 136.332031 56.960938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.882812 66.1875 L 129.527344 66.6875 L 129.527344 64.378906 L 135.882812 63.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 129.527344 66.6875 L 124.503906 68.453125 L 124.503906 66.148438 L 129.527344 64.378906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 124.503906 68.453125 L 121.441406 69.992188 L 121.441406 67.6875 L 124.503906 66.148438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 84.253906 74.566406 L 88.605469 74.414062 L 88.605469 72.105469 L 84.253906 72.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.039062 74.605469 L 112.578125 61.574219 L 112.578125 59.265625 L 105.039062 72.300781 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 88.605469 74.414062 L 105.039062 74.605469 L 105.039062 72.300781 L 88.605469 72.105469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 79.761719 74.644531 L 84.253906 74.566406 L 84.253906 72.261719 L 79.761719 72.335938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 68.664062 79.027344 L 79.761719 74.644531 L 79.761719 72.335938 L 68.664062 76.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 67.753906 79.296875 L 68.664062 79.027344 L 68.664062 76.71875 L 67.753906 76.988281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 121.441406 69.992188 L 109.082031 80.371094 L 109.082031 78.066406 L 121.441406 67.6875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.082031 80.371094 L 104.925781 81.371094 L 104.925781 79.066406 L 109.082031 78.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.679688 81.753906 L 67.753906 79.296875 L 67.753906 76.988281 L 61.679688 79.449219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 104.925781 81.371094 L 102.195312 83.253906 L 102.195312 80.949219 L 104.925781 79.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.195312 83.253906 L 101.664062 85.136719 L 101.664062 82.832031 L 102.195312 80.949219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.664062 85.136719 L 100.078125 88.445312 L 100.078125 86.136719 L 101.664062 82.832031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 100.078125 88.445312 L 97.773438 91.671875 L 97.773438 89.367188 L 100.078125 86.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 97.773438 91.671875 L 94.757812 93.558594 L 94.757812 91.25 L 97.773438 89.367188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.757812 93.558594 L 91.441406 100.015625 L 91.441406 97.707031 L 94.757812 91.25 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 91.441406 100.015625 L 71.058594 108.050781 L 71.058594 105.742188 L 91.441406 97.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 71.058594 108.050781 L 61.679688 81.753906 L 61.679688 79.449219 L 71.058594 105.742188 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 115.65625 56.117188 L 118.730469 49.234375 L 122.578125 41.546875 L 127.984375 38.507812 L 131.019531 45.429688 L 134.109375 51.578125 L 136.332031 59.265625 L 135.882812 66.1875 L 129.527344 66.6875 L 124.503906 68.453125 L 121.441406 69.992188 L 109.082031 80.371094 L 104.925781 81.371094 L 102.195312 83.253906 L 101.664062 85.136719 L 10 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 115.65625 53.808594 L 118.730469 46.929688 L 122.578125 39.238281 L 127.984375 36.203125 L 131.019531 43.121094 L 134.109375 49.273438 L 136.332031 56.960938 L 135.882812 63.878906 L 129.527344 64.378906 L 124.503906 66.148438 L 121.441406 67.6875 L 109.082031 78.066406 L 104.925781 79.066406 L 102.195312 80.949219 L 101.664062 82.832031 L 100.078125 86.136719 L 97.773438 89.367188 L 94.757812 91.2 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 53.773438 58.652344 L 54.90625 61.496094 L 54.90625 59.191406 L 53.773438 56.347656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 44.109375 62.304688 L 53.773438 58.652344 L 53.773438 56.347656 L 44.109375 59.996094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 0 79.527344 L 44.109375 62.304688 L 44.109375 59.996094 L 0 77.21875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.90625 61.496094 L 61.679688 81.753906 L 61.679688 79.449219 L 54.90625 59.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 23.488281 86.253906 L 21.667969 87.945312 L 21.667969 85.636719 L 23.488281 83.945312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 28.25 90.710938 L 23.488281 86.253906 L 23.488281 83.945312 L 28.25 88.40625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 21.667969 87.945312 L 16.679688 93.402344 L 16.679688 91.097656 L 21.667969 85.636719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 7.113281 96.59375 L 0 79.527344 L 0 77.21875 L 7.113281 94.289062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 16.679688 93.402344 L 11.117188 99.59375 L 11.117188 97.285156 L 16.679688 91.097656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 11.117188 99.59375 L 7.113281 96.59375 L 7.113281 94.289062 L 11.117188 97.285156 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 14.351562 105.242188 L 28.25 90.710938 L 28.25 88.40625 L 14.351562 102.9375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 10.238281 106.011719 L 14.351562 105.242188 L 14.351562 102.9375 L 10.238281 103.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.679688 81.753906 L 71.058594 108.050781 L 71.058594 105.742188 L 61.679688 79.449219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 10.171875 111.96875 L 10.238281 106.011719 L 10.238281 103.707031 L 10.171875 109.664062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 9.871094 116.082031 L 10.171875 111.96875 L 10.171875 109.664062 L 9.871094 113.777344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.027344 114.699219 L 44.699219 118.351562 L 44.699219 116.042969 L 54.027344 112.394531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 71.058594 108.050781 L 76.140625 119.121094 L 76.140625 116.8125 L 71.058594 105.742188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 11.242188 120.925781 L 9.871094 116.082031 L 9.871094 113.777344 L 11.242188 118.621094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 76.140625 119.121094 L 72.308594 122.695312 L 72.308594 120.390625 L 76.140625 116.8125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 44.699219 118.351562 L 33.3125 123.15625 L 33.3125 120.851562 L 44.699219 116.042969 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 14.414062 130.113281 L 11.242188 120.925781 L 11.242188 118.621094 L 14.414062 127.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 33.3125 123.15625 L 14.414062 130.113281 L 14.414062 127.808594 L 33.3125 120.851562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 72.308594 122.695312 L 69.605469 130.535156 L 69.605469 128.230469 L 72.308594 120.390625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.425781 133.496094 L 54.027344 114.699219 L 54.027344 112.394531 L 61.425781 131.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 69.605469 130.535156 L 61.425781 133.496094 L 61.425781 131.191406 L 69.605469 128.230469 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 53.773438 58.652344 L 54.90625 61.496094 L 61.679688 81.753906 L 71.058594 108.050781 L 76.140625 119.121094 L 72.308594 122.695312 L 69.605469 130.535156 L 61.425781 133.496094 L 54.027344 114.699219 L 44.699219 118.351562 L 33.3125 123.15625 L 14.414062 130.113281 L 11.242188 120.925781 L 9.871094 116.082031 L 10.171875 111.96875 L 10.238 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 53.773438 56.347656 L 54.90625 59.191406 L 61.679688 79.449219 L 71.058594 105.742188 L 76.140625 116.8125 L 72.308594 120.390625 L 69.605469 128.230469 L 61.425781 131.191406 L 54.027344 112.394531 L 44.699219 116.042969 L 33.3125 120.851562 L 14.414062 127.808594 L 11.242188 118.621094 L 9.871094 113.777344 L 10.171875 109.664062 L 10.238281 103.707031 L 14.351562 102.9375 L 28.25 88.40625 L 23.4 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.90625 61.496094 L 56.199219 60.996094 L 56.199219 58.691406 L 54.90625 59.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 56.199219 60.996094 L 62.101562 68.839844 L 62.101562 66.53125 L 56.199219 58.691406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 62.101562 68.839844 L 67.753906 79.296875 L 67.753906 76.988281 L 62.101562 66.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 67.753906 79.296875 L 61.679688 81.753906 L 61.679688 79.449219 L 67.753906 76.988281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.679688 81.753906 L 54.90625 61.496094 L 54.90625 59.191406 L 61.679688 79.449219 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 54.90625 61.496094 L 56.199219 60.996094 L 62.101562 68.839844 L 67.753906 79.296875 L 61.679688 81.753906 Z M 54.90625 61.496094 L 54.90625 59.191406 M 56.199219 60.996094 L 56.199219 58.691406 M 62.101562 68.839844 L 62.101562 66.53125 M 67.753906 79.296875 L 67.753906 76.988281 M 61.679688 81.753906 L 61.679688 79.449219 M 54.90625 59.19 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 54.90625 59.191406 L 56.199219 58.691406 L 62.101562 66.53125 L 67.753906 76.988281 L 61.679688 79.449219 Z "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 129.527344 66.6875 L 135.882812 66.1875 L 135.882812 63.878906 L 129.527344 64.378906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 124.503906 68.453125 L 129.527344 66.6875 L 129.527344 64.378906 L 124.503906 66.148438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 121.441406 69.992188 L 124.503906 68.453125 L 124.503906 66.148438 L 121.441406 67.6875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.882812 66.1875 L 135.675781 71.570312 L 135.675781 69.261719 L 135.882812 63.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.082031 80.371094 L 121.441406 69.992188 L 121.441406 67.6875 L 109.082031 78.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.675781 71.570312 L 139.804688 81.023438 L 139.804688 78.71875 L 135.675781 69.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 104.925781 81.371094 L 109.082031 80.371094 L 109.082031 78.066406 L 104.925781 79.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.195312 83.253906 L 104.925781 81.371094 L 104.925781 79.066406 L 102.195312 80.949219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.664062 85.136719 L 102.195312 83.253906 L 102.195312 80.949219 L 101.664062 82.832031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 139.804688 81.023438 L 127.082031 86.292969 L 127.082031 83.984375 L 139.804688 78.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 100.078125 88.445312 L 101.664062 85.136719 L 101.664062 82.832031 L 100.078125 86.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.199219 89.058594 L 101.835938 91.441406 L 101.835938 89.136719 L 109.199219 86.753906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 97.773438 91.671875 L 100.078125 88.445312 L 100.078125 86.136719 L 97.773438 89.367188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 110.335938 92.902344 L 109.199219 89.058594 L 109.199219 86.753906 L 110.335938 90.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.082031 86.292969 L 110.335938 92.902344 L 110.335938 90.597656 L 127.082031 83.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.757812 93.558594 L 97.773438 91.671875 L 97.773438 89.367188 L 94.757812 91.25 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.835938 91.441406 L 103.257812 95.363281 L 103.257812 93.058594 L 101.835938 89.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 91.441406 100.015625 L 94.757812 93.558594 L 94.757812 91.25 L 91.441406 97.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 103.257812 95.363281 L 91.441406 100.015625 L 91.441406 97.707031 L 103.257812 93.058594 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 135.882812 66.1875 L 135.675781 71.570312 L 139.804688 81.023438 L 127.082031 86.292969 L 110.335938 92.902344 L 109.199219 89.058594 L 101.835938 91.441406 L 103.257812 95.363281 L 91.441406 100.015625 L 94.757812 93.558594 L 97.773438 91.671875 L 100.078125 88.445312 L 101.664062 85.136719 L 102.195312 83.253906 L 104.925781 81.371094 L 1 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 135.882812 63.878906 L 135.675781 69.261719 L 139.804688 78.71875 L 127.082031 83.984375 L 110.335938 90.597656 L 109.199219 86.753906 L 101.835938 89.136719 L 103.257812 93.058594 L 91.441406 97.707031 L 94.757812 91.25 L 97.773438 89.367188 L 100.078125 86.136719 L 101.664062 82.832031 L 102.195312 80.949219 L 104.925781 79.066406 L 109.082031 78.066406 L 121.441406 67.6875 L 124.503906 66.148438 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 159.464844 73.835938 L 170.804688 68.917969 L 170.804688 66.609375 L 159.464844 71.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 170.804688 68.917969 L 173.171875 76.296875 L 173.171875 73.992188 L 170.804688 66.609375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 139.804688 81.023438 L 159.464844 73.835938 L 159.464844 71.53125 L 139.804688 78.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 173.171875 76.296875 L 176.019531 82.679688 L 176.019531 80.371094 L 173.171875 73.992188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.082031 86.292969 L 139.804688 81.023438 L 139.804688 78.71875 L 127.082031 83.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 149.34375 87.90625 L 143.125 89.828125 L 143.125 87.523438 L 149.34375 85.601562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.757812 89.945312 L 171.078125 89.90625 L 171.078125 87.597656 L 174.757812 87.636719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 176.019531 82.679688 L 183.402344 90.136719 L 183.402344 87.828125 L 176.019531 80.371094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 128.355469 90.597656 L 127.082031 86.292969 L 127.082031 83.984375 L 128.355469 88.289062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 143.125 89.828125 L 136.710938 92.828125 L 136.710938 90.519531 L 143.125 87.523438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 171.078125 89.90625 L 169.109375 93.441406 L 169.109375 91.136719 L 171.078125 87.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 183.402344 90.136719 L 184.109375 93.789062 L 184.109375 91.480469 L 183.402344 87.828125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 184.109375 93.789062 L 174.757812 89.945312 L 174.757812 87.636719 L 184.109375 91.480469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.710938 92.828125 L 130.351562 95.019531 L 130.351562 92.710938 L 136.710938 90.519531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 130.351562 95.019531 L 128.355469 90.597656 L 128.355469 88.289062 L 130.351562 92.710938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 169.109375 93.441406 L 185.757812 103.707031 L 185.757812 101.398438 L 169.109375 91.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.929688 105.933594 L 149.34375 87.90625 L 149.34375 85.601562 L 155.929688 103.628906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.917969 110.203125 L 155.929688 105.933594 L 155.929688 103.628906 L 155.917969 107.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 185.757812 103.707031 L 182.722656 112.007812 L 182.722656 109.703125 L 185.757812 101.398438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 182.722656 112.007812 L 181.617188 113.125 L 181.617188 110.816406 L 182.722656 109.703125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 181.617188 113.125 L 179.890625 116.3125 L 179.890625 114.007812 L 181.617188 110.816406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 153.800781 116.507812 L 155.917969 110.203125 L 155.917969 107.894531 L 153.800781 114.199219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 179.890625 116.3125 L 174.433594 119.734375 L 174.433594 117.429688 L 179.890625 114.007812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 151.734375 120.082031 L 153.800781 116.507812 L 153.800781 114.199219 L 151.734375 117.773438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.433594 119.734375 L 167.976562 121.773438 L 167.976562 119.464844 L 174.433594 117.429688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 147.519531 123.503906 L 151.734375 120.082031 L 151.734375 117.773438 L 147.519531 121.195312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 167.976562 121.773438 L 157.070312 125.578125 L 157.070312 123.273438 L 167.976562 119.464844 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 141.585938 126.386719 L 147.519531 123.503906 L 147.519531 121.195312 L 141.585938 124.078125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.289062 132.804688 L 141.585938 126.386719 L 141.585938 124.078125 L 144.289062 130.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 157.070312 125.578125 L 144.289062 132.804688 L 144.289062 130.5 L 157.070312 123.273438 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 159.464844 73.835938 L 170.804688 68.917969 L 173.171875 76.296875 L 176.019531 82.679688 L 183.402344 90.136719 L 184.109375 93.789062 L 174.757812 89.945312 L 171.078125 89.90625 L 169.109375 93.441406 L 185.757812 103.707031 L 182.722656 112.007812 L 181.617188 113.125 L 179.890625 116.3125 L 174.433594 119.734375 L 167.976562 121.773438 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 159.464844 71.53125 L 170.804688 66.609375 L 173.171875 73.992188 L 176.019531 80.371094 L 183.402344 87.828125 L 184.109375 91.480469 L 174.757812 87.636719 L 171.078125 87.597656 L 169.109375 91.136719 L 185.757812 101.398438 L 182.722656 109.703125 L 181.617188 110.816406 L 179.890625 114.007812 L 174.433594 117.429688 L 167.976562 119.464844 L 157.070312 123.273438 L 144.289062 130.5 L 141.5859 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 247.820312 71.339844 L 250.59375 70.03125 L 250.59375 67.722656 L 247.820312 69.03125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 245.570312 72.53125 L 247.820312 71.339844 L 247.820312 69.03125 L 245.570312 70.222656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 241.550781 74.835938 L 245.570312 72.53125 L 245.570312 70.222656 L 241.550781 72.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 213.167969 74.914062 L 216.90625 70.414062 L 216.90625 68.109375 L 213.167969 72.605469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 250.59375 70.03125 L 252.679688 76.488281 L 252.679688 74.183594 L 250.59375 67.722656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 216.90625 70.414062 L 228.015625 79.488281 L 228.015625 77.179688 L 216.90625 68.109375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 205.175781 79.601562 L 213.167969 74.914062 L 213.167969 72.605469 L 205.175781 77.296875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 242.207031 79.796875 L 241.550781 74.835938 L 241.550781 72.53125 L 242.207031 77.488281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 201.0625 81.371094 L 205.175781 79.601562 L 205.175781 77.296875 L 201.0625 79.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 200.394531 81.488281 L 201.0625 81.371094 L 201.0625 79.066406 L 200.394531 79.179688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 252.679688 76.488281 L 254.8125 83.101562 L 254.8125 80.792969 L 252.679688 74.183594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 192.929688 83.832031 L 200.394531 81.488281 L 200.394531 79.179688 L 192.929688 81.523438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 228.765625 84.601562 L 242.207031 79.796875 L 242.207031 77.488281 L 228.765625 82.292969 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 228.015625 79.488281 L 228.765625 84.601562 L 228.765625 82.292969 L 228.015625 77.179688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 254.8125 83.101562 L 256 85.5625 L 256 83.253906 L 254.8125 80.792969 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 256 85.5625 L 255.28125 85.714844 L 255.28125 83.410156 L 256 83.253906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 188.046875 87.058594 L 192.929688 83.832031 L 192.929688 81.523438 L 188.046875 84.753906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 171.078125 89.90625 L 174.757812 89.945312 L 174.757812 87.636719 L 171.078125 87.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 183.402344 90.136719 L 188.046875 87.058594 L 188.046875 84.753906 L 183.402344 87.828125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 169.109375 93.441406 L 171.078125 89.90625 L 171.078125 87.597656 L 169.109375 91.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 184.109375 93.789062 L 183.402344 90.136719 L 183.402344 87.828125 L 184.109375 91.480469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.757812 89.945312 L 184.109375 93.789062 L 184.109375 91.480469 L 174.757812 87.636719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 255.28125 85.714844 L 224.476562 96.902344 L 224.476562 94.59375 L 255.28125 83.410156 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 224.476562 96.902344 L 222.519531 98.167969 L 222.519531 95.863281 L 224.476562 94.59375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 222.519531 98.167969 L 221.703125 98.9375 L 221.703125 96.632812 L 222.519531 95.863281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 221.703125 98.9375 L 220.648438 100.207031 L 220.648438 97.902344 L 221.703125 96.632812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 220.648438 100.207031 L 218.683594 102.28125 L 218.683594 99.976562 L 220.648438 97.902344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 185.757812 103.707031 L 169.109375 93.441406 L 169.109375 91.136719 L 185.757812 101.398438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 218.683594 102.28125 L 217.1875 107.625 L 217.1875 105.320312 L 218.683594 99.976562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 217.1875 107.625 L 217.277344 109.933594 L 217.277344 107.625 L 217.1875 105.320312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 182.722656 112.007812 L 185.757812 103.707031 L 185.757812 101.398438 L 182.722656 109.703125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 181.617188 113.125 L 182.722656 112.007812 L 182.722656 109.703125 L 181.617188 110.816406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 179.890625 116.3125 L 181.617188 113.125 L 181.617188 110.816406 L 179.890625 114.007812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.433594 119.734375 L 179.890625 116.3125 L 179.890625 114.007812 L 174.433594 117.429688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 167.976562 121.773438 L 174.433594 119.734375 L 174.433594 117.429688 L 167.976562 119.464844 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 157.070312 125.578125 L 167.976562 121.773438 L 167.976562 119.464844 L 157.070312 123.273438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 138.746094 132.382812 L 144.289062 132.804688 L 144.289062 130.5 L 138.746094 130.074219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.289062 132.804688 L 157.070312 125.578125 L 157.070312 123.273438 L 144.289062 130.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 217.277344 109.933594 L 221.75 133.035156 L 221.75 130.730469 L 217.277344 107.625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 221.75 133.035156 L 217.414062 146.066406 L 217.414062 143.761719 L 221.75 130.730469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 217.414062 146.066406 L 216.40625 148.449219 L 216.40625 146.144531 L 217.414062 143.761719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.914062 153.0625 L 138.746094 132.382812 L 138.746094 130.074219 L 94.914062 150.757812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 216.40625 148.449219 L 209.457031 155.371094 L 209.457031 153.0625 L 216.40625 146.144531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 90.503906 158.40625 L 94.914062 153.0625 L 94.914062 150.757812 L 90.503906 156.101562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 209.457031 155.371094 L 197.367188 172.246094 L 197.367188 169.9375 L 209.457031 153.0625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 197.367188 172.246094 L 193.015625 173.746094 L 193.015625 171.4375 L 197.367188 169.9375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 193.015625 173.746094 L 186.796875 175.4375 L 186.796875 173.128906 L 193.015625 171.4375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 186.796875 175.4375 L 179.820312 175.511719 L 179.820312 173.207031 L 186.796875 173.128906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 179.820312 175.511719 L 175.621094 173.015625 L 175.621094 170.707031 L 179.820312 173.207031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 175.621094 173.015625 L 151.179688 179.433594 L 151.179688 177.128906 L 175.621094 170.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 151.179688 179.433594 L 146.535156 182.738281 L 146.535156 180.433594 L 151.179688 177.128906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 146.535156 182.738281 L 144.859375 184.625 L 144.859375 182.316406 L 146.535156 180.433594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.859375 184.625 L 144.503906 190.695312 L 144.503906 188.390625 L 144.859375 182.316406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.503906 190.695312 L 150.546875 200.269531 L 150.546875 197.960938 L 144.503906 188.390625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 108.660156 207.035156 L 90.503906 158.40625 L 90.503906 156.101562 L 108.660156 204.726562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 150.546875 200.269531 L 153.316406 218.875 L 153.316406 216.566406 L 150.546875 197.960938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 153.316406 218.875 L 145.023438 226.640625 L 145.023438 224.332031 L 153.316406 216.566406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 117.859375 231.675781 L 108.660156 207.035156 L 108.660156 204.726562 L 117.859375 229.367188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 145.023438 226.640625 L 118.382812 232.945312 L 118.382812 230.636719 L 145.023438 224.332031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 118.382812 232.945312 L 117.859375 231.675781 L 117.859375 229.367188 L 118.382812 230.636719 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 183.402344 90.136719 L 188.046875 87.058594 L 192.929688 83.832031 L 200.394531 81.488281 L 201.0625 81.371094 L 205.175781 79.601562 L 213.167969 74.914062 L 216.90625 70.414062 L 228.015625 79.488281 L 228.765625 84.601562 L 242.207031 79.796875 L 241.550781 74.835938 L 245.570312 72.53125 L 247.820312 71.339844 L 250.59375 70.03125 L 252 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 183.402344 87.828125 L 188.046875 84.753906 L 192.929688 81.523438 L 200.394531 79.179688 L 201.0625 79.066406 L 205.175781 77.296875 L 213.167969 72.605469 L 216.90625 68.109375 L 228.015625 77.179688 L 228.765625 82.292969 L 242.207031 77.488281 L 241.550781 72.53125 L 245.570312 70.222656 L 247.820312 69.03125 L 250.59375 67.722656 L 252.679688 74.183594 L 254.8125 80.792969 L 256 83.253906 L 25 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 143.125 89.828125 L 149.34375 87.90625 L 149.34375 85.601562 L 143.125 87.523438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.082031 86.292969 L 128.355469 90.597656 L 128.355469 88.289062 L 127.082031 83.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.710938 92.828125 L 143.125 89.828125 L 143.125 87.523438 L 136.710938 90.519531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 110.335938 92.902344 L 127.082031 86.292969 L 127.082031 83.984375 L 110.335938 90.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 128.355469 90.597656 L 130.351562 95.019531 L 130.351562 92.710938 L 128.355469 88.289062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 130.351562 95.019531 L 136.710938 92.828125 L 136.710938 90.519531 L 130.351562 92.710938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 103.257812 95.363281 L 110.335938 92.902344 L 110.335938 90.597656 L 103.257812 93.058594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 91.441406 100.015625 L 103.257812 95.363281 L 103.257812 93.058594 L 91.441406 97.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 149.34375 87.90625 L 155.929688 105.933594 L 155.929688 103.628906 L 149.34375 85.601562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 71.058594 108.050781 L 91.441406 100.015625 L 91.441406 97.707031 L 71.058594 105.742188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.929688 105.933594 L 155.917969 110.203125 L 155.917969 107.894531 L 155.929688 103.628906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.917969 110.203125 L 153.800781 116.507812 L 153.800781 114.199219 L 155.917969 107.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 76.140625 119.121094 L 71.058594 108.050781 L 71.058594 105.742188 L 76.140625 116.8125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 153.800781 116.507812 L 151.734375 120.082031 L 151.734375 117.773438 L 153.800781 114.199219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 151.734375 120.082031 L 147.519531 123.503906 L 147.519531 121.195312 L 151.734375 117.773438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 147.519531 123.503906 L 141.585938 126.386719 L 141.585938 124.078125 L 147.519531 121.195312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 79.503906 129.191406 L 76.140625 119.121094 L 76.140625 116.8125 L 79.503906 126.886719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.289062 132.804688 L 138.746094 132.382812 L 138.746094 130.074219 L 144.289062 130.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 141.585938 126.386719 L 144.289062 132.804688 L 144.289062 130.5 L 141.585938 124.078125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 83.292969 139.1875 L 79.503906 129.191406 L 79.503906 126.886719 L 83.292969 136.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 86.144531 145.105469 L 83.292969 139.1875 L 83.292969 136.878906 L 86.144531 142.800781 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 86.664062 147.644531 L 86.144531 145.105469 L 86.144531 142.800781 L 86.664062 145.335938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 138.746094 132.382812 L 94.914062 153.0625 L 94.914062 150.757812 L 138.746094 130.074219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 90.121094 157.292969 L 86.664062 147.644531 L 86.664062 145.335938 L 90.121094 154.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.914062 153.0625 L 90.503906 158.40625 L 90.503906 156.101562 L 94.914062 150.757812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 90.503906 158.40625 L 90.121094 157.292969 L 90.121094 154.984375 L 90.503906 156.101562 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 110.335938 92.902344 L 127.082031 86.292969 L 128.355469 90.597656 L 130.351562 95.019531 L 136.710938 92.828125 L 143.125 89.828125 L 149.34375 87.90625 L 155.929688 105.933594 L 155.917969 110.203125 L 153.800781 116.507812 L 151.734375 120.082031 L 147.519531 123.503906 L 141.585938 126.386719 L 144.289062 132.804688 L 138.746094 132.382 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 110.335938 90.597656 L 127.082031 83.984375 L 128.355469 88.289062 L 130.351562 92.710938 L 136.710938 90.519531 L 143.125 87.523438 L 149.34375 85.601562 L 155.929688 103.628906 L 155.917969 107.894531 L 153.800781 114.199219 L 151.734375 117.773438 L 147.519531 121.195312 L 141.585938 124.078125 L 144.289062 130.5 L 138.746094 130.074219 L 94.914062 150.757812 L 90.503906 156.101562 L 90.121094 1 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.835938 91.441406 L 109.199219 89.058594 L 109.199219 86.753906 L 101.835938 89.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.199219 89.058594 L 110.335938 92.902344 L 110.335938 90.597656 L 109.199219 86.753906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 110.335938 92.902344 L 103.257812 95.363281 L 103.257812 93.058594 L 110.335938 90.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 103.257812 95.363281 L 101.835938 91.441406 L 101.835938 89.136719 L 103.257812 93.058594 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 110.335938 92.902344 L 103.257812 95.363281 L 101.835938 91.441406 L 109.199219 89.058594 Z M 101.835938 91.441406 L 101.835938 89.136719 M 109.199219 89.058594 L 109.199219 86.753906 M 110.335938 92.902344 L 110.335938 90.597656 M 103.257812 95.363281 L 103.257812 93.058594 M 110.335938 90.597656 L 103.257812 93.058594 L 101.835938 89.1367 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 110.335938 90.597656 L 103.257812 93.058594 L 101.835938 89.136719 L 109.199219 86.753906 Z "/>
+</g>
+</svg>
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.point.pdf b/test/python_tests/images/pycairo/cairo-surface-expected.point.pdf
new file mode 100644
index 0000000..eff8ca6
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-surface-expected.point.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.point.svg b/test/python_tests/images/pycairo/cairo-surface-expected.point.svg
new file mode 100644
index 0000000..0b73c8c
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-surface-expected.point.svg
@@ -0,0 +1,413 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256pt" height="256pt" viewBox="0 0 256 256" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 0.5 1.765625 L 0.5 -7.046875 L 5.5 -7.046875 L 5.5 1.765625 Z M 1.0625 1.21875 L 4.9375 1.21875 L 4.9375 -6.484375 L 1.0625 -6.484375 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d="M 3.421875 -2.75 C 2.703125 -2.75 2.203125 -2.664062 1.921875 -2.5 C 1.640625 -2.332031 1.5 -2.050781 1.5 -1.65625 C 1.5 -1.332031 1.601562 -1.078125 1.8125 -0.890625 C 2.019531 -0.703125 2.304688 -0.609375 2.671875 -0.609375 C 3.171875 -0.609375 3.570312 -0.785156 3.875 -1.140625 C 4.175781 -1.492188 4.328125 -1.960938 4.328125 -2.546875 L 4.328125 -2.75 Z M 5.21875 -3.125 L 5.21875 0 L 4.328125 0 L 4.328125 -0.828125 C 4.117188 -0.492188 3.859375 -0.25 3.5 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 1.828125 -7.015625 L 1.828125 -5.46875 L 3.6875 -5.46875 L 3.6875 -4.765625 L 1.828125 -4.765625 L 1.828125 -1.796875 C 1.828125 -1.359375 1.890625 -1.070312 2.015625 -0.9375 C 2.140625 -0.8125 2.390625 -0.75 2.765625 -0.75 L 3.6875 -0.75 L 3.6875 0 L 2.765625 0 C 2.066406 0 1.582031 -0.128906 1.3125 -0.390625 C 1.050781 -0.648438 0.921875 -1.117188 0.921875 -1.796875 L 0.921875 -4.765625 L 0.265625 -4.765625 L 0.265625 -5.46875 L 0.921875 -5.46875 L 0.921 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 5.625 -2.953125 L 5.625 -2.515625 L 1.484375 -2.515625 C 1.523438 -1.898438 1.710938 -1.429688 2.046875 -1.109375 C 2.378906 -0.785156 2.84375 -0.625 3.4375 -0.625 C 3.78125 -0.625 4.113281 -0.664062 4.4375 -0.75 C 4.769531 -0.832031 5.09375 -0.957031 5.40625 -1.125 L 5.40625 -0.28125 C 5.082031 -0.144531 4.75 -0.0390625 4.40625 0.03125 C 4.070312 0.101562 3.734375 0.140625 3.390625 0.140625 C 2.515625 0.140625 1.820312 -0.109375 1.3125 -0.609375 C 0.80078 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d="M 4.421875 -5.3125 L 4.421875 -4.453125 C 4.171875 -4.585938 3.910156 -4.6875 3.640625 -4.75 C 3.367188 -4.8125 3.082031 -4.84375 2.78125 -4.84375 C 2.34375 -4.84375 2.007812 -4.773438 1.78125 -4.640625 C 1.5625 -4.503906 1.453125 -4.300781 1.453125 -4.03125 C 1.453125 -3.820312 1.53125 -3.65625 1.6875 -3.53125 C 1.84375 -3.414062 2.164062 -3.304688 2.65625 -3.203125 L 2.953125 -3.125 C 3.597656 -2.988281 4.050781 -2.796875 4.3125 -2.546875 C 4.582031 -2.296 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 3.09375 -7.59375 C 2.664062 -6.84375 2.34375 -6.097656 2.125 -5.359375 C 1.914062 -4.628906 1.8125 -3.890625 1.8125 -3.140625 C 1.8125 -2.390625 1.914062 -1.644531 2.125 -0.90625 C 2.34375 -0.164062 2.664062 0.570312 3.09375 1.3125 L 2.3125 1.3125 C 1.832031 0.550781 1.46875 -0.195312 1.21875 -0.9375 C 0.976562 -1.675781 0.859375 -2.410156 0.859375 -3.140625 C 0.859375 -3.867188 0.976562 -4.597656 1.21875 -5.328125 C 1.457031 -6.066406 1.820312 -6.820312 2 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 0.9375 -5.46875 L 1.84375 -5.46875 L 1.84375 0.09375 C 1.84375 0.789062 1.707031 1.296875 1.4375 1.609375 C 1.175781 1.921875 0.75 2.078125 0.15625 2.078125 L -0.1875 2.078125 L -0.1875 1.3125 L 0.0625 1.3125 C 0.40625 1.3125 0.632812 1.234375 0.75 1.078125 C 0.875 0.921875 0.9375 0.59375 0.9375 0.09375 Z M 0.9375 -7.59375 L 1.84375 -7.59375 L 1.84375 -6.453125 L 0.9375 -6.453125 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-8">
+<path style="stroke:none;" d="M 1.8125 -0.828125 L 1.8125 2.078125 L 0.90625 2.078125 L 0.90625 -5.46875 L 1.8125 -5.46875 L 1.8125 -4.640625 C 2 -4.960938 2.234375 -5.203125 2.515625 -5.359375 C 2.804688 -5.515625 3.15625 -5.59375 3.5625 -5.59375 C 4.226562 -5.59375 4.765625 -5.328125 5.171875 -4.796875 C 5.585938 -4.273438 5.796875 -3.585938 5.796875 -2.734375 C 5.796875 -1.867188 5.585938 -1.171875 5.171875 -0.640625 C 4.765625 -0.117188 4.226562 0.140625 3.5625 0.140625 C 3.15625 0.1 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-9">
+<path style="stroke:none;" d="M 4.546875 -2.796875 C 4.546875 -3.453125 4.410156 -3.957031 4.140625 -4.3125 C 3.867188 -4.664062 3.492188 -4.84375 3.015625 -4.84375 C 2.523438 -4.84375 2.144531 -4.664062 1.875 -4.3125 C 1.613281 -3.957031 1.484375 -3.453125 1.484375 -2.796875 C 1.484375 -2.148438 1.613281 -1.644531 1.875 -1.28125 C 2.144531 -0.925781 2.523438 -0.75 3.015625 -0.75 C 3.492188 -0.75 3.867188 -0.925781 4.140625 -1.28125 C 4.410156 -1.644531 4.546875 -2.148438 4.546875 -2.796 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-10">
+<path style="stroke:none;" d="M 0.796875 -7.59375 L 1.578125 -7.59375 C 2.066406 -6.820312 2.429688 -6.066406 2.671875 -5.328125 C 2.921875 -4.597656 3.046875 -3.867188 3.046875 -3.140625 C 3.046875 -2.410156 2.921875 -1.675781 2.671875 -0.9375 C 2.429688 -0.195312 2.066406 0.550781 1.578125 1.3125 L 0.796875 1.3125 C 1.234375 0.570312 1.554688 -0.164062 1.765625 -0.90625 C 1.984375 -1.644531 2.09375 -2.390625 2.09375 -3.140625 C 2.09375 -3.890625 1.984375 -4.628906 1.765625 -5.359375 C  [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-11">
+<path style="stroke:none;" d="M 3.421875 -6.3125 L 2.078125 -2.6875 L 4.765625 -2.6875 Z M 2.859375 -7.296875 L 3.984375 -7.296875 L 6.765625 0 L 5.734375 0 L 5.0625 -1.875 L 1.78125 -1.875 L 1.125 0 L 0.078125 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-12">
+<path style="stroke:none;" d="M 0.84375 -2.15625 L 0.84375 -5.46875 L 1.75 -5.46875 L 1.75 -2.1875 C 1.75 -1.675781 1.847656 -1.289062 2.046875 -1.03125 C 2.253906 -0.769531 2.554688 -0.640625 2.953125 -0.640625 C 3.441406 -0.640625 3.828125 -0.789062 4.109375 -1.09375 C 4.390625 -1.40625 4.53125 -1.832031 4.53125 -2.375 L 4.53125 -5.46875 L 5.4375 -5.46875 L 5.4375 0 L 4.53125 0 L 4.53125 -0.84375 C 4.3125 -0.507812 4.054688 -0.257812 3.765625 -0.09375 C 3.484375 0.0625 3.148438 0.14062 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-13">
+<path style="stroke:none;" d="M 4.109375 -4.625 C 4.003906 -4.6875 3.894531 -4.726562 3.78125 -4.75 C 3.664062 -4.78125 3.535156 -4.796875 3.390625 -4.796875 C 2.878906 -4.796875 2.488281 -4.628906 2.21875 -4.296875 C 1.945312 -3.972656 1.8125 -3.5 1.8125 -2.875 L 1.8125 0 L 0.90625 0 L 0.90625 -5.46875 L 1.8125 -5.46875 L 1.8125 -4.625 C 2 -4.957031 2.242188 -5.203125 2.546875 -5.359375 C 2.847656 -5.515625 3.21875 -5.59375 3.65625 -5.59375 C 3.71875 -5.59375 3.785156 -5.585938 3.859375 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-14">
+<path style="stroke:none;" d="M 0.9375 -7.59375 L 1.84375 -7.59375 L 1.84375 0 L 0.9375 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-15">
+<path style="stroke:none;" d="M 0.9375 -5.46875 L 1.84375 -5.46875 L 1.84375 0 L 0.9375 0 Z M 0.9375 -7.59375 L 1.84375 -7.59375 L 1.84375 -6.453125 L 0.9375 -6.453125 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-16">
+<path style="stroke:none;" d="M 1.96875 -3.484375 L 1.96875 -0.8125 L 3.546875 -0.8125 C 4.078125 -0.8125 4.46875 -0.921875 4.71875 -1.140625 C 4.976562 -1.359375 5.109375 -1.695312 5.109375 -2.15625 C 5.109375 -2.601562 4.976562 -2.9375 4.71875 -3.15625 C 4.46875 -3.375 4.078125 -3.484375 3.546875 -3.484375 Z M 1.96875 -6.484375 L 1.96875 -4.28125 L 3.421875 -4.28125 C 3.910156 -4.28125 4.269531 -4.367188 4.5 -4.546875 C 4.738281 -4.734375 4.859375 -5.007812 4.859375 -5.375 C 4.859375 - [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-17">
+<path style="stroke:none;" d="M 0.546875 -5.46875 L 4.8125 -5.46875 L 4.8125 -4.65625 L 1.4375 -0.71875 L 4.8125 -0.71875 L 4.8125 0 L 0.4375 0 L 0.4375 -0.828125 L 3.8125 -4.75 L 0.546875 -4.75 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-18">
+<path style="stroke:none;" d="M 0.296875 -5.46875 L 1.25 -5.46875 L 2.953125 -0.875 L 4.671875 -5.46875 L 5.625 -5.46875 L 3.5625 0 L 2.34375 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-19">
+<path style="stroke:none;" d="M 2.4375 -3.921875 C 2.132812 -3.660156 1.914062 -3.394531 1.78125 -3.125 C 1.644531 -2.863281 1.578125 -2.59375 1.578125 -2.3125 C 1.578125 -1.832031 1.75 -1.4375 2.09375 -1.125 C 2.4375 -0.8125 2.867188 -0.65625 3.390625 -0.65625 C 3.703125 -0.65625 3.988281 -0.703125 4.25 -0.796875 C 4.519531 -0.898438 4.773438 -1.054688 5.015625 -1.265625 Z M 3.125 -4.46875 L 5.59375 -1.921875 C 5.789062 -2.210938 5.941406 -2.523438 6.046875 -2.859375 C 6.160156 -3.19140 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-20">
+<path style="stroke:none;" d="M 3.71875 -7.59375 L 3.71875 -6.84375 L 2.859375 -6.84375 C 2.535156 -6.84375 2.3125 -6.773438 2.1875 -6.640625 C 2.0625 -6.515625 2 -6.285156 2 -5.953125 L 2 -5.46875 L 3.46875 -5.46875 L 3.46875 -4.765625 L 2 -4.765625 L 2 0 L 1.09375 0 L 1.09375 -4.765625 L 0.234375 -4.765625 L 0.234375 -5.46875 L 1.09375 -5.46875 L 1.09375 -5.84375 C 1.09375 -6.457031 1.234375 -6.898438 1.515625 -7.171875 C 1.796875 -7.453125 2.242188 -7.59375 2.859375 -7.59375 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-21">
+<path style="stroke:none;" d="M 0.984375 -7.296875 L 2.453125 -7.296875 L 4.3125 -2.328125 L 6.1875 -7.296875 L 7.65625 -7.296875 L 7.65625 0 L 6.6875 0 L 6.6875 -6.40625 L 4.8125 -1.40625 L 3.8125 -1.40625 L 1.9375 -6.40625 L 1.9375 0 L 0.984375 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-22">
+<path style="stroke:none;" d="M 3.0625 -4.84375 C 2.582031 -4.84375 2.203125 -4.65625 1.921875 -4.28125 C 1.640625 -3.90625 1.5 -3.390625 1.5 -2.734375 C 1.5 -2.078125 1.632812 -1.5625 1.90625 -1.1875 C 2.1875 -0.8125 2.570312 -0.625 3.0625 -0.625 C 3.539062 -0.625 3.921875 -0.8125 4.203125 -1.1875 C 4.484375 -1.5625 4.625 -2.078125 4.625 -2.734375 C 4.625 -3.378906 4.484375 -3.890625 4.203125 -4.265625 C 3.921875 -4.648438 3.539062 -4.84375 3.0625 -4.84375 Z M 3.0625 -5.59375 C 3.84375  [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-23">
+<path style="stroke:none;" d="M 5.484375 -3.296875 L 5.484375 0 L 4.59375 0 L 4.59375 -3.265625 C 4.59375 -3.785156 4.488281 -4.171875 4.28125 -4.421875 C 4.082031 -4.679688 3.78125 -4.8125 3.375 -4.8125 C 2.894531 -4.8125 2.515625 -4.65625 2.234375 -4.34375 C 1.953125 -4.039062 1.8125 -3.625 1.8125 -3.09375 L 1.8125 0 L 0.90625 0 L 0.90625 -5.46875 L 1.8125 -5.46875 L 1.8125 -4.625 C 2.03125 -4.945312 2.285156 -5.1875 2.578125 -5.34375 C 2.867188 -5.507812 3.203125 -5.59375 3.578125 -5. [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-24">
+<path style="stroke:none;" d="M 7.078125 -7.59375 L 7.078125 -6.84375 L 6.21875 -6.84375 C 5.894531 -6.84375 5.671875 -6.773438 5.546875 -6.640625 C 5.421875 -6.515625 5.359375 -6.285156 5.359375 -5.953125 L 5.359375 -5.46875 L 6.84375 -5.46875 L 6.84375 -4.765625 L 5.359375 -4.765625 L 5.359375 0 L 4.453125 0 L 4.453125 -4.765625 L 2 -4.765625 L 2 0 L 1.09375 0 L 1.09375 -4.765625 L 0.234375 -4.765625 L 0.234375 -5.46875 L 1.09375 -5.46875 L 1.09375 -5.84375 C 1.09375 -6.457031 1.234375 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-25">
+<path style="stroke:none;" d="M 4.875 -5.265625 L 4.875 -4.421875 C 4.625 -4.554688 4.367188 -4.660156 4.109375 -4.734375 C 3.859375 -4.804688 3.601562 -4.84375 3.34375 -4.84375 C 2.757812 -4.84375 2.304688 -4.65625 1.984375 -4.28125 C 1.660156 -3.914062 1.5 -3.398438 1.5 -2.734375 C 1.5 -2.066406 1.660156 -1.546875 1.984375 -1.171875 C 2.304688 -0.804688 2.757812 -0.625 3.34375 -0.625 C 3.601562 -0.625 3.859375 -0.65625 4.109375 -0.71875 C 4.367188 -0.789062 4.625 -0.898438 4.875 -1.046 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-26">
+<path style="stroke:none;" d="M 5.484375 -3.296875 L 5.484375 0 L 4.59375 0 L 4.59375 -3.265625 C 4.59375 -3.785156 4.488281 -4.171875 4.28125 -4.421875 C 4.082031 -4.679688 3.78125 -4.8125 3.375 -4.8125 C 2.894531 -4.8125 2.515625 -4.65625 2.234375 -4.34375 C 1.953125 -4.039062 1.8125 -3.625 1.8125 -3.09375 L 1.8125 0 L 0.90625 0 L 0.90625 -7.59375 L 1.8125 -7.59375 L 1.8125 -4.625 C 2.03125 -4.945312 2.285156 -5.1875 2.578125 -5.34375 C 2.867188 -5.507812 3.203125 -5.59375 3.578125 -5. [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-27">
+<path style="stroke:none;" d="M 4.546875 -4.640625 L 4.546875 -7.59375 L 5.4375 -7.59375 L 5.4375 0 L 4.546875 0 L 4.546875 -0.828125 C 4.359375 -0.492188 4.117188 -0.25 3.828125 -0.09375 C 3.535156 0.0625 3.1875 0.140625 2.78125 0.140625 C 2.125 0.140625 1.585938 -0.117188 1.171875 -0.640625 C 0.753906 -1.171875 0.546875 -1.867188 0.546875 -2.734375 C 0.546875 -3.585938 0.753906 -4.273438 1.171875 -4.796875 C 1.585938 -5.328125 2.125 -5.59375 2.78125 -5.59375 C 3.1875 -5.59375 3.535156  [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-28">
+<path style="stroke:none;" d="M 5.484375 -5.46875 L 3.515625 -2.8125 L 5.59375 0 L 4.53125 0 L 2.9375 -2.15625 L 1.34375 0 L 0.28125 0 L 2.40625 -2.859375 L 0.46875 -5.46875 L 1.53125 -5.46875 L 2.984375 -3.515625 L 4.421875 -5.46875 Z "/>
+</symbol>
+</g>
+<image id="image75" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAB40lEQVQokT2Su24UQRREz+2+PTM7+2ANESSAbCMIiPgKQAYJRGjJCL6Xv3BowN717Ly6i2At8pLq6FRRQQU1dcQxSAahSpxsGQ5vlE80o4yERBHBzQtBRgGArJSKJ66vPwfrCGCQAQoYhuN1aMAhYFQ16zVd92Gaz6RNHtCEtMiFIgZhTspIlFgXE5Xz58+FhV8eb8wmSsYMi90YYnwKLRgxOYGYeLTl9u7TOJ2W0iiHuT+i2zghnR+6q80aLAaLNAvaln74lsuraVxISFHyUpjmtuj079379YLGAYL7A/ehfzFPrYpLUSUNPVKSXt/uvrctjeNAlVitOBw+DuMzaSG [...]
+<image id="image78" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image81" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="0" y1="-30" x2="0" y2="-10" gradientTransform="matrix(1,0,0,1,53.809602,179.402308)">
+<stop offset="0" style="stop-color:rgb(0%,50.196078%,0%);stop-opacity:1;"/>
+<stop offset="1" style="stop-color:rgb(0%,50.196078%,0%);stop-opacity:0;"/>
+</linearGradient>
+<radialGradient id="radial0" gradientUnits="userSpaceOnUse" cx="-2.5" cy="-22.5" fx="0.5" fy="-20.5" r="5" >
+<stop offset="0" style="stop-color:rgb(100%,0%,0%);stop-opacity:1;"/>
+<stop offset="0.5" style="stop-color:rgb(100%,25.098039%,25.098039%);stop-opacity:1;"/>
+<stop offset="0.6" style="stop-color:rgb(50.196078%,100%,50.196078%);stop-opacity:1;"/>
+<stop offset="0.75" style="stop-color:rgb(100%,67.45098%,67.45098%);stop-opacity:1;"/>
+<stop offset="1" style="stop-color:rgb(100%,100%,100%);stop-opacity:1;"/>
+</radialGradient>
+<image id="image84" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image87" width="4" height="4" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAABmJLR0QA/wD/AP+gvaeTAAAADElEQVQImWNgIB0AAAA0AAEjQ4N1AAAAAElFTkSuQmCC"/>
+</defs>
+<g id="surface71">
+<rect x="0" y="0" width="256" height="256" style="fill:rgb(70.980392%,81.568627%,81.568627%);fill-opacity:1;stroke:none;"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 2.640625 127.453125 C 1.921875 127.453125 1.421875 127.539062 1.140625 127.703125 C 0.859375 127.871094 0.71875 128.152344 0.71875 128.546875 C 0.71875 128.871094 0.820312 129.125 1.03125 129.3125 C 1.238281 129.5 1.523438 129.59375 1.890625 129.59375 C 2.390625 129.59375 2.789062 129.417969 3.09375 129.0625 C 3.394531 128.710938 3.546875 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.175781 123.1875 L 7.175781 124.734375 L 9.035156 124.734375 L 9.035156 125.4375 L 7.175781 125.4375 L 7.175781 128.40625 C 7.175781 128.84375 7.238281 129.132812 7.363281 129.265625 C 7.488281 129.390625 7.738281 129.453125 8.113281 129.453125 L 9.035156 129.453125 L 9.035156 130.203125 L 8.113281 130.203125 C 7.414062 130.203125 6.9296 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 14.890625 127.25 L 14.890625 127.6875 L 10.75 127.6875 C 10.789062 128.304688 10.976562 128.773438 11.3125 129.09375 C 11.644531 129.417969 12.109375 129.578125 12.703125 129.578125 C 13.046875 129.578125 13.378906 129.539062 13.703125 129.453125 C 14.035156 129.371094 14.359375 129.246094 14.671875 129.078125 L 14.671875 129.921875 C 14. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 19.839844 124.890625 L 19.839844 125.75 C 19.589844 125.617188 19.328125 125.515625 19.058594 125.453125 C 18.785156 125.390625 18.5 125.359375 18.199219 125.359375 C 17.761719 125.359375 17.425781 125.429688 17.199219 125.5625 C 16.980469 125.699219 16.871094 125.902344 16.871094 126.171875 C 16.871094 126.382812 16.949219 126.546875 17. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 26.902344 122.609375 C 26.472656 123.359375 26.152344 124.105469 25.933594 124.84375 C 25.722656 125.574219 25.621094 126.3125 25.621094 127.0625 C 25.621094 127.8125 25.722656 128.558594 25.933594 129.296875 C 26.152344 130.039062 26.472656 130.773438 26.902344 131.515625 L 26.121094 131.515625 C 25.640625 130.753906 25.277344 130.007812 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.648438 124.734375 L 29.554688 124.734375 L 29.554688 130.296875 C 29.554688 130.992188 29.417969 131.5 29.148438 131.8125 C 28.886719 132.125 28.460938 132.28125 27.867188 132.28125 L 27.523438 132.28125 L 27.523438 131.515625 L 27.773438 131.515625 C 28.117188 131.515625 28.34375 131.4375 28.460938 131.28125 C 28.585938 131.125 28.648 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 32.300781 129.375 L 32.300781 132.28125 L 31.394531 132.28125 L 31.394531 124.734375 L 32.300781 124.734375 L 32.300781 125.5625 C 32.488281 125.242188 32.722656 125 33.003906 124.84375 C 33.292969 124.6875 33.644531 124.609375 34.050781 124.609375 C 34.714844 124.609375 35.253906 124.875 35.660156 125.40625 C 36.074219 125.929688 36.2851 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.382812 127.40625 C 41.382812 126.75 41.246094 126.246094 40.976562 125.890625 C 40.703125 125.539062 40.328125 125.359375 39.851562 125.359375 C 39.359375 125.359375 38.980469 125.539062 38.710938 125.890625 C 38.449219 126.246094 38.320312 126.75 38.320312 127.40625 C 38.320312 128.054688 38.449219 128.558594 38.710938 128.921875 C 38 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 43.980469 122.609375 L 44.761719 122.609375 C 45.25 123.382812 45.613281 124.136719 45.855469 124.875 C 46.105469 125.605469 46.230469 126.335938 46.230469 127.0625 C 46.230469 127.792969 46.105469 128.527344 45.855469 129.265625 C 45.613281 130.007812 45.25 130.753906 44.761719 131.515625 L 43.980469 131.515625 C 44.417969 130.773438 44. [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-1" x="-0.78125" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-2" x="5.346679" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-3" x="9.267578" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-4" x="15.419921" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="20.629882" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-6" x="23.808593" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-7" x="27.709961" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-8" x="30.488281" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-9" x="36.835937" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-10" x="43.183593" y="130.204817"/>
+</g>
+<use xlink:href="#image75" transform="matrix(1,0,0,1,-8,92.517317)"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 222.296875 191.632812 L 220.953125 195.257812 L 223.640625 195.257812 Z M 221.734375 190.648438 L 222.859375 190.648438 L 225.640625 197.945312 L 224.609375 197.945312 L 223.9375 196.070312 L 220.65625 196.070312 L 220 197.945312 L 218.953125 197.945312 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 226.5625 195.789062 L 226.5625 192.476562 L 227.46875 192.476562 L 227.46875 195.757812 C 227.46875 196.269531 227.566406 196.65625 227.765625 196.914062 C 227.972656 197.175781 228.273438 197.304688 228.671875 197.304688 C 229.160156 197.304688 229.546875 197.15625 229.828125 196.851562 C 230.109375 196.539062 230.25 196.113281 230.25 19 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 236.476562 192.632812 L 236.476562 193.492188 C 236.226562 193.359375 235.964844 193.257812 235.695312 193.195312 C 235.421875 193.132812 235.136719 193.101562 234.835938 193.101562 C 234.398438 193.101562 234.0625 193.171875 233.835938 193.304688 C 233.617188 193.441406 233.507812 193.644531 233.507812 193.914062 C 233.507812 194.125 233 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 239.09375 190.929688 L 239.09375 192.476562 L 240.953125 192.476562 L 240.953125 193.179688 L 239.09375 193.179688 L 239.09375 196.148438 C 239.09375 196.585938 239.15625 196.875 239.28125 197.007812 C 239.40625 197.132812 239.65625 197.195312 240.03125 197.195312 L 240.953125 197.195312 L 240.953125 197.945312 L 240.03125 197.945312 C 23 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 245.296875 193.320312 C 245.191406 193.257812 245.082031 193.21875 244.96875 193.195312 C 244.851562 193.164062 244.722656 193.148438 244.578125 193.148438 C 244.066406 193.148438 243.675781 193.316406 243.40625 193.648438 C 243.132812 193.972656 243 194.445312 243 195.070312 L 243 197.945312 L 242.09375 197.945312 L 242.09375 192.476562  [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 248.71875 195.195312 C 248 195.195312 247.5 195.28125 247.21875 195.445312 C 246.9375 195.613281 246.796875 195.894531 246.796875 196.289062 C 246.796875 196.613281 246.898438 196.867188 247.109375 197.054688 C 247.316406 197.242188 247.601562 197.335938 247.96875 197.335938 C 248.46875 197.335938 248.867188 197.160156 249.171875 196.8046 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 252.363281 190.351562 L 253.269531 190.351562 L 253.269531 197.945312 L 252.363281 197.945312 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 255.140625 192.476562 L 256.046875 192.476562 L 256.046875 197.945312 L 255.140625 197.945312 Z M 255.140625 190.351562 L 256.046875 190.351562 L 256.046875 191.492188 L 255.140625 191.492188 Z "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-11" x="218.875978" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-12" x="225.716799" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-4" x="232.054689" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-2" x="237.26465" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-13" x="241.185549" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-1" x="245.296877" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-14" x="251.424807" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="254.203127" y="197.94629"/>
+</g>
+<use xlink:href="#image78" transform="matrix(1,0,0,1,248.000002,160.88379)"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.496094 184.980469 L 15.496094 187.652344 L 17.074219 187.652344 C 17.605469 187.652344 17.996094 187.542969 18.246094 187.324219 C 18.503906 187.105469 18.636719 186.769531 18.636719 186.308594 C 18.636719 185.863281 18.503906 185.527344 18.246094 185.308594 C 17.996094 185.089844 17.605469 184.980469 17.074219 184.980469 Z M 15.496094 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.496094 183.839844 C 24.390625 183.777344 24.28125 183.738281 24.167969 183.714844 C 24.050781 183.683594 23.921875 183.667969 23.777344 183.667969 C 23.265625 183.667969 22.875 183.835938 22.605469 184.167969 C 22.332031 184.492188 22.199219 184.964844 22.199219 185.589844 L 22.199219 188.464844 L 21.292969 188.464844 L 21.292969 182.9 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.917969 185.714844 C 27.199219 185.714844 26.699219 185.800781 26.417969 185.964844 C 26.136719 186.132812 25.996094 186.414062 25.996094 186.808594 C 25.996094 187.132812 26.097656 187.386719 26.308594 187.574219 C 26.515625 187.761719 26.800781 187.855469 27.167969 187.855469 C 27.667969 187.855469 28.066406 187.679688 28.371094 187.3 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.171875 182.996094 L 35.4375 182.996094 L 35.4375 183.808594 L 32.0625 187.746094 L 35.4375 187.746094 L 35.4375 188.464844 L 31.0625 188.464844 L 31.0625 187.636719 L 34.4375 183.714844 L 31.171875 183.714844 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.8125 182.996094 L 37.71875 182.996094 L 37.71875 188.464844 L 36.8125 188.464844 Z M 36.8125 180.871094 L 37.71875 180.871094 L 37.71875 182.011719 L 36.8125 182.011719 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 39.589844 180.871094 L 40.496094 180.871094 L 40.496094 188.464844 L 39.589844 188.464844 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 47.703125 180.871094 C 47.273438 181.621094 46.953125 182.367188 46.734375 183.105469 C 46.523438 183.835938 46.421875 184.574219 46.421875 185.324219 C 46.421875 186.074219 46.523438 186.820312 46.734375 187.558594 C 46.953125 188.300781 47.273438 189.035156 47.703125 189.777344 L 46.921875 189.777344 C 46.441406 189.015625 46.078125 188 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 52.933594 183.152344 L 52.933594 184.011719 C 52.683594 183.878906 52.421875 183.777344 52.152344 183.714844 C 51.878906 183.652344 51.59375 183.621094 51.292969 183.621094 C 50.855469 183.621094 50.519531 183.691406 50.292969 183.824219 C 50.074219 183.960938 49.964844 184.164062 49.964844 184.433594 C 49.964844 184.644531 50.042969 184. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 54.019531 182.996094 L 54.972656 182.996094 L 56.675781 187.589844 L 58.394531 182.996094 L 59.347656 182.996094 L 57.285156 188.464844 L 56.066406 188.464844 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 64.1875 185.667969 C 64.1875 185.011719 64.050781 184.507812 63.78125 184.152344 C 63.507812 183.800781 63.132812 183.621094 62.65625 183.621094 C 62.164062 183.621094 61.785156 183.800781 61.515625 184.152344 C 61.253906 184.507812 61.125 185.011719 61.125 185.667969 C 61.125 186.316406 61.253906 186.820312 61.515625 187.183594 C 61.7851 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 71.601562 184.542969 C 71.296875 184.804688 71.078125 185.070312 70.945312 185.339844 C 70.808594 185.601562 70.742188 185.871094 70.742188 186.152344 C 70.742188 186.632812 70.914062 187.027344 71.257812 187.339844 C 71.601562 187.652344 72.03125 187.808594 72.554688 187.808594 C 72.867188 187.808594 73.152344 187.761719 73.414062 187.66 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 81.96875 181.449219 L 81.96875 182.996094 L 83.828125 182.996094 L 83.828125 183.699219 L 81.96875 183.699219 L 81.96875 186.667969 C 81.96875 187.105469 82.03125 187.394531 82.15625 187.527344 C 82.28125 187.652344 82.53125 187.714844 82.90625 187.714844 L 83.828125 187.714844 L 83.828125 188.464844 L 82.90625 188.464844 C 82.207031 188. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 85 182.996094 L 85.90625 182.996094 L 85.90625 188.464844 L 85 188.464844 Z M 85 180.871094 L 85.90625 180.871094 L 85.90625 182.011719 L 85 182.011719 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 90.558594 180.871094 L 90.558594 181.621094 L 89.699219 181.621094 C 89.375 181.621094 89.152344 181.691406 89.027344 181.824219 C 88.902344 181.949219 88.839844 182.179688 88.839844 182.511719 L 88.839844 182.996094 L 90.308594 182.996094 L 90.308594 183.699219 L 88.839844 183.699219 L 88.839844 188.464844 L 87.933594 188.464844 L 87.933 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 91.160156 180.871094 L 91.941406 180.871094 C 92.429688 181.644531 92.792969 182.398438 93.035156 183.136719 C 93.285156 183.867188 93.410156 184.597656 93.410156 185.324219 C 93.410156 186.054688 93.285156 186.789062 93.035156 187.527344 C 92.792969 188.269531 92.429688 189.015625 91.941406 189.777344 L 91.160156 189.777344 C 91.597656 1 [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-16" x="13.526085" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-13" x="20.386436" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-1" x="24.497764" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-17" x="30.625694" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="35.874717" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-14" x="38.653038" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="41.431358" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-6" x="44.610069" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-4" x="48.511436" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-18" x="53.721397" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-9" x="59.639366" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="65.987022" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-19" x="69.165733" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="76.963585" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-2" x="80.142296" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="84.063194" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-20" x="86.841514" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-10" x="90.362022" y="188.464808"/>
+</g>
+<use xlink:href="#image81" transform="matrix(1,0,0,1,45.894737,151.402308)"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 63.808594 159.402344 L 63.132812 160.847656 L 61.199219 162.097656 L 58.265625 162.984375 L 54.730469 163.386719 L 51.074219 163.25 L 47.785156 162.59375 L 45.308594 161.507812 L 43.980469 160.136719 L 43.980469 158.667969 L 45.308594 157.296875 L 47.785156 156.210938 L 51.074219 155.554688 L 54.730469 155.417969 L 58.265625 155.820312 L 61.199219 156.707031 L 63.132812 157.957031 Z "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,100%);fill-opacity:1;" d="M 57.808594 159.402344 L 57.539062 163.015625 L 56.765625 166.140625 L 55.59375 168.355469 L 54.179688 169.359375 L 52.714844 169.019531 L 51.398438 167.382812 L 50.410156 164.667969 L 49.878906 161.238281 L 49.878906 157.566406 L 50.410156 154.136719 L 51.398438 151.421875 L 52.714844 149.785156 L 54.179688 149.445312 L 55.59375 150.449219 L 56.765625 152.664062 L 57.539062 155.789062 Z "/>
+<path style="fill-rule:nonzero;fill:url(#radial0);stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,50.196078%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 4.998991 -19.999964 L 4.502898 -17.831996 L 3.116179 -16.089808 L 1.112273 -15.124964 L -1.11429 -15.124964 L -3.118196 -16.089808 L -4.504915 -17.831996 L -5.001009 -19.999964 L -4.504915 -22.167933 L -3.118196 -23.910121 L -1.11429 -24.874964 L 1.112273 -24.874964 L 3.116179 -23.910121 L 4.502898 -22.167933 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 185.652344 115.796875 L 187.121094 115.796875 L 188.980469 120.765625 L 190.855469 115.796875 L 192.324219 115.796875 L 192.324219 123.09375 L 191.355469 123.09375 L 191.355469 116.6875 L 189.480469 121.6875 L 188.480469 121.6875 L 186.605469 116.6875 L 186.605469 123.09375 L 185.652344 123.09375 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 196.359375 118.25 C 195.878906 118.25 195.5 118.4375 195.21875 118.8125 C 194.9375 119.1875 194.796875 119.703125 194.796875 120.359375 C 194.796875 121.015625 194.929688 121.53125 195.203125 121.90625 C 195.484375 122.28125 195.867188 122.46875 196.359375 122.46875 C 196.835938 122.46875 197.21875 122.28125 197.5 121.90625 C 197.78125 12 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 204.898438 119.796875 L 204.898438 123.09375 L 204.007812 123.09375 L 204.007812 119.828125 C 204.007812 119.308594 203.902344 118.921875 203.695312 118.671875 C 203.496094 118.414062 203.195312 118.28125 202.789062 118.28125 C 202.308594 118.28125 201.929688 118.4375 201.648438 118.75 C 201.367188 119.054688 201.226562 119.46875 201.2265 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 210.300781 120.296875 C 210.300781 119.640625 210.164062 119.136719 209.894531 118.78125 C 209.621094 118.429688 209.246094 118.25 208.769531 118.25 C 208.277344 118.25 207.898438 118.429688 207.628906 118.78125 C 207.367188 119.136719 207.238281 119.640625 207.238281 120.296875 C 207.238281 120.945312 207.367188 121.449219 207.628906 121 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 215.164062 118.25 C 214.683594 118.25 214.304688 118.4375 214.023438 118.8125 C 213.742188 119.1875 213.601562 119.703125 213.601562 120.359375 C 213.601562 121.015625 213.734375 121.53125 214.007812 121.90625 C 214.289062 122.28125 214.671875 122.46875 215.164062 122.46875 C 215.640625 122.46875 216.023438 122.28125 216.304688 121.90625  [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 219.15625 115.5 L 220.0625 115.5 L 220.0625 123.09375 L 219.15625 123.09375 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 221.933594 117.625 L 222.839844 117.625 L 222.839844 123.09375 L 221.933594 123.09375 Z M 221.933594 115.5 L 222.839844 115.5 L 222.839844 116.640625 L 221.933594 116.640625 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 227.195312 120.34375 C 226.476562 120.34375 225.976562 120.429688 225.695312 120.59375 C 225.414062 120.761719 225.273438 121.042969 225.273438 121.4375 C 225.273438 121.761719 225.375 122.015625 225.585938 122.203125 C 225.792969 122.390625 226.078125 122.484375 226.445312 122.484375 C 226.945312 122.484375 227.34375 122.308594 227.64843 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 236.175781 115.5 C 235.746094 116.25 235.425781 116.996094 235.207031 117.734375 C 234.996094 118.464844 234.894531 119.203125 234.894531 119.953125 C 234.894531 120.703125 234.996094 121.449219 235.207031 122.1875 C 235.425781 122.929688 235.746094 123.664062 236.175781 124.40625 L 235.394531 124.40625 C 234.914062 123.644531 234.550781  [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 238.8125 116.078125 L 238.8125 117.625 L 240.671875 117.625 L 240.671875 118.328125 L 238.8125 118.328125 L 238.8125 121.296875 C 238.8125 121.734375 238.875 122.023438 239 122.15625 C 239.125 122.28125 239.375 122.34375 239.75 122.34375 L 240.671875 122.34375 L 240.671875 123.09375 L 239.75 123.09375 C 239.050781 123.09375 238.566406 122 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 241.839844 117.625 L 242.746094 117.625 L 242.746094 123.09375 L 241.839844 123.09375 Z M 241.839844 115.5 L 242.746094 115.5 L 242.746094 116.640625 L 241.839844 116.640625 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 250.761719 115.5 L 250.761719 116.25 L 249.902344 116.25 C 249.578125 116.25 249.355469 116.320312 249.230469 116.453125 C 249.105469 116.578125 249.042969 116.808594 249.042969 117.140625 L 249.042969 117.625 L 250.527344 117.625 L 250.527344 118.328125 L 249.042969 118.328125 L 249.042969 123.09375 L 248.136719 123.09375 L 248.136719 11 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 251.367188 115.5 L 252.148438 115.5 C 252.636719 116.273438 253 117.027344 253.242188 117.765625 C 253.492188 118.496094 253.617188 119.226562 253.617188 119.953125 C 253.617188 120.683594 253.492188 121.417969 253.242188 122.15625 C 253 122.898438 252.636719 123.644531 252.148438 124.40625 L 251.367188 124.40625 C 251.804688 123.664062 2 [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-21" x="184.668808" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-22" x="193.296738" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-23" x="199.414902" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-9" x="205.752792" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-22" x="212.100448" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-14" x="218.218613" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="220.996933" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-1" x="223.775253" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="229.903183" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-6" x="233.081894" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-2" x="236.983261" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="240.904159" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-24" x="243.68248" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-10" x="250.572128" y="123.092488"/>
+</g>
+<use xlink:href="#image84" transform="matrix(1,0,0,1,211.571152,86.029988)"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.992188 201.113281 L 20.992188 201.957031 C 20.742188 201.824219 20.484375 201.71875 20.226562 201.644531 C 19.976562 201.574219 19.71875 201.535156 19.460938 201.535156 C 18.875 201.535156 18.421875 201.722656 18.101562 202.097656 C 17.777344 202.464844 17.617188 202.980469 17.617188 203.644531 C 17.617188 204.3125 17.777344 204.832031 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.097656 203.082031 L 27.097656 206.378906 L 26.207031 206.378906 L 26.207031 203.113281 C 26.207031 202.59375 26.101562 202.207031 25.894531 201.957031 C 25.695312 201.699219 25.394531 201.566406 24.988281 201.566406 C 24.507812 201.566406 24.128906 201.722656 23.847656 202.035156 C 23.566406 202.339844 23.425781 202.753906 23.425781 20 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.890625 200.910156 L 29.796875 200.910156 L 29.796875 206.378906 L 28.890625 206.378906 Z M 28.890625 198.785156 L 29.796875 198.785156 L 29.796875 199.925781 L 28.890625 199.925781 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.667969 198.785156 L 32.574219 198.785156 L 32.574219 206.378906 L 31.667969 206.378906 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 39.132812 203.425781 L 39.132812 203.863281 L 34.992188 203.863281 C 35.03125 204.480469 35.21875 204.949219 35.554688 205.269531 C 35.886719 205.59375 36.351562 205.753906 36.945312 205.753906 C 37.289062 205.753906 37.621094 205.714844 37.945312 205.628906 C 38.277344 205.546875 38.601562 205.421875 38.914062 205.253906 L 38.914062 206. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11.441406 210.347656 C 11.011719 211.097656 10.691406 211.84375 10.472656 212.582031 C 10.261719 213.3125 10.160156 214.050781 10.160156 214.800781 C 10.160156 215.550781 10.261719 216.296875 10.472656 217.035156 C 10.691406 217.777344 11.011719 218.511719 11.441406 219.253906 L 10.660156 219.253906 C 10.179688 218.492188 9.816406 217.746 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.792969 213.300781 L 16.792969 210.347656 L 17.683594 210.347656 L 17.683594 217.941406 L 16.792969 217.941406 L 16.792969 217.113281 C 16.605469 217.449219 16.363281 217.691406 16.074219 217.847656 C 15.78125 218.003906 15.433594 218.082031 15.027344 218.082031 C 14.371094 218.082031 13.832031 217.824219 13.417969 217.300781 C 13 216.7 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.21875 214.988281 L 24.21875 215.425781 L 20.078125 215.425781 C 20.117188 216.042969 20.304688 216.511719 20.640625 216.832031 C 20.972656 217.15625 21.4375 217.316406 22.03125 217.316406 C 22.375 217.316406 22.707031 217.277344 23.03125 217.191406 C 23.363281 217.109375 23.6875 216.984375 24 216.816406 L 24 217.660156 C 23.675781 217. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.464844 210.347656 L 28.464844 211.097656 L 27.605469 211.097656 C 27.28125 211.097656 27.058594 211.167969 26.933594 211.300781 C 26.808594 211.425781 26.746094 211.65625 26.746094 211.988281 L 26.746094 212.472656 L 28.214844 212.472656 L 28.214844 213.175781 L 26.746094 213.175781 L 26.746094 217.941406 L 25.839844 217.941406 L 25.83 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.691406 215.191406 C 30.972656 215.191406 30.472656 215.277344 30.191406 215.441406 C 29.910156 215.609375 29.769531 215.890625 29.769531 216.285156 C 29.769531 216.609375 29.871094 216.863281 30.082031 217.050781 C 30.289062 217.238281 30.574219 217.332031 30.941406 217.332031 C 31.441406 217.332031 31.839844 217.15625 32.144531 216.80 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35.238281 215.785156 L 35.238281 212.472656 L 36.144531 212.472656 L 36.144531 215.753906 C 36.144531 216.265625 36.242188 216.652344 36.441406 216.910156 C 36.648438 217.171875 36.949219 217.300781 37.347656 217.300781 C 37.835938 217.300781 38.222656 217.152344 38.503906 216.847656 C 38.785156 216.535156 38.925781 216.109375 38.925781 2 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.671875 210.347656 L 42.578125 210.347656 L 42.578125 217.941406 L 41.671875 217.941406 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 45.339844 210.925781 L 45.339844 212.472656 L 47.199219 212.472656 L 47.199219 213.175781 L 45.339844 213.175781 L 45.339844 216.144531 C 45.339844 216.582031 45.402344 216.871094 45.527344 217.003906 C 45.652344 217.128906 45.902344 217.191406 46.277344 217.191406 L 47.199219 217.191406 L 47.199219 217.941406 L 46.277344 217.941406 C 45. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.96875 224.660156 C 21.488281 224.660156 21.109375 224.847656 20.828125 225.222656 C 20.546875 225.597656 20.40625 226.113281 20.40625 226.769531 C 20.40625 227.425781 20.539062 227.941406 20.8125 228.316406 C 21.09375 228.691406 21.476562 228.878906 21.96875 228.878906 C 22.445312 228.878906 22.828125 228.691406 23.109375 228.316406 C  [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 29.574219 226.707031 C 29.574219 226.050781 29.4375 225.546875 29.167969 225.191406 C 28.894531 224.839844 28.519531 224.660156 28.042969 224.660156 C 27.550781 224.660156 27.171875 224.839844 26.902344 225.191406 C 26.640625 225.546875 26.511719 226.050781 26.511719 226.707031 C 26.511719 227.355469 26.640625 227.859375 26.902344 228.222 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.25 224.238281 L 36.25 225.082031 C 36 224.949219 35.742188 224.84375 35.484375 224.769531 C 35.234375 224.699219 34.976562 224.660156 34.71875 224.660156 C 34.132812 224.660156 33.679688 224.847656 33.359375 225.222656 C 33.035156 225.589844 32.875 226.105469 32.875 226.769531 C 32.875 227.4375 33.035156 227.957031 33.359375 228.332031 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 17.507812 240.238281 L 17.507812 243.144531 L 16.601562 243.144531 L 16.601562 235.597656 L 17.507812 235.597656 L 17.507812 236.425781 C 17.695312 236.105469 17.929688 235.863281 18.210938 235.707031 C 18.5 235.550781 18.851562 235.472656 19.257812 235.472656 C 19.921875 235.472656 20.460938 235.738281 20.867188 236.269531 C 21.28125 236 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.980469 235.597656 L 23.886719 235.597656 L 23.886719 241.066406 L 22.980469 241.066406 Z M 22.980469 233.472656 L 23.886719 233.472656 L 23.886719 234.613281 L 22.980469 234.613281 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.304688 235.597656 L 28.335938 238.253906 L 30.414062 241.066406 L 29.351562 241.066406 L 27.757812 238.910156 L 26.164062 241.066406 L 25.101562 241.066406 L 27.226562 238.207031 L 25.289062 235.597656 L 26.351562 235.597656 L 27.804688 237.550781 L 29.242188 235.597656 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.054688 238.113281 L 36.054688 238.550781 L 31.914062 238.550781 C 31.953125 239.167969 32.140625 239.636719 32.476562 239.957031 C 32.808594 240.28125 33.273438 240.441406 33.867188 240.441406 C 34.210938 240.441406 34.542969 240.402344 34.867188 240.316406 C 35.199219 240.234375 35.523438 240.109375 35.835938 239.941406 L 35.835938 24 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 37.519531 233.472656 L 38.425781 233.472656 L 38.425781 241.066406 L 37.519531 241.066406 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 40.160156 233.472656 L 40.941406 233.472656 C 41.429688 234.246094 41.792969 235 42.035156 235.738281 C 42.285156 236.46875 42.410156 237.199219 42.410156 237.925781 C 42.410156 238.65625 42.285156 239.390625 42.035156 240.128906 C 41.792969 240.871094 41.429688 241.617188 40.941406 242.378906 L 40.160156 242.378906 C 40.597656 241.636719 [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-25" x="16.116835" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-26" x="21.614882" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="27.952772" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-14" x="30.731093" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-3" x="33.509413" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="39.661757" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-6" x="8.345839" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-27" x="12.247206" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-3" x="18.594862" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-20" x="24.747206" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-1" x="28.267714" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-12" x="34.395643" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-14" x="40.733534" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-2" x="43.511854" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="47.432753" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-22" x="18.907362" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-9" x="25.025526" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-25" x="31.373182" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-5" x="36.871229" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-8" x="15.694471" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-15" x="22.042128" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-28" x="24.820448" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-3" x="30.4308" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-14" x="36.583143" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+  <use xlink:href="#glyph0-10" x="39.361464" y="241.065278"/>
+</g>
+<use xlink:href="#image87" transform="matrix(1,0,0,1,27.478651,191.877778)"/>
+</g>
+</svg>
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.polygon.pdf b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.pdf
new file mode 100644
index 0000000..201bb9b
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.polygon.svg b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.svg
new file mode 100644
index 0000000..4f2a943
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.svg
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256pt" height="256pt" viewBox="0 0 256 256" version="1.1">
+<defs>
+<image id="image98" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image101" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image104" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image107" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image110" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image113" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image116" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image119" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image122" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+</defs>
+<g id="surface94">
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 115.65625 56.117188 L 105.6875 50.042969 L 102.367188 41.585938 L 108.777344 39.203125 L 132.507812 28.515625 L 132.222656 27.054688 L 141.789062 23.054688 L 145.058594 32.933594 L 157.566406 68.800781 L 159.464844 73.835938 L 139.804688 81.023438 L 135.675781 71.570312 L 135.882812 66.1875 L 136.332031 59.265625 L 134.109375 51.578125 L 131.019531 45.429688 L 127.984375 38.5078 [...]
+<use xlink:href="#image98" transform="matrix(1,0,0,1,127.94131,42.841104)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 132.222656 27.054688 L 132.507812 28.515625 L 108.777344 39.203125 L 102.367188 41.585938 L 105.6875 50.042969 L 115.65625 56.117188 L 112.578125 61.574219 L 105.039062 74.605469 L 88.605469 74.414062 L 84.253906 74.566406 L 79.761719 74.644531 L 68.664062 79.027344 L 67.753906 79.296875 L 62.101562 68.839844 L 56.199219 60.996094 L 54.90625 61.496094 L 53.773438 58.652344 L 64. [...]
+<use xlink:href="#image101" transform="matrix(1,0,0,1,79.836035,51.528053)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 115.65625 56.117188 L 118.730469 49.234375 L 122.578125 41.546875 L 127.984375 38.507812 L 131.019531 45.429688 L 134.109375 51.578125 L 136.332031 59.265625 L 135.882812 66.1875 L 129.527344 66.6875 L 124.503906 68.453125 L 121.441406 69.992188 L 109.082031 80.371094 L 104.925781 81.371094 L 102.195312 83.253906 L 101.664062 85.136719 L 100.078125 88.445312 L 97.773438 91.67187 [...]
+<use xlink:href="#image104" transform="matrix(1,0,0,1,90.102079,69.169472)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 53.773438 58.652344 L 54.90625 61.496094 L 61.679688 81.753906 L 71.058594 108.050781 L 76.140625 119.121094 L 72.308594 122.695312 L 69.605469 130.535156 L 61.425781 133.496094 L 54.027344 114.699219 L 44.699219 118.351562 L 33.3125 123.15625 L 14.414062 130.113281 L 11.242188 120.925781 L 9.871094 116.082031 L 10.171875 111.96875 L 10.238281 106.011719 L 14.351562 105.242188 L [...]
+<use xlink:href="#image107" transform="matrix(1,0,0,1,31.167216,88.825)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 54.90625 61.496094 L 56.199219 60.996094 L 62.101562 68.839844 L 67.753906 79.296875 L 61.679688 81.753906 Z "/>
+<use xlink:href="#image110" transform="matrix(1,0,0,1,53.212824,64.674519)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 135.882812 66.1875 L 135.675781 71.570312 L 139.804688 81.023438 L 127.082031 86.292969 L 110.335938 92.902344 L 109.199219 89.058594 L 101.835938 91.441406 L 103.257812 95.363281 L 91.441406 100.015625 L 94.757812 93.558594 L 97.773438 91.671875 L 100.078125 88.445312 L 101.664062 85.136719 L 102.195312 83.253906 L 104.925781 81.371094 L 109.082031 80.371094 L 121.441406 69.992 [...]
+<use xlink:href="#image113" transform="matrix(1,0,0,1,111.187238,73.378811)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 159.464844 73.835938 L 170.804688 68.917969 L 173.171875 76.296875 L 176.019531 82.679688 L 183.402344 90.136719 L 184.109375 93.789062 L 174.757812 89.945312 L 171.078125 89.90625 L 169.109375 93.441406 L 185.757812 103.707031 L 182.722656 112.007812 L 181.617188 113.125 L 179.890625 116.3125 L 174.433594 119.734375 L 167.976562 121.773438 L 157.070312 125.578125 L 144.289062 1 [...]
+<use xlink:href="#image116" transform="matrix(1,0,0,1,153.144399,90.01164)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 183.402344 90.136719 L 188.046875 87.058594 L 192.929688 83.832031 L 200.394531 81.488281 L 201.0625 81.371094 L 205.175781 79.601562 L 213.167969 74.914062 L 216.90625 70.414062 L 228.015625 79.488281 L 228.765625 84.601562 L 242.207031 79.796875 L 241.550781 74.835938 L 245.570312 72.53125 L 247.820312 71.339844 L 250.59375 70.03125 L 252.679688 76.488281 L 254.8125 83.101562  [...]
+<use xlink:href="#image119" transform="matrix(1,0,0,1,155.527409,142.794669)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 110.335938 92.902344 L 127.082031 86.292969 L 128.355469 90.597656 L 130.351562 95.019531 L 136.710938 92.828125 L 143.125 89.828125 L 149.34375 87.90625 L 155.929688 105.933594 L 155.917969 110.203125 L 153.800781 116.507812 L 151.734375 120.082031 L 147.519531 123.503906 L 141.585938 126.386719 L 144.289062 132.804688 L 138.746094 132.382812 L 94.914062 153.0625 L 90.503906 15 [...]
+<use xlink:href="#image122" transform="matrix(1,0,0,1,105.830723,109.774724)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 110.335938 92.902344 L 103.257812 95.363281 L 101.835938 91.441406 L 109.199219 89.058594 Z "/>
+</g>
+</svg>
diff --git a/test/python_tests/images/style-comp-op/clear.png b/test/python_tests/images/style-comp-op/clear.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/style-comp-op/clear.png differ
diff --git a/test/python_tests/images/style-comp-op/color.png b/test/python_tests/images/style-comp-op/color.png
new file mode 100644
index 0000000..81dae90
Binary files /dev/null and b/test/python_tests/images/style-comp-op/color.png differ
diff --git a/test/python_tests/images/style-comp-op/color_burn.png b/test/python_tests/images/style-comp-op/color_burn.png
new file mode 100644
index 0000000..97a2700
Binary files /dev/null and b/test/python_tests/images/style-comp-op/color_burn.png differ
diff --git a/test/python_tests/images/style-comp-op/color_dodge.png b/test/python_tests/images/style-comp-op/color_dodge.png
new file mode 100644
index 0000000..932484b
Binary files /dev/null and b/test/python_tests/images/style-comp-op/color_dodge.png differ
diff --git a/test/python_tests/images/style-comp-op/contrast.png b/test/python_tests/images/style-comp-op/contrast.png
new file mode 100644
index 0000000..34b0a0a
Binary files /dev/null and b/test/python_tests/images/style-comp-op/contrast.png differ
diff --git a/test/python_tests/images/style-comp-op/darken.png b/test/python_tests/images/style-comp-op/darken.png
new file mode 100644
index 0000000..851ea68
Binary files /dev/null and b/test/python_tests/images/style-comp-op/darken.png differ
diff --git a/test/python_tests/images/style-comp-op/difference.png b/test/python_tests/images/style-comp-op/difference.png
new file mode 100644
index 0000000..7caf8dd
Binary files /dev/null and b/test/python_tests/images/style-comp-op/difference.png differ
diff --git a/test/python_tests/images/style-comp-op/divide.png b/test/python_tests/images/style-comp-op/divide.png
new file mode 100644
index 0000000..6f3aa77
Binary files /dev/null and b/test/python_tests/images/style-comp-op/divide.png differ
diff --git a/test/python_tests/images/style-comp-op/dst.png b/test/python_tests/images/style-comp-op/dst.png
new file mode 100644
index 0000000..68e0b39
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_atop.png b/test/python_tests/images/style-comp-op/dst_atop.png
new file mode 100644
index 0000000..bae1c89
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_atop.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_in.png b/test/python_tests/images/style-comp-op/dst_in.png
new file mode 100644
index 0000000..bae1c89
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_in.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_out.png b/test/python_tests/images/style-comp-op/dst_out.png
new file mode 100644
index 0000000..03e60dd
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_out.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_over.png b/test/python_tests/images/style-comp-op/dst_over.png
new file mode 100644
index 0000000..68e0b39
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_over.png differ
diff --git a/test/python_tests/images/style-comp-op/exclusion.png b/test/python_tests/images/style-comp-op/exclusion.png
new file mode 100644
index 0000000..2cbe25e
Binary files /dev/null and b/test/python_tests/images/style-comp-op/exclusion.png differ
diff --git a/test/python_tests/images/style-comp-op/grain_extract.png b/test/python_tests/images/style-comp-op/grain_extract.png
new file mode 100644
index 0000000..9bc59c8
Binary files /dev/null and b/test/python_tests/images/style-comp-op/grain_extract.png differ
diff --git a/test/python_tests/images/style-comp-op/grain_merge.png b/test/python_tests/images/style-comp-op/grain_merge.png
new file mode 100644
index 0000000..5dd3d47
Binary files /dev/null and b/test/python_tests/images/style-comp-op/grain_merge.png differ
diff --git a/test/python_tests/images/style-comp-op/hard_light.png b/test/python_tests/images/style-comp-op/hard_light.png
new file mode 100644
index 0000000..0ad6fe5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/hard_light.png differ
diff --git a/test/python_tests/images/style-comp-op/hue.png b/test/python_tests/images/style-comp-op/hue.png
new file mode 100644
index 0000000..2955c1a
Binary files /dev/null and b/test/python_tests/images/style-comp-op/hue.png differ
diff --git a/test/python_tests/images/style-comp-op/invert.png b/test/python_tests/images/style-comp-op/invert.png
new file mode 100644
index 0000000..5e97592
Binary files /dev/null and b/test/python_tests/images/style-comp-op/invert.png differ
diff --git a/test/python_tests/images/style-comp-op/lighten.png b/test/python_tests/images/style-comp-op/lighten.png
new file mode 100644
index 0000000..4d44fc5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/lighten.png differ
diff --git a/test/python_tests/images/style-comp-op/linear_burn.png b/test/python_tests/images/style-comp-op/linear_burn.png
new file mode 100644
index 0000000..458772c
Binary files /dev/null and b/test/python_tests/images/style-comp-op/linear_burn.png differ
diff --git a/test/python_tests/images/style-comp-op/linear_dodge.png b/test/python_tests/images/style-comp-op/linear_dodge.png
new file mode 100644
index 0000000..c94969c
Binary files /dev/null and b/test/python_tests/images/style-comp-op/linear_dodge.png differ
diff --git a/test/python_tests/images/style-comp-op/minus.png b/test/python_tests/images/style-comp-op/minus.png
new file mode 100644
index 0000000..83d68aa
Binary files /dev/null and b/test/python_tests/images/style-comp-op/minus.png differ
diff --git a/test/python_tests/images/style-comp-op/multiply.png b/test/python_tests/images/style-comp-op/multiply.png
new file mode 100644
index 0000000..87b2452
Binary files /dev/null and b/test/python_tests/images/style-comp-op/multiply.png differ
diff --git a/test/python_tests/images/style-comp-op/overlay.png b/test/python_tests/images/style-comp-op/overlay.png
new file mode 100644
index 0000000..44a864b
Binary files /dev/null and b/test/python_tests/images/style-comp-op/overlay.png differ
diff --git a/test/python_tests/images/style-comp-op/plus.png b/test/python_tests/images/style-comp-op/plus.png
new file mode 100644
index 0000000..c94969c
Binary files /dev/null and b/test/python_tests/images/style-comp-op/plus.png differ
diff --git a/test/python_tests/images/style-comp-op/saturation.png b/test/python_tests/images/style-comp-op/saturation.png
new file mode 100644
index 0000000..fcbf639
Binary files /dev/null and b/test/python_tests/images/style-comp-op/saturation.png differ
diff --git a/test/python_tests/images/style-comp-op/screen.png b/test/python_tests/images/style-comp-op/screen.png
new file mode 100644
index 0000000..43ab7ac
Binary files /dev/null and b/test/python_tests/images/style-comp-op/screen.png differ
diff --git a/test/python_tests/images/style-comp-op/soft_light.png b/test/python_tests/images/style-comp-op/soft_light.png
new file mode 100644
index 0000000..5d5487a
Binary files /dev/null and b/test/python_tests/images/style-comp-op/soft_light.png differ
diff --git a/test/python_tests/images/style-comp-op/src.png b/test/python_tests/images/style-comp-op/src.png
new file mode 100644
index 0000000..a7a96f5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src.png differ
diff --git a/test/python_tests/images/style-comp-op/src_atop.png b/test/python_tests/images/style-comp-op/src_atop.png
new file mode 100644
index 0000000..1f04875
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_atop.png differ
diff --git a/test/python_tests/images/style-comp-op/src_in.png b/test/python_tests/images/style-comp-op/src_in.png
new file mode 100644
index 0000000..a7a96f5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_in.png differ
diff --git a/test/python_tests/images/style-comp-op/src_out.png b/test/python_tests/images/style-comp-op/src_out.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_out.png differ
diff --git a/test/python_tests/images/style-comp-op/src_over.png b/test/python_tests/images/style-comp-op/src_over.png
new file mode 100644
index 0000000..42d7b16
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_over.png differ
diff --git a/test/python_tests/images/style-comp-op/value.png b/test/python_tests/images/style-comp-op/value.png
new file mode 100644
index 0000000..8fd064d
Binary files /dev/null and b/test/python_tests/images/style-comp-op/value.png differ
diff --git a/test/python_tests/images/style-comp-op/xor.png b/test/python_tests/images/style-comp-op/xor.png
new file mode 100644
index 0000000..b062cb6
Binary files /dev/null and b/test/python_tests/images/style-comp-op/xor.png differ
diff --git a/test/python_tests/images/style-image-filter/agg-stack-blur22.png b/test/python_tests/images/style-image-filter/agg-stack-blur22.png
new file mode 100644
index 0000000..1d1b7ca
Binary files /dev/null and b/test/python_tests/images/style-image-filter/agg-stack-blur22.png differ
diff --git a/test/python_tests/images/style-image-filter/blur.png b/test/python_tests/images/style-image-filter/blur.png
new file mode 100644
index 0000000..ec6fc7f
Binary files /dev/null and b/test/python_tests/images/style-image-filter/blur.png differ
diff --git a/test/python_tests/images/style-image-filter/edge-detect.png b/test/python_tests/images/style-image-filter/edge-detect.png
new file mode 100644
index 0000000..74eff4d
Binary files /dev/null and b/test/python_tests/images/style-image-filter/edge-detect.png differ
diff --git a/test/python_tests/images/style-image-filter/emboss.png b/test/python_tests/images/style-image-filter/emboss.png
new file mode 100644
index 0000000..bf74d99
Binary files /dev/null and b/test/python_tests/images/style-image-filter/emboss.png differ
diff --git a/test/python_tests/images/style-image-filter/gray.png b/test/python_tests/images/style-image-filter/gray.png
new file mode 100644
index 0000000..7ee05f5
Binary files /dev/null and b/test/python_tests/images/style-image-filter/gray.png differ
diff --git a/test/python_tests/images/style-image-filter/invert.png b/test/python_tests/images/style-image-filter/invert.png
new file mode 100644
index 0000000..52bcf95
Binary files /dev/null and b/test/python_tests/images/style-image-filter/invert.png differ
diff --git a/test/python_tests/images/style-image-filter/none.png b/test/python_tests/images/style-image-filter/none.png
new file mode 100644
index 0000000..245966d
Binary files /dev/null and b/test/python_tests/images/style-image-filter/none.png differ
diff --git a/test/python_tests/images/style-image-filter/sharpen.png b/test/python_tests/images/style-image-filter/sharpen.png
new file mode 100644
index 0000000..8599186
Binary files /dev/null and b/test/python_tests/images/style-image-filter/sharpen.png differ
diff --git a/test/python_tests/images/style-image-filter/sobel.png b/test/python_tests/images/style-image-filter/sobel.png
new file mode 100644
index 0000000..c1b7092
Binary files /dev/null and b/test/python_tests/images/style-image-filter/sobel.png differ
diff --git a/test/python_tests/images/style-image-filter/x-gradient.png b/test/python_tests/images/style-image-filter/x-gradient.png
new file mode 100644
index 0000000..fdc5f74
Binary files /dev/null and b/test/python_tests/images/style-image-filter/x-gradient.png differ
diff --git a/test/python_tests/images/style-image-filter/y-gradient.png b/test/python_tests/images/style-image-filter/y-gradient.png
new file mode 100644
index 0000000..b84a491
Binary files /dev/null and b/test/python_tests/images/style-image-filter/y-gradient.png differ
diff --git a/test/python_tests/images/support/a.png b/test/python_tests/images/support/a.png
new file mode 100644
index 0000000..3d0cc72
Binary files /dev/null and b/test/python_tests/images/support/a.png differ
diff --git a/test/python_tests/images/support/b.png b/test/python_tests/images/support/b.png
new file mode 100644
index 0000000..6eca9e1
Binary files /dev/null and b/test/python_tests/images/support/b.png differ
diff --git a/test/python_tests/images/support/dataraster_coloring.png b/test/python_tests/images/support/dataraster_coloring.png
new file mode 100644
index 0000000..da3cac4
Binary files /dev/null and b/test/python_tests/images/support/dataraster_coloring.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png+e=miniz.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+e=miniz.png
new file mode 100644
index 0000000..4cc101b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+t=0.png
new file mode 100644
index 0000000..b2aa991
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png.png
new file mode 100644
index 0000000..b2aa991
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+e=miniz.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+e=miniz.png
new file mode 100644
index 0000000..046b4fc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+t=0.png
new file mode 100644
index 0000000..16481a4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png32.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32.png
new file mode 100644
index 0000000..0f15a35
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+e=miniz.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+e=miniz.png
new file mode 100644
index 0000000..d7dd172
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png
new file mode 100644
index 0000000..d6e19b7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1.png
new file mode 100644
index 0000000..d6e19b7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=0.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=1.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=2.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=2.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png
new file mode 100644
index 0000000..ad0aca7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1.png
new file mode 100644
index 0000000..ad0aca7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=0.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=1.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=2.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=2.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha=false.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha=false.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha=false.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_compression=0.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_compression=0.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_compression=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_filtering=2.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_filtering=2.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_filtering=2.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_quality=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_quality=50.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_quality=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+autofilter=0.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+autofilter=0.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+autofilter=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_sharpness=4.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_sharpness=4.webp
new file mode 100644
index 0000000..f29a97e
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_sharpness=4.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_strength=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_strength=50.webp
new file mode 100644
index 0000000..87c1fe6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_type=1+autofilter=1.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_type=1+autofilter=1.webp
new file mode 100644
index 0000000..752148d
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_type=1+autofilter=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=0.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=0.webp
new file mode 100644
index 0000000..f0f3838
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=6.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=6.webp
new file mode 100644
index 0000000..be253e2
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=6.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partition_limit=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partition_limit=50.webp
new file mode 100644
index 0000000..837ff3b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partition_limit=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partitions=3.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partitions=3.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partitions=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+pass=10.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+pass=10.webp
new file mode 100644
index 0000000..7d71893
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+pass=10.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+preprocessing=1.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+preprocessing=1.webp
new file mode 100644
index 0000000..f5e64b7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+preprocessing=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+quality=64.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+quality=64.webp
new file mode 100644
index 0000000..efd13ad
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+quality=64.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+segments=3.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+segments=3.webp
new file mode 100644
index 0000000..967cc57
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+segments=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+sns_strength=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+sns_strength=50.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+sns_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_PSNR=.5.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_PSNR=.5.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_PSNR=.5.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_size=100.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_size=100.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_size=100.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png+e=miniz.png b/test/python_tests/images/support/encoding-opts/blank-png+e=miniz.png
new file mode 100644
index 0000000..334ffe5
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png.png b/test/python_tests/images/support/encoding-opts/blank-png.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png32+e=miniz.png b/test/python_tests/images/support/encoding-opts/blank-png32+e=miniz.png
new file mode 100644
index 0000000..8708c32
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png32+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png32+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png32+t=0.png
new file mode 100644
index 0000000..50e3890
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png32+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png32.png b/test/python_tests/images/support/encoding-opts/blank-png32.png
new file mode 100644
index 0000000..04469ac
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png32.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+e=miniz.png b/test/python_tests/images/support/encoding-opts/blank-png8+e=miniz.png
new file mode 100644
index 0000000..334ffe5
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=2.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=2.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=2.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=2.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha=false.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha=false.webp
new file mode 100644
index 0000000..da95b42
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha=false.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha_compression=0.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_compression=0.webp
new file mode 100644
index 0000000..2e264d4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_compression=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha_filtering=2.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_filtering=2.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_filtering=2.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha_quality=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_quality=50.webp
new file mode 100644
index 0000000..10cea1c
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_quality=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+autofilter=0.webp b/test/python_tests/images/support/encoding-opts/blank-webp+autofilter=0.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+autofilter=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+filter_sharpness=4.webp b/test/python_tests/images/support/encoding-opts/blank-webp+filter_sharpness=4.webp
new file mode 100644
index 0000000..932a4de
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+filter_sharpness=4.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+filter_strength=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+filter_strength=50.webp
new file mode 100644
index 0000000..2e65b9b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+filter_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+filter_type=1+autofilter=1.webp b/test/python_tests/images/support/encoding-opts/blank-webp+filter_type=1+autofilter=1.webp
new file mode 100644
index 0000000..7e3bd76
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+filter_type=1+autofilter=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+method=0.webp b/test/python_tests/images/support/encoding-opts/blank-webp+method=0.webp
new file mode 100644
index 0000000..5c64924
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+method=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+method=6.webp b/test/python_tests/images/support/encoding-opts/blank-webp+method=6.webp
new file mode 100644
index 0000000..ef84f4c
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+method=6.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+partition_limit=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+partition_limit=50.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+partition_limit=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+partitions=3.webp b/test/python_tests/images/support/encoding-opts/blank-webp+partitions=3.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+partitions=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+pass=10.webp b/test/python_tests/images/support/encoding-opts/blank-webp+pass=10.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+pass=10.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+preprocessing=1.webp b/test/python_tests/images/support/encoding-opts/blank-webp+preprocessing=1.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+preprocessing=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+quality=64.webp b/test/python_tests/images/support/encoding-opts/blank-webp+quality=64.webp
new file mode 100644
index 0000000..0eb26aa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+quality=64.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+segments=3.webp b/test/python_tests/images/support/encoding-opts/blank-webp+segments=3.webp
new file mode 100644
index 0000000..af3082b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+segments=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+sns_strength=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+sns_strength=50.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+sns_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+target_PSNR=.5.webp b/test/python_tests/images/support/encoding-opts/blank-webp+target_PSNR=.5.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+target_PSNR=.5.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+target_size=100.webp b/test/python_tests/images/support/encoding-opts/blank-webp+target_size=100.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+target_size=100.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp.webp b/test/python_tests/images/support/encoding-opts/blank-webp.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-17cols.png b/test/python_tests/images/support/encoding-opts/png8-17cols.png
new file mode 100644
index 0000000..22f4c35
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-17cols.png differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-2px.A.png b/test/python_tests/images/support/encoding-opts/png8-2px.A.png
new file mode 100644
index 0000000..f047b08
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-2px.A.png differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-2px.png b/test/python_tests/images/support/encoding-opts/png8-2px.png
new file mode 100644
index 0000000..f047b08
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-2px.png differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-9cols.png b/test/python_tests/images/support/encoding-opts/png8-9cols.png
new file mode 100644
index 0000000..a781b37
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-9cols.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png+e=miniz.png b/test/python_tests/images/support/encoding-opts/solid-png+e=miniz.png
new file mode 100644
index 0000000..c3e5db4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png.png b/test/python_tests/images/support/encoding-opts/solid-png.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png32+e=miniz.png b/test/python_tests/images/support/encoding-opts/solid-png32+e=miniz.png
new file mode 100644
index 0000000..f155fce
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png32+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png32+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png32+t=0.png
new file mode 100644
index 0000000..21fffbf
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png32+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png32.png b/test/python_tests/images/support/encoding-opts/solid-png32.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png32.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+e=miniz.png b/test/python_tests/images/support/encoding-opts/solid-png8+e=miniz.png
new file mode 100644
index 0000000..c3e5db4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=2.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=2.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=2.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=2.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha=false.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha=false.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha=false.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha_compression=0.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_compression=0.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_compression=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha_filtering=2.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_filtering=2.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_filtering=2.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha_quality=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_quality=50.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_quality=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+autofilter=0.webp b/test/python_tests/images/support/encoding-opts/solid-webp+autofilter=0.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+autofilter=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+filter_sharpness=4.webp b/test/python_tests/images/support/encoding-opts/solid-webp+filter_sharpness=4.webp
new file mode 100644
index 0000000..1eadaa1
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+filter_sharpness=4.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+filter_strength=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+filter_strength=50.webp
new file mode 100644
index 0000000..621e6cc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+filter_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+filter_type=1+autofilter=1.webp b/test/python_tests/images/support/encoding-opts/solid-webp+filter_type=1+autofilter=1.webp
new file mode 100644
index 0000000..1372321
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+filter_type=1+autofilter=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+method=0.webp b/test/python_tests/images/support/encoding-opts/solid-webp+method=0.webp
new file mode 100644
index 0000000..e7cff65
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+method=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+method=6.webp b/test/python_tests/images/support/encoding-opts/solid-webp+method=6.webp
new file mode 100644
index 0000000..5a76594
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+method=6.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+partition_limit=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+partition_limit=50.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+partition_limit=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+partitions=3.webp b/test/python_tests/images/support/encoding-opts/solid-webp+partitions=3.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+partitions=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+pass=10.webp b/test/python_tests/images/support/encoding-opts/solid-webp+pass=10.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+pass=10.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+preprocessing=1.webp b/test/python_tests/images/support/encoding-opts/solid-webp+preprocessing=1.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+preprocessing=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+quality=64.webp b/test/python_tests/images/support/encoding-opts/solid-webp+quality=64.webp
new file mode 100644
index 0000000..fba30ff
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+quality=64.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+segments=3.webp b/test/python_tests/images/support/encoding-opts/solid-webp+segments=3.webp
new file mode 100644
index 0000000..84ba1a8
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+segments=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+sns_strength=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+sns_strength=50.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+sns_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+target_PSNR=.5.webp b/test/python_tests/images/support/encoding-opts/solid-webp+target_PSNR=.5.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+target_PSNR=.5.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+target_size=100.webp b/test/python_tests/images/support/encoding-opts/solid-webp+target_size=100.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+target_size=100.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp.webp b/test/python_tests/images/support/encoding-opts/solid-webp.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp.webp differ
diff --git a/test/python_tests/images/support/mapnik-layer-buffer-size.png b/test/python_tests/images/support/mapnik-layer-buffer-size.png
new file mode 100644
index 0000000..f1de74a
Binary files /dev/null and b/test/python_tests/images/support/mapnik-layer-buffer-size.png differ
diff --git a/test/python_tests/images/support/mapnik-marker-ellipse-render1.png b/test/python_tests/images/support/mapnik-marker-ellipse-render1.png
new file mode 100644
index 0000000..7854c56
Binary files /dev/null and b/test/python_tests/images/support/mapnik-marker-ellipse-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-marker-ellipse-render2.png b/test/python_tests/images/support/mapnik-marker-ellipse-render2.png
new file mode 100644
index 0000000..c2a4963
Binary files /dev/null and b/test/python_tests/images/support/mapnik-marker-ellipse-render2.png differ
diff --git a/test/python_tests/images/support/mapnik-merc2merc-reprojection-render1.png b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render1.png
new file mode 100644
index 0000000..2b49eb1
Binary files /dev/null and b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-merc2merc-reprojection-render2.png b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render2.png
new file mode 100644
index 0000000..e2c237d
Binary files /dev/null and b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render2.png differ
diff --git a/test/python_tests/images/support/mapnik-merc2wgs84-reprojection-render.png b/test/python_tests/images/support/mapnik-merc2wgs84-reprojection-render.png
new file mode 100644
index 0000000..718d60a
Binary files /dev/null and b/test/python_tests/images/support/mapnik-merc2wgs84-reprojection-render.png differ
diff --git a/test/python_tests/images/support/mapnik-palette-test.png b/test/python_tests/images/support/mapnik-palette-test.png
new file mode 100644
index 0000000..94ae976
Binary files /dev/null and b/test/python_tests/images/support/mapnik-palette-test.png differ
diff --git a/test/python_tests/images/support/mapnik-python-circle-render1.png b/test/python_tests/images/support/mapnik-python-circle-render1.png
new file mode 100644
index 0000000..cb5eba4
Binary files /dev/null and b/test/python_tests/images/support/mapnik-python-circle-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-python-point-render1.png b/test/python_tests/images/support/mapnik-python-point-render1.png
new file mode 100644
index 0000000..b131d86
Binary files /dev/null and b/test/python_tests/images/support/mapnik-python-point-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-style-level-opacity.png b/test/python_tests/images/support/mapnik-style-level-opacity.png
new file mode 100644
index 0000000..91c8d47
Binary files /dev/null and b/test/python_tests/images/support/mapnik-style-level-opacity.png differ
diff --git a/test/python_tests/images/support/mapnik-wgs842merc-reprojection-render.png b/test/python_tests/images/support/mapnik-wgs842merc-reprojection-render.png
new file mode 100644
index 0000000..d0afcd2
Binary files /dev/null and b/test/python_tests/images/support/mapnik-wgs842merc-reprojection-render.png differ
diff --git a/test/python_tests/images/support/marker-in-center-not-placed.png b/test/python_tests/images/support/marker-in-center-not-placed.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/marker-in-center-not-placed.png differ
diff --git a/test/python_tests/images/support/marker-in-center.png b/test/python_tests/images/support/marker-in-center.png
new file mode 100644
index 0000000..7845d4f
Binary files /dev/null and b/test/python_tests/images/support/marker-in-center.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-0.005.png b/test/python_tests/images/support/marker-text-line-scale-factor-0.005.png
new file mode 100644
index 0000000..5ba0afc
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-0.005.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-0.1.png b/test/python_tests/images/support/marker-text-line-scale-factor-0.1.png
new file mode 100644
index 0000000..306424d
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-0.1.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-0.899.png b/test/python_tests/images/support/marker-text-line-scale-factor-0.899.png
new file mode 100644
index 0000000..cb2c651
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-0.899.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-1.5.png b/test/python_tests/images/support/marker-text-line-scale-factor-1.5.png
new file mode 100644
index 0000000..a1b34a3
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-1.5.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-1.png b/test/python_tests/images/support/marker-text-line-scale-factor-1.png
new file mode 100644
index 0000000..c86c5fa
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-1.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-10.png b/test/python_tests/images/support/marker-text-line-scale-factor-10.png
new file mode 100644
index 0000000..8a7842f
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-10.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-100.png b/test/python_tests/images/support/marker-text-line-scale-factor-100.png
new file mode 100644
index 0000000..f1c0b52
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-100.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-1e-05.png b/test/python_tests/images/support/marker-text-line-scale-factor-1e-05.png
new file mode 100644
index 0000000..0f36830
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-1e-05.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-2.png b/test/python_tests/images/support/marker-text-line-scale-factor-2.png
new file mode 100644
index 0000000..e3ad67f
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-2.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-5.png b/test/python_tests/images/support/marker-text-line-scale-factor-5.png
new file mode 100644
index 0000000..2be5f2d
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-5.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_16bsi_subquery-16BSI-135.png b/test/python_tests/images/support/pgraster/data_subquery-data_16bsi_subquery-16BSI-135.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_16bsi_subquery-16BSI-135.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_16bui_subquery-16BUI-126.png b/test/python_tests/images/support/pgraster/data_subquery-data_16bui_subquery-16BUI-126.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_16bui_subquery-16BUI-126.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_2bui_subquery-2BUI-3.png b/test/python_tests/images/support/pgraster/data_subquery-data_2bui_subquery-2BUI-3.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_2bui_subquery-2BUI-3.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_32bf_subquery-32BF-450.png b/test/python_tests/images/support/pgraster/data_subquery-data_32bf_subquery-32BF-450.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_32bf_subquery-32BF-450.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_32bsi_subquery-32BSI-264.png b/test/python_tests/images/support/pgraster/data_subquery-data_32bsi_subquery-32BSI-264.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_32bsi_subquery-32BSI-264.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_32bui_subquery-32BUI-255.png b/test/python_tests/images/support/pgraster/data_subquery-data_32bui_subquery-32BUI-255.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_32bui_subquery-32BUI-255.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_4bui_subquery-4BUI-15.png b/test/python_tests/images/support/pgraster/data_subquery-data_4bui_subquery-4BUI-15.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_4bui_subquery-4BUI-15.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_64bf_subquery-64BF-3072.png b/test/python_tests/images/support/pgraster/data_subquery-data_64bf_subquery-64BF-3072.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_64bf_subquery-64BF-3072.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_8bsi_subquery-8BSI-69.png b/test/python_tests/images/support/pgraster/data_subquery-data_8bsi_subquery-8BSI-69.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_8bsi_subquery-8BSI-69.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_8bui_subquery-8BUI-63.png b/test/python_tests/images/support/pgraster/data_subquery-data_8bui_subquery-8BUI-63.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_8bui_subquery-8BUI-63.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bsi_subquery-16BSI-144.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bsi_subquery-16BSI-144.png
new file mode 100644
index 0000000..719c7e0
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bsi_subquery-16BSI-144.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bui_subquery-16BUI-126.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bui_subquery-16BUI-126.png
new file mode 100644
index 0000000..a6aa1a6
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bui_subquery-16BUI-126.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_2bui_subquery-2BUI-3.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_2bui_subquery-2BUI-3.png
new file mode 100644
index 0000000..62aa163
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_2bui_subquery-2BUI-3.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bsi_subquery-32BSI-129.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bsi_subquery-32BSI-129.png
new file mode 100644
index 0000000..b134b2d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bsi_subquery-32BSI-129.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bui_subquery-32BUI-255.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bui_subquery-32BUI-255.png
new file mode 100644
index 0000000..5f8035a
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bui_subquery-32BUI-255.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_4bui_subquery-4BUI-15.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_4bui_subquery-4BUI-15.png
new file mode 100644
index 0000000..2667c06
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_4bui_subquery-4BUI-15.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bsi_subquery-8BSI-69.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bsi_subquery-8BSI-69.png
new file mode 100644
index 0000000..85abadd
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bsi_subquery-8BSI-69.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bui_subquery-8BUI-63.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bui_subquery-8BUI-63.png
new file mode 100644
index 0000000..06d6249
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bui_subquery-8BUI-63.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png
new file mode 100644
index 0000000..cae6205
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png
new file mode 100644
index 0000000..4fdf9ff
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png
new file mode 100644
index 0000000..4fdf9ff
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png
new file mode 100644
index 0000000..cae6205
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_subquery-rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png b/test/python_tests/images/support/pgraster/rgba_subquery-rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png
new file mode 100644
index 0000000..d83016d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_subquery-rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png differ
diff --git a/test/python_tests/images/support/raster-alpha.png b/test/python_tests/images/support/raster-alpha.png
new file mode 100644
index 0000000..3c79bb3
Binary files /dev/null and b/test/python_tests/images/support/raster-alpha.png differ
diff --git a/test/python_tests/images/support/raster_warping.png b/test/python_tests/images/support/raster_warping.png
new file mode 100644
index 0000000..7a6dea7
Binary files /dev/null and b/test/python_tests/images/support/raster_warping.png differ
diff --git a/test/python_tests/images/support/raster_warping_does_not_overclip_source.png b/test/python_tests/images/support/raster_warping_does_not_overclip_source.png
new file mode 100644
index 0000000..0d6ccc5
Binary files /dev/null and b/test/python_tests/images/support/raster_warping_does_not_overclip_source.png differ
diff --git a/test/python_tests/images/support/spacing.png b/test/python_tests/images/support/spacing.png
new file mode 100644
index 0000000..8d6bb40
Binary files /dev/null and b/test/python_tests/images/support/spacing.png differ
diff --git a/test/python_tests/images/support/transparency/aerial_rgb.png b/test/python_tests/images/support/transparency/aerial_rgb.png
new file mode 100644
index 0000000..16481a4
Binary files /dev/null and b/test/python_tests/images/support/transparency/aerial_rgb.png differ
diff --git a/test/python_tests/images/support/transparency/aerial_rgba.png b/test/python_tests/images/support/transparency/aerial_rgba.png
new file mode 100644
index 0000000..0f15a35
Binary files /dev/null and b/test/python_tests/images/support/transparency/aerial_rgba.png differ
diff --git a/test/python_tests/images/support/transparency/white0.png b/test/python_tests/images/support/transparency/white0.png
new file mode 100644
index 0000000..955861a
Binary files /dev/null and b/test/python_tests/images/support/transparency/white0.png differ
diff --git a/test/python_tests/images/support/transparency/white0.webp b/test/python_tests/images/support/transparency/white0.webp
new file mode 100644
index 0000000..f276b81
Binary files /dev/null and b/test/python_tests/images/support/transparency/white0.webp differ
diff --git a/test/python_tests/images/support/transparency/white1.png b/test/python_tests/images/support/transparency/white1.png
new file mode 100644
index 0000000..db4e827
Binary files /dev/null and b/test/python_tests/images/support/transparency/white1.png differ
diff --git a/test/python_tests/images/support/transparency/white2.png b/test/python_tests/images/support/transparency/white2.png
new file mode 100644
index 0000000..136e098
Binary files /dev/null and b/test/python_tests/images/support/transparency/white2.png differ
diff --git a/test/python_tests/introspection_test.py b/test/python_tests/introspection_test.py
new file mode 100644
index 0000000..afb1cc2
--- /dev/null
+++ b/test/python_tests/introspection_test.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+import os
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+import mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_introspect_symbolizers():
+    # create a symbolizer
+    p = mapnik.PointSymbolizer()
+    p.file = "../data/images/dummy.png"
+    p.allow_overlap = True
+    p.opacity = 0.5
+
+    eq_(p.allow_overlap, True)
+    eq_(p.opacity, 0.5)
+    eq_(p.filename,'../data/images/dummy.png')
+
+    # make sure the defaults
+    # are what we think they are
+    eq_(p.allow_overlap, True)
+    eq_(p.opacity,0.5)
+    eq_(p.filename,'../data/images/dummy.png')
+
+    # contruct objects to hold it
+    r = mapnik.Rule()
+    r.symbols.append(p)
+    s = mapnik.Style()
+    s.rules.append(r)
+    m = mapnik.Map(0,0)
+    m.append_style('s',s)
+
+    # try to figure out what is
+    # in the map and make sure
+    # style is there and the same
+
+    s2 = m.find_style('s')
+    rules = s2.rules
+    eq_(len(rules),1)
+    r2 = rules[0]
+    syms = r2.symbols
+    eq_(len(syms),1)
+
+    ## TODO here, we can do...
+    sym = syms[0]
+    p2 = sym.extract()
+    assert isinstance(p2,mapnik.PointSymbolizer)
+
+    eq_(p2.allow_overlap, True)
+    eq_(p2.opacity, 0.5)
+    eq_(p2.filename,'../data/images/dummy.png')
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/json_feature_properties_test.py b/test/python_tests/json_feature_properties_test.py
new file mode 100644
index 0000000..47f2428
--- /dev/null
+++ b/test/python_tests/json_feature_properties_test.py
@@ -0,0 +1,102 @@
+#encoding: utf8
+
+from nose.tools import eq_
+import mapnik
+from utilities import run_all
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+chars = [
+ {
+   "name":"single_quote",
+   "test": "string with ' quote",
+   "json": '{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \' quote"}}'
+ },
+ {
+   "name":"escaped_single_quote",
+   "test":"string with \' quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \' quote"}}'
+ },
+ {
+   "name":"double_quote",
+   "test":'string with " quote',
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\" quote"}}'
+ },
+ {
+   "name":"double_quote2",
+   "test":"string with \" quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\" quote"}}'
+ },
+ {
+   "name":"reverse_solidus", # backslash
+   "test":"string with \\ quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\\ quote"}}'
+ },
+ {
+   "name":"solidus", # forward slash
+   "test":"string with / quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with / quote"}}'
+ },
+ {
+   "name":"backspace",
+   "test":"string with \b quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\b quote"}}'
+ },
+ {
+   "name":"formfeed",
+   "test":"string with \f quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\f quote"}}'
+ },
+ {
+   "name":"newline",
+   "test":"string with \n quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\n quote"}}'
+ },
+ {
+   "name":"carriage_return",
+   "test":"string with \r quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\r quote"}}'
+ },
+ {
+   "name":"horiztonal_tab",
+   "test":"string with \t quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\t quote"}}'
+ },
+ # remainder are c++ reserved, but not json
+ {
+   "name":"vert_tab",
+   "test":"string with \v quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\u000b quote"}}'
+ },
+ {
+   "name":"alert",
+   "test":"string with \a quote",
+   "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \u0007 quote"}}'
+ }
+]
+
+ctx = mapnik.Context()
+ctx.push('name')
+
+def test_char_escaping():
+    for char in chars:
+        feat = mapnik.Feature(ctx,1)
+        expected = char['test']
+        feat["name"] = expected
+        eq_(feat["name"],expected)
+        # confirm the python json module
+        # is working as we would expect
+        pyjson2 = json.loads(char['json'])
+        eq_(pyjson2['properties']['name'],expected)
+        # confirm our behavior is the same as python json module
+        # for the original string
+        geojson_feat_string = feat.to_geojson()
+        eq_(geojson_feat_string,char['json'],"Mapnik's json escaping is not to spec: actual(%s) and expected(%s) for %s" % (geojson_feat_string,char['json'],char['name']))
+        # and the round tripped string
+        pyjson = json.loads(geojson_feat_string)
+        eq_(pyjson['properties']['name'],expected)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/layer_buffer_size_test.py b/test/python_tests/layer_buffer_size_test.py
new file mode 100644
index 0000000..83765a7
--- /dev/null
+++ b/test/python_tests/layer_buffer_size_test.py
@@ -0,0 +1,35 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+
+    # the negative buffer on the layer should
+    # override the postive map buffer leading
+    # only one point to be rendered in the map
+    def test_layer_buffer_size_1():
+        m = mapnik.Map(512,512)
+        eq_(m.buffer_size,0)
+        mapnik.load_map(m,'../data/good_maps/layer_buffer_size_reduction.xml')
+        eq_(m.buffer_size,256)
+        eq_(m.layers[0].buffer_size,-150)
+        m.zoom_all()
+        im = mapnik.Image(m.width,m.height)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-layer-buffer-size.png'
+        expected = 'images/support/mapnik-layer-buffer-size.png'
+        im.save(actual,"png32")
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/layer_modification_test.py b/test/python_tests/layer_modification_test.py
new file mode 100644
index 0000000..7517ac2
--- /dev/null
+++ b/test/python_tests/layer_modification_test.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+
+import os
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_adding_datasource_to_layer():
+    map_string = '''<?xml version="1.0" encoding="utf-8"?>
+<Map>
+
+    <Layer name="world_borders">
+        <StyleName>world_borders_style</StyleName>
+        <StyleName>point_style</StyleName>
+        <!-- leave datasource empty -->
+        <!--
+        <Datasource>
+            <Parameter name="file">../data/shp/world_merc.shp</Parameter>
+            <Parameter name="type">shape</Parameter>
+        </Datasource>
+        -->
+    </Layer>
+
+</Map>
+'''
+    m = mapnik.Map(256, 256)
+
+    try:
+        mapnik.load_map_from_string(m, map_string)
+
+        # validate it loaded fine
+        eq_(m.layers[0].styles[0],'world_borders_style')
+        eq_(m.layers[0].styles[1],'point_style')
+        eq_(len(m.layers),1)
+
+        # also assign a variable reference to that layer
+        # below we will test that this variable references
+        # the same object that is attached to the map
+        lyr = m.layers[0]
+
+        # ensure that there was no datasource for the layer...
+        eq_(m.layers[0].datasource,None)
+        eq_(lyr.datasource,None)
+
+        # also note that since the srs was black it defaulted to wgs84
+        eq_(m.layers[0].srs,'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+        eq_(lyr.srs,'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+
+        # now add a datasource one...
+        ds = mapnik.Shapefile(file='../data/shp/world_merc.shp')
+        m.layers[0].datasource = ds
+
+        # now ensure it is attached
+        eq_(m.layers[0].datasource.describe()['name'],"shape")
+        eq_(lyr.datasource.describe()['name'],"shape")
+
+        # and since we have now added a shapefile in spherical mercator, adjust the projection
+        lyr.srs = '+proj=merc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
+
+        # test that assignment
+        eq_(m.layers[0].srs,'+proj=merc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs')
+        eq_(lyr.srs,'+proj=merc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs')
+    except RuntimeError, e:
+        # only test datasources that we have installed
+        if not 'Could not create datasource' in str(e):
+            raise RuntimeError(e)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/layer_test.py b/test/python_tests/layer_test.py
new file mode 100644
index 0000000..00ea434
--- /dev/null
+++ b/test/python_tests/layer_test.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+
+# Map initialization
+def test_layer_init():
+    l = mapnik.Layer('test')
+    eq_(l.name,'test')
+    eq_(l.srs,'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+    eq_(l.envelope(),mapnik.Box2d())
+    eq_(l.clear_label_cache,False)
+    eq_(l.cache_features,False)
+    eq_(l.visible(1),True)
+    eq_(l.active,True)
+    eq_(l.datasource,None)
+    eq_(l.queryable,False)
+    eq_(l.minimum_scale_denominator,0.0)
+    eq_(l.maximum_scale_denominator > 1e+6,True)
+    eq_(l.group_by,"")
+    eq_(l.maximum_extent,None)
+    eq_(l.buffer_size,None)
+    eq_(len(l.styles),0)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/load_map_test.py b/test/python_tests/load_map_test.py
new file mode 100644
index 0000000..5eb211e
--- /dev/null
+++ b/test/python_tests/load_map_test.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+import os, glob, mapnik
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+    # make the tests silent to suppress unsupported params from harfbuzz tests
+    # TODO: remove this after harfbuzz branch merges
+    mapnik.logger.set_severity(mapnik.severity_type.None)
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def teardown():
+    mapnik.logger.set_severity(default_logging_severity)
+
+def test_broken_files():
+    default_logging_severity = mapnik.logger.get_severity()
+    mapnik.logger.set_severity(mapnik.severity_type.None)
+    broken_files = glob.glob("../data/broken_maps/*.xml")
+    # Add a filename that doesn't exist 
+    broken_files.append("../data/broken/does_not_exist.xml")
+
+    failures = [];
+    for filename in broken_files:
+        try:
+            m = mapnik.Map(512, 512)
+            strict = True
+            mapnik.load_map(m, filename, strict)
+            failures.append('Loading broken map (%s) did not raise RuntimeError!' % filename)
+        except RuntimeError:
+            pass
+    eq_(len(failures),0,'\n'+'\n'.join(failures))
+    mapnik.logger.set_severity(default_logging_severity)
+
+def test_can_parse_xml_with_deprecated_properties():
+    default_logging_severity = mapnik.logger.get_severity()
+    mapnik.logger.set_severity(mapnik.severity_type.None)
+    files_with_deprecated_props = glob.glob("../data/deprecated_maps/*.xml")
+
+    failures = [];
+    for filename in files_with_deprecated_props:
+        try:
+            m = mapnik.Map(512, 512)
+            strict = True
+            mapnik.load_map(m, filename, strict)
+            base_path = os.path.dirname(filename)
+            mapnik.load_map_from_string(m,open(filename,'rb').read(),strict,base_path)
+        except RuntimeError, e:
+            # only test datasources that we have installed
+            if not 'Could not create datasource' in str(e) \
+               and not 'could not connect' in str(e):
+                failures.append('Failed to load valid map %s (%s)' % (filename,e))
+    eq_(len(failures),0,'\n'+'\n'.join(failures))
+    mapnik.logger.set_severity(default_logging_severity)
+
+def test_good_files():
+    good_files = glob.glob("../data/good_maps/*.xml")
+    good_files.extend(glob.glob("../visual_tests/styles/*.xml"))
+
+    failures = [];
+    for filename in good_files:
+        try:
+            m = mapnik.Map(512, 512)
+            strict = True
+            mapnik.load_map(m, filename, strict)
+            base_path = os.path.dirname(filename)
+            mapnik.load_map_from_string(m,open(filename,'rb').read(),strict,base_path)
+        except RuntimeError, e:
+            # only test datasources that we have installed
+            if not 'Could not create datasource' in str(e) \
+               and not 'could not connect' in str(e):
+                failures.append('Failed to load valid map %s (%s)' % (filename,e))
+    eq_(len(failures),0,'\n'+'\n'.join(failures))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/map_query_test.py b/test/python_tests/map_query_test.py
new file mode 100644
index 0000000..4035f7a
--- /dev/null
+++ b/test/python_tests/map_query_test.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,raises,assert_almost_equal
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+# map has no layers
+ at raises(IndexError)
+def test_map_query_throw1():
+    m = mapnik.Map(256,256)
+    m.zoom_to_box(mapnik.Box2d(-1,-1,0,0))
+    m.query_point(0,0,0)
+
+# only positive indexes
+ at raises(IndexError)
+def test_map_query_throw2():
+    m = mapnik.Map(256,256)
+    m.query_point(-1,0,0)
+
+# map has never been zoomed (nodata)
+ at raises(RuntimeError)
+def test_map_query_throw3():
+    m = mapnik.Map(256,256)
+    m.query_point(0,0,0)
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+    # map has never been zoomed (even with data)
+    @raises(RuntimeError)
+    def test_map_query_throw4():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml')
+        m.query_point(0,0,0)
+
+    # invalid coords in general (do not intersect)
+    @raises(RuntimeError)
+    def test_map_query_throw5():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml')
+        m.zoom_all()
+        m.query_point(0,9999999999999999,9999999999999999)
+
+    def test_map_query_works1():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+        merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+        m.maximum_extent = merc_bounds
+        m.zoom_all()
+        fs = m.query_point(0,-11012435.5376, 4599674.6134) # somewhere in kansas
+        feat = fs.next()
+        eq_(feat.attributes['NAME_FORMA'],u'United States of America')
+
+    def test_map_query_works2():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/merc2wgs84_reprojection.xml')
+        wgs84_bounds = mapnik.Box2d(-179.999999975,-85.0511287776,179.999999975,85.0511287776)
+        m.maximum_extent = wgs84_bounds
+        # caution - will go square due to evil aspect_fix_mode backhandedness
+        m.zoom_all()
+        #mapnik.render_to_file(m,'works2.png')
+        # validate that aspect_fix_mode modified the bbox reasonably
+        e = m.envelope()
+        assert_almost_equal(e.minx, -179.999999975, places=7)
+        assert_almost_equal(e.miny, -167.951396161, places=7)
+        assert_almost_equal(e.maxx, 179.999999975, places=7)
+        assert_almost_equal(e.maxy, 192.048603789, places=7)
+        fs = m.query_point(0,-98.9264, 38.1432) # somewhere in kansas
+        feat = fs.next()
+        eq_(feat.attributes['NAME'],u'United States')
+
+    def test_map_query_in_pixels_works1():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+        merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+        m.maximum_extent = merc_bounds
+        m.zoom_all()
+        fs = m.query_map_point(0,55,100) # somewhere in middle of us
+        feat = fs.next()
+        eq_(feat.attributes['NAME_FORMA'],u'United States of America')
+
+    def test_map_query_in_pixels_works2():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/merc2wgs84_reprojection.xml')
+        wgs84_bounds = mapnik.Box2d(-179.999999975,-85.0511287776,179.999999975,85.0511287776)
+        m.maximum_extent = wgs84_bounds
+        # caution - will go square due to evil aspect_fix_mode backhandedness
+        m.zoom_all()
+        # validate that aspect_fix_mode modified the bbox reasonably
+        e = m.envelope()
+        assert_almost_equal(e.minx, -179.999999975, places=7)
+        assert_almost_equal(e.miny, -167.951396161, places=7)
+        assert_almost_equal(e.maxx, 179.999999975, places=7)
+        assert_almost_equal(e.maxy, 192.048603789, places=7)
+        fs = m.query_map_point(0,55,100) # somewhere in Canada
+        feat = fs.next()
+        eq_(feat.attributes['NAME'],u'Canada')
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/mapnik_logger_test.py b/test/python_tests/mapnik_logger_test.py
new file mode 100644
index 0000000..c27ff46
--- /dev/null
+++ b/test/python_tests/mapnik_logger_test.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+
+def test_logger_init():
+    eq_(mapnik.severity_type.Debug,0)
+    eq_(mapnik.severity_type.Warn,1)
+    eq_(mapnik.severity_type.Error,2)
+    eq_(mapnik.severity_type.None,3)
+    default = mapnik.logger.get_severity()
+    mapnik.logger.set_severity(mapnik.severity_type.Debug)
+    eq_(mapnik.logger.get_severity(),mapnik.severity_type.Debug)
+    mapnik.logger.set_severity(default)
+    eq_(mapnik.logger.get_severity(),default)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/mapnik_test_data_test.py b/test/python_tests/mapnik_test_data_test.py
new file mode 100644
index 0000000..b4226e1
--- /dev/null
+++ b/test/python_tests/mapnik_test_data_test.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from utilities import execution_path, run_all
+import os, mapnik
+from glob import glob
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+    mapnik.logger.set_severity(mapnik.severity_type.None)
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def teardown():
+    mapnik.logger.set_severity(default_logging_severity)
+
+plugin_mapping = {
+    '.csv' : ['csv'],
+    '.json': ['geojson','ogr'],
+    '.tif' : ['gdal'],
+    #'.tif' : ['gdal','raster'],
+    '.kml' : ['ogr'],
+    '.gpx' : ['ogr'],
+    '.vrt' : ['gdal']
+}
+
+def test_opening_data():
+    # https://github.com/mapbox/mapnik-test-data
+    # cd tests/data
+    # git clone --depth 1 https://github.com/mapbox/mapnik-test-data
+    if os.path.exists('../data/mapnik-test-data/'):
+        files = glob('../data/mapnik-test-data/data/*/*.*')
+        for filepath in files:
+            ext = os.path.splitext(filepath)[1]
+            if plugin_mapping.get(ext):
+                #print 'testing opening %s' % filepath
+                if 'topo' in filepath:
+                    kwargs = {'type': 'ogr','file': filepath}
+                    kwargs['layer_by_index'] = 0
+                    try:
+                        mapnik.Datasource(**kwargs)
+                    except Exception, e:
+                        print 'could not open, %s: %s' % (kwargs,e)
+                else:
+                   for plugin in plugin_mapping[ext]:
+                      kwargs = {'type': plugin,'file': filepath}
+                      if plugin is 'ogr':
+                          kwargs['layer_by_index'] = 0
+                      try:
+                          mapnik.Datasource(**kwargs)
+                      except Exception, e:
+                          print 'could not open, %s: %s' % (kwargs,e)
+            #else:
+            #    print 'skipping opening %s' % filepath
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/markers_complex_rendering_test.py b/test/python_tests/markers_complex_rendering_test.py
new file mode 100644
index 0000000..efce684
--- /dev/null
+++ b/test/python_tests/markers_complex_rendering_test.py
@@ -0,0 +1,43 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'csv' in mapnik.DatasourceCache.plugin_names():
+    def test_marker_ellipse_render1():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/marker_ellipse_transform.xml')
+        m.zoom_all()
+        im = mapnik.Image(m.width,m.height)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-marker-ellipse-render1.png'
+        expected = 'images/support/mapnik-marker-ellipse-render1.png'
+        im.save(actual,'png32')
+        if os.environ.get('UPDATE'):
+            im.save(expected,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+    def test_marker_ellipse_render2():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/marker_ellipse_transform2.xml')
+        m.zoom_all()
+        im = mapnik.Image(m.width,m.height)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-marker-ellipse-render2.png'
+        expected = 'images/support/mapnik-marker-ellipse-render2.png'
+        im.save(actual,'png32')
+        if os.environ.get('UPDATE'):
+            im.save(expected,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/memory_datasource_test.py b/test/python_tests/memory_datasource_test.py
new file mode 100644
index 0000000..bd82bea
--- /dev/null
+++ b/test/python_tests/memory_datasource_test.py
@@ -0,0 +1,34 @@
+#encoding: utf8
+import mapnik
+from utilities import run_all
+from nose.tools import eq_
+
+def test_add_feature():
+    md = mapnik.MemoryDatasource()
+    eq_(md.num_features(), 0)
+    context = mapnik.Context()
+    context.push('foo')
+    feature = mapnik.Feature(context,1)
+    feature['foo'] = 'bar'
+    feature.geometry = mapnik.Geometry.from_wkt('POINT(2 3)')
+    md.add_feature(feature)
+    eq_(md.num_features(), 1)
+
+    featureset = md.features_at_point(mapnik.Coord(2,3))
+    retrieved = []
+
+    for feat in featureset:
+        retrieved.append(feat)
+
+    eq_(len(retrieved), 1)
+    f = retrieved[0]
+    eq_(f['foo'], 'bar')
+
+    featureset = md.features_at_point(mapnik.Coord(20,30))
+    retrieved = []
+    for feat in featureset:
+        retrieved.append(feat)
+    eq_(len(retrieved), 0)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/multi_tile_raster_test.py b/test/python_tests/multi_tile_raster_test.py
new file mode 100644
index 0000000..7dda876
--- /dev/null
+++ b/test/python_tests/multi_tile_raster_test.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_multi_tile_policy():
+    srs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+    lyr = mapnik.Layer('raster')
+    if 'raster' in mapnik.DatasourceCache.plugin_names():
+        lyr.datasource = mapnik.Raster(
+            file = '../data/raster_tiles/${x}/${y}.tif',
+            lox = -180,
+            loy = -90,
+            hix = 180,
+            hiy = 90,
+            multi = 1,
+            tile_size = 256,
+            x_width = 2,
+            y_width = 2
+            )
+        lyr.srs = srs
+        _map = mapnik.Map(256, 256, srs)
+        style = mapnik.Style()
+        rule = mapnik.Rule()
+        sym = mapnik.RasterSymbolizer()
+        rule.symbols.append(sym)
+        style.rules.append(rule)
+        _map.append_style('foo', style)
+        lyr.styles.append('foo')
+        _map.layers.append(lyr)
+        _map.zoom_to_box(lyr.envelope())
+
+        im = mapnik.Image(_map.width, _map.height)
+        mapnik.render(_map, im)
+
+        # test green chunk
+        eq_(im.view(0,64,1,1).tostring(), '\x00\xff\x00\xff')
+        eq_(im.view(127,64,1,1).tostring(), '\x00\xff\x00\xff')
+        eq_(im.view(0,127,1,1).tostring(), '\x00\xff\x00\xff')
+        eq_(im.view(127,127,1,1).tostring(), '\x00\xff\x00\xff')
+
+        # test blue chunk
+        eq_(im.view(128,64,1,1).tostring(), '\x00\x00\xff\xff')
+        eq_(im.view(255,64,1,1).tostring(), '\x00\x00\xff\xff')
+        eq_(im.view(128,127,1,1).tostring(), '\x00\x00\xff\xff')
+        eq_(im.view(255,127,1,1).tostring(), '\x00\x00\xff\xff')
+
+        # test red chunk
+        eq_(im.view(0,128,1,1).tostring(), '\xff\x00\x00\xff')
+        eq_(im.view(127,128,1,1).tostring(), '\xff\x00\x00\xff')
+        eq_(im.view(0,191,1,1).tostring(), '\xff\x00\x00\xff')
+        eq_(im.view(127,191,1,1).tostring(), '\xff\x00\x00\xff')
+
+        # test magenta chunk
+        eq_(im.view(128,128,1,1).tostring(), '\xff\x00\xff\xff')
+        eq_(im.view(255,128,1,1).tostring(), '\xff\x00\xff\xff')
+        eq_(im.view(128,191,1,1).tostring(), '\xff\x00\xff\xff')
+        eq_(im.view(255,191,1,1).tostring(), '\xff\x00\xff\xff')
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/object_test.py b/test/python_tests/object_test.py
new file mode 100644
index 0000000..0f23e71
--- /dev/null
+++ b/test/python_tests/object_test.py
@@ -0,0 +1,569 @@
+# #!/usr/bin/env python
+# # -*- coding: utf-8 -*-
+
+# import os
+# from nose.tools import *
+# from utilities import execution_path, run_all
+# import tempfile
+
+# import mapnik
+
+# def setup():
+#     # All of the paths used are relative, if we run the tests
+#     # from another directory we need to chdir()
+#     os.chdir(execution_path('.'))
+
+# def test_debug_symbolizer():
+#     s = mapnik.DebugSymbolizer()
+#     eq_(s.mode,mapnik.debug_symbolizer_mode.collision)
+
+# def test_raster_symbolizer():
+#     s = mapnik.RasterSymbolizer()
+#     eq_(s.comp_op,mapnik.CompositeOp.src_over) # note: mode is deprecated
+#     eq_(s.scaling,mapnik.scaling_method.NEAR)
+#     eq_(s.opacity,1.0)
+#     eq_(s.colorizer,None)
+#     eq_(s.filter_factor,-1)
+#     eq_(s.mesh_size,16)
+#     eq_(s.premultiplied,None)
+#     s.premultiplied = True
+#     eq_(s.premultiplied,True)
+
+# def test_line_pattern():
+#     s = mapnik.LinePatternSymbolizer(mapnik.PathExpression('../data/images/dummy.png'))
+#     eq_(s.filename, '../data/images/dummy.png')
+#     eq_(s.smooth,0.0)
+#     eq_(s.transform,'')
+#     eq_(s.offset,0.0)
+#     eq_(s.comp_op,mapnik.CompositeOp.src_over)
+#     eq_(s.clip,True)
+
+# def test_line_symbolizer():
+#     s = mapnik.LineSymbolizer()
+#     eq_(s.rasterizer, mapnik.line_rasterizer.FULL)
+#     eq_(s.smooth,0.0)
+#     eq_(s.comp_op,mapnik.CompositeOp.src_over)
+#     eq_(s.clip,True)
+#     eq_(s.stroke.width, 1)
+#     eq_(s.stroke.opacity, 1)
+#     eq_(s.stroke.color, mapnik.Color('black'))
+#     eq_(s.stroke.line_cap, mapnik.line_cap.BUTT_CAP)
+#     eq_(s.stroke.line_join, mapnik.line_join.MITER_JOIN)
+
+#     l = mapnik.LineSymbolizer(mapnik.Color('blue'), 5.0)
+
+#     eq_(l.stroke.width, 5)
+#     eq_(l.stroke.opacity, 1)
+#     eq_(l.stroke.color, mapnik.Color('blue'))
+#     eq_(l.stroke.line_cap, mapnik.line_cap.BUTT_CAP)
+#     eq_(l.stroke.line_join, mapnik.line_join.MITER_JOIN)
+
+#     s = mapnik.Stroke(mapnik.Color('blue'), 5.0)
+#     l = mapnik.LineSymbolizer(s)
+
+#     eq_(l.stroke.width, 5)
+#     eq_(l.stroke.opacity, 1)
+#     eq_(l.stroke.color, mapnik.Color('blue'))
+#     eq_(l.stroke.line_cap, mapnik.line_cap.BUTT_CAP)
+#     eq_(l.stroke.line_join, mapnik.line_join.MITER_JOIN)
+
+# def test_line_symbolizer_stroke_reference():
+#     l = mapnik.LineSymbolizer(mapnik.Color('green'),0.1)
+#     l.stroke.add_dash(.1,.1)
+#     l.stroke.add_dash(.1,.1)
+#     eq_(l.stroke.get_dashes(), [(.1,.1),(.1,.1)])
+#     eq_(l.stroke.color,mapnik.Color('green'))
+#     eq_(l.stroke.opacity,1.0)
+#     assert_almost_equal(l.stroke.width,0.1)
+
+# # https://github.com/mapnik/mapnik/issues/1427
+# def test_stroke_dash_api():
+#     stroke = mapnik.Stroke()
+#     dashes = [(1.0,1.0)]
+#     stroke.dasharray = dashes
+#     eq_(stroke.dasharray, dashes)
+#     stroke.add_dash(.1,.1)
+#     dashes.append((.1,.1))
+#     eq_(stroke.dasharray, dashes)
+
+
+# def test_text_symbolizer():
+#     s = mapnik.TextSymbolizer()
+#     eq_(s.comp_op,mapnik.CompositeOp.src_over)
+#     eq_(s.clip,True)
+#     eq_(s.halo_rasterizer,mapnik.halo_rasterizer.FULL)
+
+#     # https://github.com/mapnik/mapnik/issues/1420
+#     eq_(s.text_transform, mapnik.text_transform.NONE)
+
+#     # old args required method
+#     ts = mapnik.TextSymbolizer(mapnik.Expression('[Field_Name]'), 'Font Name', 8, mapnik.Color('black'))
+# #    eq_(str(ts.name), str(mapnik2.Expression('[Field_Name]'))) name field is no longer supported
+#     eq_(ts.format.face_name, 'Font Name')
+#     eq_(ts.format.text_size, 8)
+#     eq_(ts.format.fill, mapnik.Color('black'))
+#     eq_(ts.properties.label_placement, mapnik.label_placement.POINT_PLACEMENT)
+#     eq_(ts.properties.horizontal_alignment, mapnik.horizontal_alignment.AUTO)
+
+# def test_shield_symbolizer_init():
+#     s = mapnik.ShieldSymbolizer(mapnik.Expression('[Field Name]'), 'DejaVu Sans Bold', 6, mapnik.Color('#000000'), mapnik.PathExpression('../data/images/dummy.png'))
+#     eq_(s.comp_op,mapnik.CompositeOp.src_over)
+#     eq_(s.clip,True)
+#     eq_(s.displacement, (0.0,0.0))
+#     eq_(s.allow_overlap, False)
+#     eq_(s.avoid_edges, False)
+#     eq_(s.character_spacing,0)
+#     #eq_(str(s.name), str(mapnik2.Expression('[Field Name]'))) name field is no longer supported
+#     eq_(s.face_name, 'DejaVu Sans Bold')
+#     eq_(s.allow_overlap, False)
+#     eq_(s.fill, mapnik.Color('#000000'))
+#     eq_(s.halo_fill, mapnik.Color('rgb(255,255,255)'))
+#     eq_(s.halo_radius, 0)
+#     eq_(s.label_placement, mapnik.label_placement.POINT_PLACEMENT)
+#     eq_(s.minimum_distance, 0.0)
+#     eq_(s.text_ratio, 0)
+#     eq_(s.text_size, 6)
+#     eq_(s.wrap_width, 0)
+#     eq_(s.vertical_alignment, mapnik.vertical_alignment.AUTO)
+#     eq_(s.label_spacing, 0)
+#     eq_(s.label_position_tolerance, 0)
+#     # 22.5 * M_PI/180.0 initialized by default
+#     assert_almost_equal(s.max_char_angle_delta, 0.39269908169872414)
+
+#     eq_(s.text_transform, mapnik.text_transform.NONE)
+#     eq_(s.line_spacing, 0)
+#     eq_(s.character_spacing, 0)
+
+#     # r1341
+#     eq_(s.wrap_before, False)
+#     eq_(s.horizontal_alignment, mapnik.horizontal_alignment.AUTO)
+#     eq_(s.justify_alignment, mapnik.justify_alignment.AUTO)
+#     eq_(s.opacity, 1.0)
+
+#     # r2300
+#     eq_(s.minimum_padding, 0.0)
+
+#     # was mixed with s.opacity
+#     eq_(s.text_opacity, 1.0)
+
+#     eq_(s.shield_displacement, (0.0,0.0))
+#     # TODO - the pattern in bindings seems to be to get/set
+#     # strings for PathExpressions... should we pass objects?
+#     eq_(s.filename, '../data/images/dummy.png')
+
+#     # 11c34b1: default transform list is empty, not identity matrix
+#     eq_(s.transform, '')
+
+#     eq_(s.fontset, None)
+
+# # ShieldSymbolizer missing image file
+# # images paths are now PathExpressions are evaluated at runtime
+# # so it does not make sense to throw...
+# #@raises(RuntimeError)
+# #def test_shieldsymbolizer_missing_image():
+# #    s = mapnik.ShieldSymbolizer(mapnik.Expression('[Field Name]'), 'DejaVu Sans Bold', 6, mapnik.Color('#000000'), mapnik.PathExpression('../#data/images/broken.png'))
+
+# def test_shield_symbolizer_modify():
+#     s = mapnik.ShieldSymbolizer(mapnik.Expression('[Field Name]'), 'DejaVu Sans Bold', 6, mapnik.Color('#000000'), mapnik.PathExpression('../data/images/dummy.png'))
+#     # transform expression
+#     def check_transform(expr, expect_str=None):
+#         s.transform = expr
+#         eq_(s.transform, expr if expect_str is None else expect_str)
+#     check_transform("matrix(1 2 3 4 5 6)", "matrix(1, 2, 3, 4, 5, 6)")
+#     check_transform("matrix(1, 2, 3, 4, 5, 6 +7)", "matrix(1, 2, 3, 4, 5, (6+7))")
+#     check_transform("rotate([a])")
+#     check_transform("rotate([a] -2)", "rotate(([a]-2))")
+#     check_transform("rotate([a] -2 -3)", "rotate([a], -2, -3)")
+#     check_transform("rotate([a] -2 -3 -4)", "rotate(((([a]-2)-3)-4))")
+#     check_transform("rotate([a] -2, 3, 4)", "rotate(([a]-2), 3, 4)")
+#     check_transform("translate([tx]) rotate([a])")
+#     check_transform("scale([sx], [sy]/2)")
+#     # TODO check expected failures
+
+# def test_point_symbolizer():
+#     p = mapnik.PointSymbolizer()
+#     eq_(p.filename,'')
+#     eq_(p.transform,'')
+#     eq_(p.opacity,1.0)
+#     eq_(p.allow_overlap,False)
+#     eq_(p.ignore_placement,False)
+#     eq_(p.comp_op,mapnik.CompositeOp.src_over)
+#     eq_(p.placement, mapnik.point_placement.CENTROID)
+
+#     p = mapnik.PointSymbolizer(mapnik.PathExpression("../data/images/dummy.png"))
+#     p.allow_overlap = True
+#     p.opacity = 0.5
+#     p.ignore_placement = True
+#     p.placement = mapnik.point_placement.INTERIOR
+#     eq_(p.allow_overlap, True)
+#     eq_(p.opacity, 0.5)
+#     eq_(p.filename,'../data/images/dummy.png')
+#     eq_(p.ignore_placement,True)
+#     eq_(p.placement, mapnik.point_placement.INTERIOR)
+
+# def test_markers_symbolizer():
+#     p = mapnik.MarkersSymbolizer()
+#     eq_(p.allow_overlap, False)
+#     eq_(p.opacity,1.0)
+#     eq_(p.fill_opacity,None)
+#     eq_(p.filename,'shape://ellipse')
+#     eq_(p.placement,mapnik.marker_placement.POINT_PLACEMENT)
+#     eq_(p.multi_policy,mapnik.marker_multi_policy.EACH)
+#     eq_(p.fill,None)
+#     eq_(p.ignore_placement,False)
+#     eq_(p.spacing,100)
+#     eq_(p.max_error,0.2)
+#     eq_(p.width,None)
+#     eq_(p.height,None)
+#     eq_(p.transform,'')
+#     eq_(p.clip,True)
+#     eq_(p.comp_op,mapnik.CompositeOp.src_over)
+
+
+#     p.width = mapnik.Expression('12')
+#     p.height = mapnik.Expression('12')
+#     eq_(str(p.width),'12')
+#     eq_(str(p.height),'12')
+
+#     p.width = mapnik.Expression('[field] + 2')
+#     p.height = mapnik.Expression('[field] + 2')
+#     eq_(str(p.width),'([field]+2)')
+#     eq_(str(p.height),'([field]+2)')
+
+#     stroke = mapnik.Stroke()
+#     stroke.color = mapnik.Color('black')
+#     stroke.width = 1.0
+
+#     p.stroke = stroke
+#     p.fill = mapnik.Color('white')
+#     p.allow_overlap = True
+#     p.opacity = 0.5
+#     p.fill_opacity = 0.5
+#     p.placement = mapnik.marker_placement.LINE_PLACEMENT
+#     p.multi_policy = mapnik.marker_multi_policy.WHOLE
+
+#     eq_(p.allow_overlap, True)
+#     eq_(p.opacity, 0.5)
+#     eq_(p.fill_opacity, 0.5)
+#     eq_(p.multi_policy,mapnik.marker_multi_policy.WHOLE)
+#     eq_(p.placement,mapnik.marker_placement.LINE_PLACEMENT)
+
+#     #https://github.com/mapnik/mapnik/issues/1285
+#     #https://github.com/mapnik/mapnik/issues/1427
+#     p.marker_type = 'arrow'
+#     eq_(p.marker_type,'shape://arrow')
+#     eq_(p.filename,'shape://arrow')
+
+
+# # PointSymbolizer missing image file
+# # images paths are now PathExpressions are evaluated at runtime
+# # so it does not make sense to throw...
+# #@raises(RuntimeError)
+# #def test_pointsymbolizer_missing_image():
+#  #   p = mapnik.PointSymbolizer(mapnik.PathExpression("../data/images/broken.png"))
+
+# def test_polygon_symbolizer():
+#     p = mapnik.PolygonSymbolizer()
+#     eq_(p.smooth,0.0)
+#     eq_(p.comp_op,mapnik.CompositeOp.src_over)
+#     eq_(p.clip,True)
+#     eq_(p.fill, mapnik.Color('gray'))
+#     eq_(p.fill_opacity, 1)
+
+#     p = mapnik.PolygonSymbolizer(mapnik.Color('blue'))
+
+#     eq_(p.fill, mapnik.Color('blue'))
+#     eq_(p.fill_opacity, 1)
+
+# def test_building_symbolizer_init():
+#     p = mapnik.BuildingSymbolizer()
+
+#     eq_(p.fill, mapnik.Color('gray'))
+#     eq_(p.fill_opacity, 1)
+#     eq_(p.height,None)
+
+# def test_group_symbolizer_init():
+#     s = mapnik.GroupSymbolizer()
+
+#     p = mapnik.GroupSymbolizerProperties()
+
+#     l = mapnik.PairLayout()
+#     l.item_margin = 5.0
+#     p.set_layout(l)
+
+#     r = mapnik.GroupRule(mapnik.Expression("[name%1]"))
+#     r.append(mapnik.PointSymbolizer())
+#     p.add_rule(r)
+#     s.symbolizer_properties = p
+
+#     eq_(s.comp_op,mapnik.CompositeOp.src_over)
+
+# def test_stroke_init():
+#     s = mapnik.Stroke()
+
+#     eq_(s.width, 1)
+#     eq_(s.opacity, 1)
+#     eq_(s.color, mapnik.Color('black'))
+#     eq_(s.line_cap, mapnik.line_cap.BUTT_CAP)
+#     eq_(s.line_join, mapnik.line_join.MITER_JOIN)
+#     eq_(s.gamma,1.0)
+
+#     s = mapnik.Stroke(mapnik.Color('blue'), 5.0)
+#     s.gamma = .5
+
+#     eq_(s.width, 5)
+#     eq_(s.opacity, 1)
+#     eq_(s.color, mapnik.Color('blue'))
+#     eq_(s.gamma, .5)
+#     eq_(s.line_cap, mapnik.line_cap.BUTT_CAP)
+#     eq_(s.line_join, mapnik.line_join.MITER_JOIN)
+
+# def test_stroke_dash_arrays():
+#     s = mapnik.Stroke()
+#     s.add_dash(1,2)
+#     s.add_dash(3,4)
+#     s.add_dash(5,6)
+
+#     eq_(s.get_dashes(), [(1,2),(3,4),(5,6)])
+
+# def test_map_init():
+#     m = mapnik.Map(256, 256)
+
+#     eq_(m.width, 256)
+#     eq_(m.height, 256)
+#     eq_(m.srs, '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+#     eq_(m.base, '')
+#     eq_(m.maximum_extent, None)
+#     eq_(m.background_image, None)
+#     eq_(m.background_image_comp_op, mapnik.CompositeOp.src_over)
+#     eq_(m.background_image_opacity, 1.0)
+
+#     m = mapnik.Map(256, 256, '+proj=latlong')
+#     eq_(m.srs, '+proj=latlong')
+
+# def test_map_style_access():
+#     m = mapnik.Map(256, 256)
+#     sty = mapnik.Style()
+#     m.append_style("style",sty)
+#     styles = list(m.styles)
+#     eq_(len(styles),1)
+#     eq_(styles[0][0],'style')
+#     # returns a copy so let's just check it is the right instance
+#     eq_(isinstance(styles[0][1],mapnik.Style),True)
+
+# def test_map_maximum_extent_modification():
+#     m = mapnik.Map(256, 256)
+#     eq_(m.maximum_extent, None)
+#     m.maximum_extent = mapnik.Box2d()
+#     eq_(m.maximum_extent, mapnik.Box2d())
+#     m.maximum_extent = None
+#     eq_(m.maximum_extent, None)
+
+# # Map initialization from string
+# def test_map_init_from_string():
+#     map_string = '''<Map background-color="steelblue" base="./" srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
+#      <Style name="My Style">
+#       <Rule>
+#        <PolygonSymbolizer fill="#f2eff9"/>
+#        <LineSymbolizer stroke="rgb(50%,50%,50%)" stroke-width="0.1"/>
+#       </Rule>
+#      </Style>
+#      <Layer name="boundaries">
+#       <StyleName>My Style</StyleName>
+#        <Datasource>
+#         <Parameter name="type">shape</Parameter>
+#         <Parameter name="file">../../demo/data/boundaries</Parameter>
+#        </Datasource>
+#       </Layer>
+#     </Map>'''
+
+#     m = mapnik.Map(600, 300)
+#     eq_(m.base, '')
+#     try:
+#         mapnik.load_map_from_string(m, map_string)
+#         eq_(m.base, './')
+#         mapnik.load_map_from_string(m, map_string, False, "") # this "" will have no effect
+#         eq_(m.base, './')
+
+#         tmp_dir = tempfile.gettempdir()
+#         try:
+#             mapnik.load_map_from_string(m, map_string, False, tmp_dir)
+#         except RuntimeError:
+#             pass # runtime error expected because shapefile path should be wrong and datasource will throw
+#         eq_(m.base, tmp_dir) # tmp_dir will be set despite the exception because load_map mostly worked
+#         m.base = 'foo'
+#         mapnik.load_map_from_string(m, map_string, True, ".")
+#         eq_(m.base, '.')
+#     except RuntimeError, e:
+#         # only test datasources that we have installed
+#         if not 'Could not create datasource' in str(e):
+#             raise RuntimeError(e)
+
+# # Color initialization
+# @raises(Exception) # Boost.Python.ArgumentError
+# def test_color_init_errors():
+#     c = mapnik.Color()
+
+# @raises(RuntimeError)
+# def test_color_init_errors():
+#     c = mapnik.Color('foo') # mapnik config
+
+# def test_color_init():
+#     c = mapnik.Color('blue')
+
+#     eq_(c.a, 255)
+#     eq_(c.r, 0)
+#     eq_(c.g, 0)
+#     eq_(c.b, 255)
+
+#     eq_(c.to_hex_string(), '#0000ff')
+
+#     c = mapnik.Color('#f2eff9')
+
+#     eq_(c.a, 255)
+#     eq_(c.r, 242)
+#     eq_(c.g, 239)
+#     eq_(c.b, 249)
+
+#     eq_(c.to_hex_string(), '#f2eff9')
+
+#     c = mapnik.Color('rgb(50%,50%,50%)')
+
+#     eq_(c.a, 255)
+#     eq_(c.r, 128)
+#     eq_(c.g, 128)
+#     eq_(c.b, 128)
+
+#     eq_(c.to_hex_string(), '#808080')
+
+#     c = mapnik.Color(0, 64, 128)
+
+#     eq_(c.a, 255)
+#     eq_(c.r, 0)
+#     eq_(c.g, 64)
+#     eq_(c.b, 128)
+
+#     eq_(c.to_hex_string(), '#004080')
+
+#     c = mapnik.Color(0, 64, 128, 192)
+
+#     eq_(c.a, 192)
+#     eq_(c.r, 0)
+#     eq_(c.g, 64)
+#     eq_(c.b, 128)
+
+#     eq_(c.to_hex_string(), '#004080c0')
+
+# def test_color_equality():
+
+#     c1 = mapnik.Color('blue')
+#     c2 = mapnik.Color(0,0,255)
+#     c3 = mapnik.Color('black')
+
+#     c3.r = 0
+#     c3.g = 0
+#     c3.b = 255
+#     c3.a = 255
+
+#     eq_(c1, c2)
+#     eq_(c1, c3)
+
+#     c1 = mapnik.Color(0, 64, 128)
+#     c2 = mapnik.Color(0, 64, 128)
+#     c3 = mapnik.Color(0, 0, 0)
+
+#     c3.r = 0
+#     c3.g = 64
+#     c3.b = 128
+
+#     eq_(c1, c2)
+#     eq_(c1, c3)
+
+#     c1 = mapnik.Color(0, 64, 128, 192)
+#     c2 = mapnik.Color(0, 64, 128, 192)
+#     c3 = mapnik.Color(0, 0, 0, 255)
+
+#     c3.r = 0
+#     c3.g = 64
+#     c3.b = 128
+#     c3.a = 192
+
+#     eq_(c1, c2)
+#     eq_(c1, c3)
+
+#     c1 = mapnik.Color('rgb(50%,50%,50%)')
+#     c2 = mapnik.Color(128, 128, 128, 255)
+#     c3 = mapnik.Color('#808080')
+#     c4 = mapnik.Color('gray')
+
+#     eq_(c1, c2)
+#     eq_(c1, c3)
+#     eq_(c1, c4)
+
+#     c1 = mapnik.Color('hsl(0, 100%, 50%)')   # red
+#     c2 = mapnik.Color('hsl(120, 100%, 50%)') # lime
+#     c3 = mapnik.Color('hsla(240, 100%, 50%, 0.5)') # semi-transparent solid blue
+
+#     eq_(c1, mapnik.Color('red'))
+#     eq_(c2, mapnik.Color('lime'))
+#     eq_(c3, mapnik.Color(0,0,255,128))
+
+# def test_rule_init():
+#     min_scale = 5
+#     max_scale = 10
+
+#     r = mapnik.Rule()
+
+#     eq_(r.name, '')
+#     eq_(r.min_scale, 0)
+#     eq_(r.max_scale, float('inf'))
+#     eq_(r.has_else(), False)
+#     eq_(r.has_also(), False)
+
+#     r = mapnik.Rule()
+
+#     r.set_else(True)
+#     eq_(r.has_else(), True)
+#     eq_(r.has_also(), False)
+
+#     r = mapnik.Rule()
+
+#     r.set_also(True)
+#     eq_(r.has_else(), False)
+#     eq_(r.has_also(), True)
+
+#     r = mapnik.Rule("Name")
+
+#     eq_(r.name, 'Name')
+#     eq_(r.min_scale, 0)
+#     eq_(r.max_scale, float('inf'))
+#     eq_(r.has_else(), False)
+#     eq_(r.has_also(), False)
+
+#     r = mapnik.Rule("Name")
+
+#     eq_(r.name, 'Name')
+#     eq_(r.min_scale, 0)
+#     eq_(r.max_scale, float('inf'))
+#     eq_(r.has_else(), False)
+#     eq_(r.has_also(), False)
+
+#     r = mapnik.Rule("Name", min_scale)
+
+#     eq_(r.name, 'Name')
+#     eq_(r.min_scale, min_scale)
+#     eq_(r.max_scale, float('inf'))
+#     eq_(r.has_else(), False)
+#     eq_(r.has_also(), False)
+
+#     r = mapnik.Rule("Name", min_scale, max_scale)
+
+#     eq_(r.name, 'Name')
+#     eq_(r.min_scale, min_scale)
+#     eq_(r.max_scale, max_scale)
+#     eq_(r.has_else(), False)
+#     eq_(r.has_also(), False)
+
+# if __name__ == "__main__":
+#     setup()
+#     run_all(eval(x) for x in dir() if x.startswith("test_"))
diff --git a/test/python_tests/ogr_and_shape_geometries_test.py b/test/python_tests/ogr_and_shape_geometries_test.py
new file mode 100644
index 0000000..5c6918e
--- /dev/null
+++ b/test/python_tests/ogr_and_shape_geometries_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+# TODO - fix truncation in shapefile...
+polys = ["POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))",
+         "POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))",
+         "MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))"
+         "MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 45 20, 30 5, 10 10, 10 30, 20 35),(30 20, 20 25, 20 15, 30 20)))"
+        ]
+
+plugins = mapnik.DatasourceCache.plugin_names()
+if 'shape' in plugins and 'ogr' in plugins:
+
+    def ensure_geometries_are_interpreted_equivalently(filename):
+        ds1 = mapnik.Ogr(file=filename,layer_by_index=0)
+        ds2 = mapnik.Shapefile(file=filename)
+        fs1 = ds1.featureset()
+        fs2 = ds2.featureset()
+        count = 0;
+        import itertools
+        for feat1,feat2 in itertools.izip(fs1, fs2):
+            count += 1
+            eq_(feat1.attributes,feat2.attributes)
+            # TODO - revisit this: https://github.com/mapnik/mapnik/issues/1093
+            # eq_(feat1.to_geojson(),feat2.to_geojson())
+            #eq_(feat1.geometries().to_wkt(),feat2.geometries().to_wkt())
+            #eq_(feat1.geometries().to_wkb(mapnik.wkbByteOrder.NDR),feat2.geometries().to_wkb(mapnik.wkbByteOrder.NDR))
+            #eq_(feat1.geometries().to_wkb(mapnik.wkbByteOrder.XDR),feat2.geometries().to_wkb(mapnik.wkbByteOrder.XDR))
+
+    def test_simple_polys():
+        ensure_geometries_are_interpreted_equivalently('../data/shp/wkt_poly.shp')
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/ogr_test.py b/test/python_tests/ogr_test.py
new file mode 100644
index 0000000..905eda2
--- /dev/null
+++ b/test/python_tests/ogr_test.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal,raises
+from utilities import execution_path, run_all
+import os, mapnik
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'ogr' in mapnik.DatasourceCache.plugin_names():
+
+    # Shapefile initialization
+    def test_shapefile_init():
+        ds = mapnik.Ogr(file='../data/shp/boundaries.shp',layer_by_index=0)
+        e = ds.envelope()
+        assert_almost_equal(e.minx, -11121.6896651, places=7)
+        assert_almost_equal(e.miny, -724724.216526, places=6)
+        assert_almost_equal(e.maxx, 2463000.67866, places=5)
+        assert_almost_equal(e.maxy, 1649661.267, places=3)
+        meta = ds.describe()
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+        eq_('+proj=lcc' in meta['proj4'],True)
+
+    # Shapefile properties
+    def test_shapefile_properties():
+        ds = mapnik.Ogr(file='../data/shp/boundaries.shp',layer_by_index=0)
+        f = ds.features_at_point(ds.envelope().center(), 0.001).features[0]
+        eq_(ds.geometry_type(),mapnik.DataGeometryType.Polygon)
+
+        eq_(f['CGNS_FID'], u'6f733341ba2011d892e2080020a0f4c9')
+        eq_(f['COUNTRY'], u'CAN')
+        eq_(f['F_CODE'], u'FA001')
+        eq_(f['NAME_EN'], u'Quebec')
+        eq_(f['Shape_Area'], 1512185733150.0)
+        eq_(f['Shape_Leng'], 19218883.724300001)
+        meta = ds.describe()
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+        # NOTE: encoding is latin1 but gdal >= 1.9 should now expose utf8 encoded features
+        # See SHAPE_ENCODING for overriding: http://gdal.org/ogr/drv_shapefile.html
+        # Failure for the NOM_FR field is expected for older gdal
+        #eq_(f['NOM_FR'], u'Qu\xe9bec')
+        #eq_(f['NOM_FR'], u'Québec')
+
+    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0)
+        eq_(len(ds.fields()),11)
+        eq_(ds.fields(),['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT'])
+        eq_(ds.field_types(),['str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        # also add an invalid one, triggering throw
+        query.add_property_name('bogus')
+        ds.features(query)
+
+    # disabled because OGR prints an annoying error: ERROR 1: Invalid Point object. Missing 'coordinates' member.
+    #def test_handling_of_null_features():
+    #    ds = mapnik.Ogr(file='../data/json/null_feature.geojson',layer_by_index=0)
+    #    fs = ds.all_features()
+    #    eq_(len(fs),1)
+
+    # OGR plugin extent parameter
+    def test_ogr_extent_parameter():
+        ds = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0,extent='-1,-1,1,1')
+        e = ds.envelope()
+        eq_(e.minx,-1)
+        eq_(e.miny,-1)
+        eq_(e.maxx,1)
+        eq_(e.maxy,1)
+        meta = ds.describe()
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+        eq_('+proj=merc' in meta['proj4'],True)
+
+    def test_ogr_reading_gpx_waypoint():
+        ds = mapnik.Ogr(file='../data/gpx/empty.gpx',layer='waypoints')
+        e = ds.envelope()
+        eq_(e.minx,-122)
+        eq_(e.miny,48)
+        eq_(e.maxx,-122)
+        eq_(e.maxy,48)
+        meta = ds.describe()
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_('+proj=longlat' in meta['proj4'],True)
+
+    def test_ogr_empty_data_should_not_throw():
+        default_logging_severity = mapnik.logger.get_severity()
+        mapnik.logger.set_severity(mapnik.severity_type.None)
+        # use logger to silence expected warnings
+        for layer in ['routes', 'tracks', 'route_points', 'track_points']:
+            ds = mapnik.Ogr(file='../data/gpx/empty.gpx',layer=layer)
+            e = ds.envelope()
+            eq_(e.minx,0)
+            eq_(e.miny,0)
+            eq_(e.maxx,0)
+            eq_(e.maxy,0)
+        mapnik.logger.set_severity(default_logging_severity)
+        meta = ds.describe()
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+        eq_('+proj=longlat' in meta['proj4'],True)
+
+    # disabled because OGR prints an annoying error: ERROR 1: Invalid Point object. Missing 'coordinates' member.
+    #def test_handling_of_null_features():
+    #    ds = mapnik.Ogr(file='../data/json/null_feature.geojson',layer_by_index=0)
+    #    fs = ds.all_features()
+    #    eq_(len(fs),1)
+
+    def test_geometry_type():
+        ds = mapnik.Ogr(file='../data/csv/wkt.csv',layer_by_index=0)
+        e = ds.envelope()
+        assert_almost_equal(e.minx, 1.0, places=1)
+        assert_almost_equal(e.miny, 1.0, places=1)
+        assert_almost_equal(e.maxx, 45.0, places=1)
+        assert_almost_equal(e.maxy, 45.0, places=1)
+        meta = ds.describe()
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+        #eq_('+proj=longlat' in meta['proj4'],True)
+        fs = ds.featureset()
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'Point', u'coordinates': [30, 10]}, u'type': u'Feature', u'id': 2, u'properties': {u'type': u'point', u'WKT': u'           POINT (30 10)'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'LineString', u'coordinates': [[30, 10], [10, 30], [40, 40]]}, u'type': u'Feature', u'id': 3, u'properties': {u'type': u'linestring', u'WKT': u'      LINESTRING (30 10, 10 30, 40 40)'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'Polygon', u'coordinates': [[[30, 10], [40, 40], [20, 40], [10, 20], [30, 10]]]}, u'type': u'Feature', u'id': 4, u'properties': {u'type': u'polygon', u'WKT': u'         POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'Polygon', u'coordinates': [[[35, 10], [45, 45], [15, 40], [10, 20], [35, 10]], [[20, 30], [35, 35], [30, 20], [20, 30]]]}, u'type': u'Feature', u'id': 5, u'properties': {u'type': u'polygon', u'WKT': u'         POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'MultiPoint', u'coordinates': [[10, 40], [40, 30], [20, 20], [30, 10]]}, u'type': u'Feature', u'id': 6, u'properties': {u'type': u'multipoint', u'WKT': u'      MULTIPOINT ((10 40), (40 30), (20 20), (30 10))'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'MultiLineString', u'coordinates': [[[10, 10], [20, 20], [10, 40]], [[40, 40], [30, 30], [40, 20], [30, 10]]]}, u'type': u'Feature', u'id': 7, u'properties': {u'type': u'multilinestring', u'WKT': u' MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'MultiPolygon', u'coordinates': [[[[30, 20], [45, 40], [10, 40], [30, 20]]], [[[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]]]]}, u'type': u'Feature', u'id': 8, u'properties': {u'type': u'multipolygon', u'WKT': u'    MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'MultiPolygon', u'coordinates': [[[[40, 40], [20, 45], [45, 30], [40, 40]]], [[[20, 35], [10, 30], [10, 10], [30, 5], [45, 20], [20, 35]], [[30, 20], [20, 15], [20, 25], [30, 20]]]]}, u'type': u'Feature', u'id': 9, u'properties': {u'type': u'multipolygon', u'WKT': u'    MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 45 20, 30 5, 10 10, 10 30, 20 35),(30 20, 20 25, 20 15, 30 20)))'}})
+        feat = fs.next()
+        actual = json.loads(feat.to_geojson())
+        eq_(actual,{u'geometry': {u'type': u'GeometryCollection', u'geometries': [{u'type': u'Polygon', u'coordinates': [[[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]]]}, {u'type': u'Point', u'coordinates': [2, 3]}, {u'type': u'LineString', u'coordinates': [[2, 3], [3, 4]]}]}, u'type': u'Feature', u'id': 10, u'properties': {u'type': u'collection', u'WKT': u'      GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))'}})
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/osm_test.py b/test/python_tests/osm_test.py
new file mode 100644
index 0000000..b9f5196
--- /dev/null
+++ b/test/python_tests/osm_test.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'osm' in mapnik.DatasourceCache.plugin_names():
+
+    # osm initialization
+    def test_osm_init():
+        ds = mapnik.Osm(file='../data/osm/nodes.osm')
+
+        e = ds.envelope()
+
+        # these are hardcoded in the plugin… ugh
+        eq_(e.minx >= -180.0,True)
+        eq_(e.miny >= -90.0,True)
+        eq_(e.maxx <= 180.0,True)
+        eq_(e.maxy <= 90,True)
+
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.Osm(file='../data/osm/nodes.osm')
+        eq_(len(ds.fields()),0)
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        # also add an invalid one, triggering throw
+        query.add_property_name('bogus')
+        ds.features(query)
+
+    def test_that_64bit_int_fields_work():
+        ds = mapnik.Osm(file='../data/osm/64bit.osm')
+        eq_(len(ds.fields()),4)
+        eq_(ds.fields(),['bigint', 'highway', 'junction', 'note'])
+        eq_(ds.field_types(),['str', 'str', 'str', 'str'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat.to_geojson(),'{"type":"Feature","id":890,"geometry":{"type":"Point","coordinates":[-61.7960248,17.1415874]},"properties":{}}')
+        eq_(feat.id(),4294968186)
+        eq_(feat['bigint'], None)
+        feat = fs.next()
+        eq_(feat['bigint'],'9223372036854775807')
+
+    def test_reading_ways():
+        ds = mapnik.Osm(file='../data/osm/ways.osm')
+        eq_(len(ds.fields()),0)
+        eq_(ds.fields(),[])
+        eq_(ds.field_types(),[])
+        feat = ds.all_features()[4]
+        eq_(feat.to_geojson(),'{"type":"Feature","id":1,"geometry":{"type":"LineString","coordinates":[[0,2],[0,-2]]},"properties":{}}')
+        eq_(feat.id(),1)
+
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/palette_test.py b/test/python_tests/palette_test.py
new file mode 100644
index 0000000..9b30895
--- /dev/null
+++ b/test/python_tests/palette_test.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+expected_64 = '[Palette 64 colors #494746 #c37631 #89827c #d1955c #7397b9 #fc9237 #a09f9c #fbc147 #9bb3ce #b7c9a1 #b5d29c #c4b9aa #cdc4a5 #d5c8a3 #c1d7aa #ccc4b6 #dbd19c #b2c4d5 #eae487 #c9c8c6 #e4db99 #c9dcb5 #dfd3ac #cbd2c2 #d6cdbc #dbd2b6 #c0ceda #ece597 #f7ef86 #d7d3c3 #dfcbc3 #d1d0cd #d1e2bf #d3dec1 #dbd3c4 #e6d8b6 #f4ef91 #d3d3cf #cad5de #ded7c9 #dfdbce #fcf993 #ffff8a #dbd9d7 #dbe7cd #d4dce2 #e4ded3 #ebe3c9 #e0e2e2 #f4edc3 #fdfcae #e9e5dc #f4edda #eeebe4 #fefdc5 #e7edf2 #edf4e5 #f [...]
+
+expected_256 = '[Palette 256 colors #272727 #3c3c3c #484847 #564b41 #605243 #6a523e #555555 #785941 #5d5d5d #746856 #676767 #956740 #ba712e #787777 #cb752a #c27c3d #b68049 #dc8030 #df9e10 #878685 #e1a214 #928b82 #a88a70 #ea8834 #e7a81d #cb8d55 #909090 #94938c #e18f48 #f68d36 #6f94b7 #e1ab2e #8e959b #c79666 #999897 #ff9238 #ef9447 #a99a88 #f1b32c #919ca6 #a1a09f #f0b04b #8aa4bf #f8bc39 #b3ac8f #d1a67a #e3b857 #a8a8a7 #ffc345 #a2adb9 #afaeab #f9ab69 #afbba4 #c4c48a #b4b2af #dec177 #9ab2cf  [...]
+
+expected_rgb = '[Palette 2 colors #ff00ff #ffffff]'
+
+def test_reading_palettes():
+    act = open('../data/palettes/palette64.act','rb')
+    palette = mapnik.Palette(act.read(),'act')
+    eq_(palette.to_string(),expected_64);
+    act = open('../data/palettes/palette256.act','rb')
+    palette = mapnik.Palette(act.read(),'act')
+    eq_(palette.to_string(),expected_256);
+    palette = mapnik.Palette('\xff\x00\xff\xff\xff\xff', 'rgb')
+    eq_(palette.to_string(),expected_rgb);
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+    def test_render_with_palette():
+        m = mapnik.Map(600,400)
+        mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml')
+        m.zoom_all()
+        im = mapnik.Image(m.width,m.height)
+        mapnik.render(m,im)
+        act = open('../data/palettes/palette256.act','rb')
+        palette = mapnik.Palette(act.read(),'act')
+        # test saving directly to filesystem
+        im.save('/tmp/mapnik-palette-test.png','png',palette)
+        expected = './images/support/mapnik-palette-test.png'
+        if os.environ.get('UPDATE'):
+            im.save(expected,"png",palette);
+
+        # test saving to a string
+        open('/tmp/mapnik-palette-test2.png','wb').write(im.tostring('png',palette));
+        # compare the two methods
+        eq_(mapnik.Image.open('/tmp/mapnik-palette-test.png').tostring('png32'),mapnik.Image.open('/tmp/mapnik-palette-test2.png').tostring('png32'),'%s not eq to %s' % ('/tmp/mapnik-palette-test.png','/tmp/mapnik-palette-test2.png'))
+        # compare to expected
+        eq_(mapnik.Image.open('/tmp/mapnik-palette-test.png').tostring('png32'),mapnik.Image.open(expected).tostring('png32'),'%s not eq to %s' % ('/tmp/mapnik-palette-test.png',expected))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/parameters_test.py b/test/python_tests/parameters_test.py
new file mode 100644
index 0000000..1587fbd
--- /dev/null
+++ b/test/python_tests/parameters_test.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import mapnik
+
+def setup():
+    os.chdir(execution_path('.'))
+
+def test_parameter_null():
+    p = mapnik.Parameter('key',None)
+    eq_(p[0],'key')
+    eq_(p[1],None)
+
+def test_parameter_string():
+    p = mapnik.Parameter('key','value')
+    eq_(p[0],'key')
+    eq_(p[1],'value')
+
+def test_parameter_unicode():
+    p = mapnik.Parameter('key',u'value')
+    eq_(p[0],'key')
+    eq_(p[1],u'value')
+
+def test_parameter_integer():
+    p = mapnik.Parameter('int',sys.maxint)
+    eq_(p[0],'int')
+    eq_(p[1],sys.maxint)
+
+def test_parameter_double():
+    p = mapnik.Parameter('double',float(sys.maxint))
+    eq_(p[0],'double')
+    eq_(p[1],float(sys.maxint))
+
+def test_parameter_boolean():
+    p = mapnik.Parameter('boolean',True)
+    eq_(p[0],'boolean')
+    eq_(p[1],True)
+    eq_(bool(p[1]),True)
+
+
+def test_parameters():
+    params = mapnik.Parameters()
+    p = mapnik.Parameter('float',1.0777)
+    eq_(p[0],'float')
+    eq_(p[1],1.0777)
+
+    params.append(p)
+
+    eq_(params[0][0],'float')
+    eq_(params[0][1],1.0777)
+
+    eq_(params.get('float'),1.0777)
+
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/pgraster_test.py b/test/python_tests/pgraster_test.py
new file mode 100644
index 0000000..dc7584f
--- /dev/null
+++ b/test/python_tests/pgraster_test.py
@@ -0,0 +1,763 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,assert_almost_equal
+import atexit
+import time
+from utilities import execution_path, run_all, side_by_side_image
+from subprocess import Popen, PIPE
+import os, mapnik
+import sys
+import re
+from binascii import hexlify
+
+MAPNIK_TEST_DBNAME = 'mapnik-tmp-pgraster-test-db'
+POSTGIS_TEMPLATE_DBNAME = 'template_postgis'
+DEBUG_OUTPUT=False
+
+def log(msg):
+    if DEBUG_OUTPUT:
+      print msg
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def call(cmd,silent=False):
+    stdin, stderr = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+    if not stderr:
+        return stdin.strip()
+    elif not silent and 'error' in stderr.lower() \
+        or 'not found' in stderr.lower() \
+        or 'could not connect' in stderr.lower() \
+        or 'bad connection' in stderr.lower() \
+        or 'not recognized as an internal' in stderr.lower():
+        raise RuntimeError(stderr.strip())
+
+def psql_can_connect():
+    """Test ability to connect to a postgis template db with no options.
+
+    Basically, to run these tests your user must have full read
+    access over unix sockets without supplying a password. This
+    keeps these tests simple and focused on postgis not on postgres
+    auth issues.
+    """
+    try:
+        call('psql %s -c "select postgis_version()"' % POSTGIS_TEMPLATE_DBNAME)
+        return True
+    except RuntimeError:
+        print 'Notice: skipping pgraster tests (connection)'
+        return False
+
+def psql_run(cmd):
+  cmd = 'psql --set ON_ERROR_STOP=1 %s -c "%s"' % \
+    (MAPNIK_TEST_DBNAME, cmd.replace('"', '\\"'))
+  log('DEBUG: running ' + cmd)
+  call(cmd)
+
+def raster2pgsql_on_path():
+    """Test for presence of raster2pgsql on the user path.
+
+    We require this program to load test data into a temporarily database.
+    """
+    try:
+        call('raster2pgsql')
+        return True
+    except RuntimeError:
+        print 'Notice: skipping pgraster tests (raster2pgsql)'
+        return False
+
+def createdb_and_dropdb_on_path():
+    """Test for presence of dropdb/createdb on user path.
+
+    We require these programs to setup and teardown the testing db.
+    """
+    try:
+        call('createdb --help')
+        call('dropdb --help')
+        return True
+    except RuntimeError:
+        print 'Notice: skipping pgraster tests (createdb/dropdb)'
+        return False
+
+def postgis_setup():
+    call('dropdb %s' % MAPNIK_TEST_DBNAME,silent=True)
+    call('createdb -T %s %s' % (POSTGIS_TEMPLATE_DBNAME,MAPNIK_TEST_DBNAME),silent=False)
+
+def postgis_takedown():
+    pass
+    # fails as the db is in use: https://github.com/mapnik/mapnik/issues/960
+    #call('dropdb %s' % MAPNIK_TEST_DBNAME)
+
+def import_raster(filename, tabname, tilesize, constraint, overview):
+  log('tile: ' + tilesize + ' constraints: ' + str(constraint) \
+      + ' overviews: ' + overview)
+  cmd = 'raster2pgsql -Y -I -q'
+  if constraint:
+    cmd += ' -C'
+  if tilesize:
+    cmd += ' -t ' + tilesize
+  if overview:
+    cmd += ' -l ' + overview
+  cmd += ' %s %s | psql --set ON_ERROR_STOP=1 -q %s' % (os.path.abspath(os.path.normpath(filename)),tabname,MAPNIK_TEST_DBNAME)
+  log('Import call: ' + cmd)
+  call(cmd)
+
+def drop_imported(tabname, overview):
+  psql_run('DROP TABLE IF EXISTS "' + tabname + '";')
+  if overview:
+    for of in overview.split(','):
+      psql_run('DROP TABLE IF EXISTS "o_' + of + '_' + tabname + '";')
+
+def compare_images(expected,im):
+  expected = os.path.join(os.path.dirname(expected),os.path.basename(expected).replace(':','_'))
+  if not os.path.exists(expected) or os.environ.get('UPDATE'):
+    print 'generating expected image %s' % expected
+    im.save(expected,'png32')
+  expected_im = mapnik.Image.open(expected)
+  diff = expected.replace('.png','-diff.png')
+  if len(im.tostring("png32")) != len(expected_im.tostring("png32")):
+    compared = side_by_side_image(expected_im, im)
+    compared.save(diff)
+    assert False,'images do not match, check diff at %s' % diff
+  else:
+    if os.path.exists(diff): os.unlink(diff)
+  return True
+
+if 'pgraster' in mapnik.DatasourceCache.plugin_names() \
+        and createdb_and_dropdb_on_path() \
+        and psql_can_connect() \
+        and raster2pgsql_on_path():
+
+    # initialize test database
+    postgis_setup()
+
+    # [old]dataraster.tif, 2283x1913 int16 single-band
+    # dataraster-small.tif, 457x383 int16 single-band
+    def _test_dataraster_16bsi_rendering(lbl, overview, rescale, clip):
+      if rescale:
+        lbl += ' Sc'
+      if clip:
+        lbl += ' Cl'
+      ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME,table='"dataRaster"',
+        band=1,use_overviews=1 if overview else 0,
+        prescale_rasters=rescale,clip_rasters=clip)
+      fs = ds.featureset()
+      feature = fs.next()
+      eq_(feature['rid'],1)
+      lyr = mapnik.Layer('dataraster_16bsi')
+      lyr.datasource = ds
+      expenv = mapnik.Box2d(-14637, 3903178, 1126863, 4859678)
+      env = lyr.envelope()
+      # As the input size is a prime number both horizontally
+      # and vertically, we expect the extent of the overview
+      # tables to be a pixel wider than the original, whereas
+      # the pixel size in geographical units depends on the
+      # overview factor. So we start with the original pixel size
+      # as base scale and multiply by the overview factor.
+      # NOTE: the overview table extent only grows north and east
+      pixsize = 500 # see gdalinfo dataraster.tif
+      pixsize = 2497 # see gdalinfo dataraster-small.tif
+      tol = pixsize * max(overview.split(',')) if overview else 0
+      assert_almost_equal(env.minx, expenv.minx)
+      assert_almost_equal(env.miny, expenv.miny, delta=tol) 
+      assert_almost_equal(env.maxx, expenv.maxx, delta=tol)
+      assert_almost_equal(env.maxy, expenv.maxy)
+      mm = mapnik.Map(256, 256)
+      style = mapnik.Style()
+      col = mapnik.RasterColorizer();
+      col.default_mode = mapnik.COLORIZER_DISCRETE;
+      col.add_stop(0, mapnik.Color(0x40,0x40,0x40,255));
+      col.add_stop(10, mapnik.Color(0x80,0x80,0x80,255));
+      col.add_stop(20, mapnik.Color(0xa0,0xa0,0xa0,255));
+      sym = mapnik.RasterSymbolizer()
+      sym.colorizer = col
+      rule = mapnik.Rule()
+      rule.symbols.append(sym)
+      style.rules.append(rule)
+      mm.append_style('foo', style)
+      lyr.styles.append('foo')
+      mm.layers.append(lyr)
+      mm.zoom_to_box(expenv)
+      im = mapnik.Image(mm.width, mm.height)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+      # no data
+      eq_(im.view(1,1,1,1).tostring(), '\x00\x00\x00\x00') 
+      eq_(im.view(255,255,1,1).tostring(), '\x00\x00\x00\x00') 
+      eq_(im.view(195,116,1,1).tostring(), '\x00\x00\x00\x00') 
+      # A0A0A0
+      eq_(im.view(100,120,1,1).tostring(), '\xa0\xa0\xa0\xff')
+      eq_(im.view( 75, 80,1,1).tostring(), '\xa0\xa0\xa0\xff')
+      # 808080
+      eq_(im.view( 74,170,1,1).tostring(), '\x80\x80\x80\xff')
+      eq_(im.view( 30, 50,1,1).tostring(), '\x80\x80\x80\xff')
+      # 404040
+      eq_(im.view(190, 70,1,1).tostring(), '\x40\x40\x40\xff')
+      eq_(im.view(140,170,1,1).tostring(), '\x40\x40\x40\xff')
+
+      # Now zoom over a portion of the env (1/10)
+      newenv = mapnik.Box2d(273663,4024478,330738,4072303)
+      mm.zoom_to_box(newenv)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:1/10')
+      # nodata
+      eq_(hexlify(im.view(255,255,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(200,254,1,1).tostring()), '00000000')
+      # A0A0A0
+      eq_(hexlify(im.view(90,232,1,1).tostring()), 'a0a0a0ff')
+      eq_(hexlify(im.view(96,245,1,1).tostring()), 'a0a0a0ff')
+      # 808080
+      eq_(hexlify(im.view(1,1,1,1).tostring()), '808080ff') 
+      eq_(hexlify(im.view(128,128,1,1).tostring()), '808080ff') 
+      # 404040
+      eq_(hexlify(im.view(255, 0,1,1).tostring()), '404040ff')
+
+    def _test_dataraster_16bsi(lbl, tilesize, constraint, overview):
+      import_raster('../data/raster/dataraster-small.tif', 'dataRaster', tilesize, constraint, overview)
+      if constraint:
+        lbl += ' C'
+      if tilesize:
+        lbl += ' T:' + tilesize
+      if overview:
+        lbl += ' O:' + overview
+      for prescale in [0,1]:
+        for clip in [0,1]:
+          _test_dataraster_16bsi_rendering(lbl, overview, prescale, clip)
+      drop_imported('dataRaster', overview)
+
+    def test_dataraster_16bsi():
+      #for tilesize in ['','256x256']:
+      for tilesize in ['256x256']:
+        for constraint in [0,1]:
+          #for overview in ['','4','2,16']:
+          for overview in ['','2']:
+            _test_dataraster_16bsi('data_16bsi', tilesize, constraint, overview)
+
+    # river.tiff, RGBA 8BUI
+    def _test_rgba_8bui_rendering(lbl, overview, rescale, clip):
+      if rescale:
+        lbl += ' Sc'
+      if clip:
+        lbl += ' Cl'
+      ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME,table='(select * from "River") foo',
+        use_overviews=1 if overview else 0,
+        prescale_rasters=rescale,clip_rasters=clip)
+      fs = ds.featureset()
+      feature = fs.next()
+      eq_(feature['rid'],1)
+      lyr = mapnik.Layer('rgba_8bui')
+      lyr.datasource = ds
+      expenv = mapnik.Box2d(0, -210, 256, 0)
+      env = lyr.envelope()
+      # As the input size is a prime number both horizontally
+      # and vertically, we expect the extent of the overview
+      # tables to be a pixel wider than the original, whereas
+      # the pixel size in geographical units depends on the
+      # overview factor. So we start with the original pixel size
+      # as base scale and multiply by the overview factor.
+      # NOTE: the overview table extent only grows north and east
+      pixsize = 1 # see gdalinfo river.tif
+      tol = pixsize * max(overview.split(',')) if overview else 0
+      assert_almost_equal(env.minx, expenv.minx)
+      assert_almost_equal(env.miny, expenv.miny, delta=tol) 
+      assert_almost_equal(env.maxx, expenv.maxx, delta=tol)
+      assert_almost_equal(env.maxy, expenv.maxy)
+      mm = mapnik.Map(256, 256)
+      style = mapnik.Style()
+      sym = mapnik.RasterSymbolizer()
+      rule = mapnik.Rule()
+      rule.symbols.append(sym)
+      style.rules.append(rule)
+      mm.append_style('foo', style)
+      lyr.styles.append('foo')
+      mm.layers.append(lyr)
+      mm.zoom_to_box(expenv)
+      im = mapnik.Image(mm.width, mm.height)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+      expected = 'images/support/pgraster/%s-%s-%s-%s-box1.png' % (lyr.name,lbl,overview,clip)
+      compare_images(expected,im)
+      # no data
+      eq_(hexlify(im.view(3,3,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(250,250,1,1).tostring()), '00000000') 
+      # full opaque river color
+      eq_(hexlify(im.view(175,118,1,1).tostring()), 'b9d8f8ff') 
+      # half-transparent pixel
+      pxstr = hexlify(im.view(122,138,1,1).tostring())
+      apat = ".*(..)$"
+      match = re.match(apat, pxstr)
+      assert match, 'pixel ' + pxstr + ' does not match pattern ' + apat
+      alpha = match.group(1)
+      assert alpha != 'ff' and alpha != '00', \
+        'unexpected full transparent/opaque pixel: ' + alpha
+
+      # Now zoom over a portion of the env (1/10)
+      newenv = mapnik.Box2d(166,-105,191,-77)
+      mm.zoom_to_box(newenv)
+      t0 = time.time() # we want wall time to include IO waits
+      im = mapnik.Image(mm.width, mm.height)
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:1/10')
+      expected = 'images/support/pgraster/%s-%s-%s-%s-box2.png' % (lyr.name,lbl,overview,clip)
+      compare_images(expected,im)
+      # no data
+      eq_(hexlify(im.view(255,255,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(200,40,1,1).tostring()), '00000000')
+      # full opaque river color
+      eq_(hexlify(im.view(100,168,1,1).tostring()), 'b9d8f8ff')
+      # half-transparent pixel
+      pxstr = hexlify(im.view(122,138,1,1).tostring())
+      apat = ".*(..)$"
+      match = re.match(apat, pxstr)
+      assert match, 'pixel ' + pxstr + ' does not match pattern ' + apat
+      alpha = match.group(1)
+      assert alpha != 'ff' and alpha != '00', \
+        'unexpected full transparent/opaque pixel: ' + alpha
+
+    def _test_rgba_8bui(lbl, tilesize, constraint, overview):
+      import_raster('../data/raster/river.tiff', 'River', tilesize, constraint, overview)
+      if constraint:
+        lbl += ' C'
+      if tilesize:
+        lbl += ' T:' + tilesize
+      if overview:
+        lbl += ' O:' + overview
+      for prescale in [0,1]:
+        for clip in [0,1]:
+          _test_rgba_8bui_rendering(lbl, overview, prescale, clip)
+      drop_imported('River', overview)
+
+    def test_rgba_8bui():
+      for tilesize in ['','16x16']:
+        for constraint in [0,1]:
+          for overview in ['2']:
+            _test_rgba_8bui('rgba_8bui', tilesize, constraint, overview)
+
+    # nodata-edge.tif, RGB 8BUI
+    def _test_rgb_8bui_rendering(lbl, tnam, overview, rescale, clip):
+      if rescale:
+        lbl += ' Sc'
+      if clip:
+        lbl += ' Cl'
+      ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME,table=tnam,
+        use_overviews=1 if overview else 0,
+        prescale_rasters=rescale,clip_rasters=clip)
+      fs = ds.featureset()
+      feature = fs.next()
+      eq_(feature['rid'],1)
+      lyr = mapnik.Layer('rgba_8bui')
+      lyr.datasource = ds
+      expenv = mapnik.Box2d(-12329035.7652168,4508650.39854396, \
+                            -12328653.0279471,4508957.34625536)
+      env = lyr.envelope()
+      # As the input size is a prime number both horizontally
+      # and vertically, we expect the extent of the overview
+      # tables to be a pixel wider than the original, whereas
+      # the pixel size in geographical units depends on the
+      # overview factor. So we start with the original pixel size
+      # as base scale and multiply by the overview factor.
+      # NOTE: the overview table extent only grows north and east
+      pixsize = 2 # see gdalinfo nodata-edge.tif
+      tol = pixsize * max(overview.split(',')) if overview else 0
+      assert_almost_equal(env.minx, expenv.minx, places=0)
+      assert_almost_equal(env.miny, expenv.miny, delta=tol)
+      assert_almost_equal(env.maxx, expenv.maxx, delta=tol)
+      assert_almost_equal(env.maxy, expenv.maxy, places=0)
+      mm = mapnik.Map(256, 256)
+      style = mapnik.Style()
+      sym = mapnik.RasterSymbolizer()
+      rule = mapnik.Rule()
+      rule.symbols.append(sym)
+      style.rules.append(rule)
+      mm.append_style('foo', style)
+      lyr.styles.append('foo')
+      mm.layers.append(lyr)
+      mm.zoom_to_box(expenv)
+      im = mapnik.Image(mm.width, mm.height)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+      expected = 'images/support/pgraster/%s-%s-%s-%s-%s-box1.png' % (lyr.name,tnam,lbl,overview,clip)
+      compare_images(expected,im)
+      # no data
+      eq_(hexlify(im.view(3,16,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(128,16,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(250,16,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(3,240,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(128,240,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(250,240,1,1).tostring()), '00000000')
+      # dark brown
+      eq_(hexlify(im.view(174,39,1,1).tostring()), 'c3a698ff') 
+      # dark gray
+      eq_(hexlify(im.view(195,132,1,1).tostring()), '575f62ff') 
+      # Now zoom over a portion of the env (1/10)
+      newenv = mapnik.Box2d(-12329035.7652168, 4508926.651484220, \
+                            -12328997.49148983,4508957.34625536)
+      mm.zoom_to_box(newenv)
+      t0 = time.time() # we want wall time to include IO waits
+      im = mapnik.Image(mm.width, mm.height)
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:1/10')
+      expected = 'images/support/pgraster/%s-%s-%s-%s-%s-box2.png' % (lyr.name,tnam,lbl,overview,clip)
+      compare_images(expected,im)
+      # no data
+      eq_(hexlify(im.view(3,16,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(128,16,1,1).tostring()), '00000000')
+      eq_(hexlify(im.view(250,16,1,1).tostring()), '00000000')
+      # black
+      eq_(hexlify(im.view(3,42,1,1).tostring()), '000000ff')
+      eq_(hexlify(im.view(3,134,1,1).tostring()), '000000ff')
+      eq_(hexlify(im.view(3,244,1,1).tostring()), '000000ff')
+      # gray
+      eq_(hexlify(im.view(135,157,1,1).tostring()), '4e555bff')
+      # brown
+      eq_(hexlify(im.view(195,223,1,1).tostring()), 'f2cdbaff')
+
+    def _test_rgb_8bui(lbl, tilesize, constraint, overview):
+      tnam = 'nodataedge'
+      import_raster('../data/raster/nodata-edge.tif', tnam, tilesize, constraint, overview)
+      if constraint:
+        lbl += ' C'
+      if tilesize:
+        lbl += ' T:' + tilesize
+      if overview:
+        lbl += ' O:' + overview
+      for prescale in [0,1]:
+        for clip in [0,1]:
+          _test_rgb_8bui_rendering(lbl, tnam, overview, prescale, clip)
+      #drop_imported(tnam, overview)
+
+    def test_rgb_8bui():
+      for tilesize in ['64x64']:
+        for constraint in [1]:
+          for overview in ['']:
+            _test_rgb_8bui('rgb_8bui', tilesize, constraint, overview)
+
+    def _test_grayscale_subquery(lbl,pixtype,value):
+      #
+      #      3   8   13
+      #    +---+---+---+
+      #  3 | v | v | v |  NOTE: writes different color
+      #    +---+---+---+        in 13,8 and 8,13
+      #  8 | v | v | a |  
+      #    +---+---+---+  
+      # 13 | v | b | v |
+      #    +---+---+---+
+      #
+      val_a = value/3;
+      val_b = val_a*2;
+      sql = "(select 3 as i, " \
+            " ST_SetValues(" \
+            "  ST_SetValues(" \
+            "   ST_AsRaster(" \
+            "    ST_MakeEnvelope(0,0,14,14), " \
+            "    1.0, -1.0, '%s', %s" \
+            "   ), " \
+            "   11, 6, 4, 5, %s::float8" \
+            "  )," \
+            "  6, 11, 5, 4, %s::float8" \
+            " ) as \"R\"" \
+            ") as foo" % (pixtype,value, val_a, val_b)
+      rescale = 0
+      clip = 0
+      if rescale:
+        lbl += ' Sc'
+      if clip:
+        lbl += ' Cl'
+      ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME, table=sql,
+        raster_field='"R"', use_overviews=1,
+        prescale_rasters=rescale,clip_rasters=clip)
+      fs = ds.featureset()
+      feature = fs.next()
+      eq_(feature['i'],3)
+      lyr = mapnik.Layer('grayscale_subquery')
+      lyr.datasource = ds
+      expenv = mapnik.Box2d(0,0,14,14)
+      env = lyr.envelope()
+      assert_almost_equal(env.minx, expenv.minx, places=0)
+      assert_almost_equal(env.miny, expenv.miny, places=0)
+      assert_almost_equal(env.maxx, expenv.maxx, places=0)
+      assert_almost_equal(env.maxy, expenv.maxy, places=0)
+      mm = mapnik.Map(15, 15)
+      style = mapnik.Style()
+      sym = mapnik.RasterSymbolizer()
+      rule = mapnik.Rule()
+      rule.symbols.append(sym)
+      style.rules.append(rule)
+      mm.append_style('foo', style)
+      lyr.styles.append('foo')
+      mm.layers.append(lyr)
+      mm.zoom_to_box(expenv)
+      im = mapnik.Image(mm.width, mm.height)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+      expected = 'images/support/pgraster/%s-%s-%s-%s.png' % (lyr.name,lbl,pixtype,value)
+      compare_images(expected,im)
+      h = format(value, '02x')
+      hex_v = h+h+h+'ff'
+      h = format(val_a, '02x')
+      hex_a = h+h+h+'ff'
+      h = format(val_b, '02x')
+      hex_b = h+h+h+'ff'
+      eq_(hexlify(im.view( 3, 3,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 8, 3,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view(13, 3,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 3, 8,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 8, 8,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view(13, 8,1,1).tostring()), hex_a);
+      eq_(hexlify(im.view( 3,13,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 8,13,1,1).tostring()), hex_b);
+      eq_(hexlify(im.view(13,13,1,1).tostring()), hex_v);
+
+    def test_grayscale_2bui_subquery():
+      _test_grayscale_subquery('grayscale_2bui_subquery', '2BUI', 3)
+
+    def test_grayscale_4bui_subquery():
+      _test_grayscale_subquery('grayscale_4bui_subquery', '4BUI', 15)
+
+    def test_grayscale_8bui_subquery():
+      _test_grayscale_subquery('grayscale_8bui_subquery', '8BUI', 63)
+
+    def test_grayscale_8bsi_subquery():
+      # NOTE: we're using a positive integer because Mapnik
+      #       does not support negative data values anyway
+      _test_grayscale_subquery('grayscale_8bsi_subquery', '8BSI', 69)
+
+    def test_grayscale_16bui_subquery():
+      _test_grayscale_subquery('grayscale_16bui_subquery', '16BUI', 126)
+
+    def test_grayscale_16bsi_subquery():
+      # NOTE: we're using a positive integer because Mapnik
+      #       does not support negative data values anyway
+      _test_grayscale_subquery('grayscale_16bsi_subquery', '16BSI', 144)
+
+    def test_grayscale_32bui_subquery():
+      _test_grayscale_subquery('grayscale_32bui_subquery', '32BUI', 255)
+
+    def test_grayscale_32bsi_subquery():
+      # NOTE: we're using a positive integer because Mapnik
+      #       does not support negative data values anyway
+      _test_grayscale_subquery('grayscale_32bsi_subquery', '32BSI', 129)
+
+    def _test_data_subquery(lbl, pixtype, value):
+      #
+      #      3   8   13
+      #    +---+---+---+
+      #  3 | v | v | v |  NOTE: writes different values
+      #    +---+---+---+        in 13,8 and 8,13
+      #  8 | v | v | a |  
+      #    +---+---+---+  
+      # 13 | v | b | v |
+      #    +---+---+---+
+      #
+      val_a = value/3;
+      val_b = val_a*2;
+      sql = "(select 3 as i, " \
+            " ST_SetValues(" \
+            "  ST_SetValues(" \
+            "   ST_AsRaster(" \
+            "    ST_MakeEnvelope(0,0,14,14), " \
+            "    1.0, -1.0, '%s', %s" \
+            "   ), " \
+            "   11, 6, 5, 5, %s::float8" \
+            "  )," \
+            "  6, 11, 5, 5, %s::float8" \
+            " ) as \"R\"" \
+            ") as foo" % (pixtype,value, val_a, val_b)
+      overview = ''
+      rescale = 0
+      clip = 0
+      if rescale:
+        lbl += ' Sc'
+      if clip:
+        lbl += ' Cl'
+      ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME, table=sql,
+        raster_field='R', use_overviews=0 if overview else 0,
+        band=1, prescale_rasters=rescale, clip_rasters=clip)
+      fs = ds.featureset()
+      feature = fs.next()
+      eq_(feature['i'],3)
+      lyr = mapnik.Layer('data_subquery')
+      lyr.datasource = ds
+      expenv = mapnik.Box2d(0,0,14,14)
+      env = lyr.envelope()
+      assert_almost_equal(env.minx, expenv.minx, places=0)
+      assert_almost_equal(env.miny, expenv.miny, places=0)
+      assert_almost_equal(env.maxx, expenv.maxx, places=0)
+      assert_almost_equal(env.maxy, expenv.maxy, places=0)
+      mm = mapnik.Map(15, 15)
+      style = mapnik.Style()
+      col = mapnik.RasterColorizer();
+      col.default_mode = mapnik.COLORIZER_DISCRETE;
+      col.add_stop(val_a, mapnik.Color(0xff,0x00,0x00,255));
+      col.add_stop(val_b, mapnik.Color(0x00,0xff,0x00,255));
+      col.add_stop(value, mapnik.Color(0x00,0x00,0xff,255));
+      sym = mapnik.RasterSymbolizer()
+      sym.colorizer = col
+      rule = mapnik.Rule()
+      rule.symbols.append(sym)
+      style.rules.append(rule)
+      mm.append_style('foo', style)
+      lyr.styles.append('foo')
+      mm.layers.append(lyr)
+      mm.zoom_to_box(expenv)
+      im = mapnik.Image(mm.width, mm.height)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+      expected = 'images/support/pgraster/%s-%s-%s-%s.png' % (lyr.name,lbl,pixtype,value)
+      compare_images(expected,im)
+
+    def test_data_2bui_subquery():
+      _test_data_subquery('data_2bui_subquery', '2BUI', 3)
+
+    def test_data_4bui_subquery():
+      _test_data_subquery('data_4bui_subquery', '4BUI', 15)
+
+    def test_data_8bui_subquery():
+      _test_data_subquery('data_8bui_subquery', '8BUI', 63)
+
+    def test_data_8bsi_subquery():
+      # NOTE: we're using a positive integer because Mapnik
+      #       does not support negative data values anyway
+      _test_data_subquery('data_8bsi_subquery', '8BSI', 69)
+
+    def test_data_16bui_subquery():
+      _test_data_subquery('data_16bui_subquery', '16BUI', 126)
+
+    def test_data_16bsi_subquery():
+      # NOTE: we're using a positive integer because Mapnik
+      #       does not support negative data values anyway
+      _test_data_subquery('data_16bsi_subquery', '16BSI', 135)
+
+    def test_data_32bui_subquery():
+      _test_data_subquery('data_32bui_subquery', '32BUI', 255)
+
+    def test_data_32bsi_subquery():
+      # NOTE: we're using a positive integer because Mapnik
+      #       does not support negative data values anyway
+      _test_data_subquery('data_32bsi_subquery', '32BSI', 264)
+
+    def test_data_32bf_subquery():
+      _test_data_subquery('data_32bf_subquery', '32BF', 450)
+
+    def test_data_64bf_subquery():
+      _test_data_subquery('data_64bf_subquery', '64BF', 3072)
+
+    def _test_rgba_subquery(lbl, pixtype, r, g, b, a, g1, b1):
+      #
+      #      3   8   13
+      #    +---+---+---+
+      #  3 | v | v | h |  NOTE: writes different alpha
+      #    +---+---+---+        in 13,8 and 8,13
+      #  8 | v | v | a |  
+      #    +---+---+---+  
+      # 13 | v | b | v |
+      #    +---+---+---+
+      #
+      sql = "(select 3 as i, " \
+            " ST_SetValues(" \
+            "  ST_SetValues(" \
+            "   ST_AddBand(" \
+            "    ST_AddBand(" \
+            "     ST_AddBand(" \
+            "      ST_AsRaster(" \
+            "       ST_MakeEnvelope(0,0,14,14), " \
+            "       1.0, -1.0, '%s', %s" \
+            "      )," \
+            "      '%s', %d::float" \
+            "     ), " \
+            "     '%s', %d::float" \
+            "    ), " \
+            "    '%s', %d::float" \
+            "   ), " \
+            "   2, 11, 6, 4, 5, %s::float8" \
+            "  )," \
+            "  3, 6, 11, 5, 4, %s::float8" \
+            " ) as r" \
+            ") as foo" % (pixtype, r, pixtype, g, pixtype, b, pixtype, a, g1, b1)
+      overview = ''
+      rescale = 0
+      clip = 0
+      if rescale:
+        lbl += ' Sc'
+      if clip:
+        lbl += ' Cl'
+      ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME, table=sql,
+        raster_field='r', use_overviews=0 if overview else 0,
+        prescale_rasters=rescale, clip_rasters=clip)
+      fs = ds.featureset()
+      feature = fs.next()
+      eq_(feature['i'],3)
+      lyr = mapnik.Layer('rgba_subquery')
+      lyr.datasource = ds
+      expenv = mapnik.Box2d(0,0,14,14)
+      env = lyr.envelope()
+      assert_almost_equal(env.minx, expenv.minx, places=0)
+      assert_almost_equal(env.miny, expenv.miny, places=0)
+      assert_almost_equal(env.maxx, expenv.maxx, places=0)
+      assert_almost_equal(env.maxy, expenv.maxy, places=0)
+      mm = mapnik.Map(15, 15)
+      style = mapnik.Style()
+      sym = mapnik.RasterSymbolizer()
+      rule = mapnik.Rule()
+      rule.symbols.append(sym)
+      style.rules.append(rule)
+      mm.append_style('foo', style)
+      lyr.styles.append('foo')
+      mm.layers.append(lyr)
+      mm.zoom_to_box(expenv)
+      im = mapnik.Image(mm.width, mm.height)
+      t0 = time.time() # we want wall time to include IO waits
+      mapnik.render(mm, im)
+      lap = time.time() - t0
+      log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+      expected = 'images/support/pgraster/%s-%s-%s-%s-%s-%s-%s-%s-%s.png' % (lyr.name,lbl, pixtype, r, g, b, a, g1, b1)
+      compare_images(expected,im)
+      hex_v = format(r << 24 | g  << 16 | b  << 8 | a, '08x')
+      hex_a = format(r << 24 | g1 << 16 | b  << 8 | a, '08x')
+      hex_b = format(r << 24 | g  << 16 | b1 << 8 | a, '08x')
+      eq_(hexlify(im.view( 3, 3,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 8, 3,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view(13, 3,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 3, 8,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 8, 8,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view(13, 8,1,1).tostring()), hex_a);
+      eq_(hexlify(im.view( 3,13,1,1).tostring()), hex_v);
+      eq_(hexlify(im.view( 8,13,1,1).tostring()), hex_b);
+      eq_(hexlify(im.view(13,13,1,1).tostring()), hex_v);
+
+    def test_rgba_8bui_subquery():
+      _test_rgba_subquery('rgba_8bui_subquery', '8BUI', 255, 0, 0, 255, 255, 255)
+
+    #def test_rgba_16bui_subquery():
+    #  _test_rgba_subquery('rgba_16bui_subquery', '16BUI', 65535, 0, 0, 65535, 65535, 65535)
+
+    #def test_rgba_32bui_subquery():
+    #  _test_rgba_subquery('rgba_32bui_subquery', '32BUI')
+
+    atexit.register(postgis_takedown)
+
+def enabled(tname):
+  enabled = len(sys.argv) < 2 or tname in sys.argv
+  if not enabled:
+    print "Skipping " + tname + " as not explicitly enabled"
+  return enabled
+
+if __name__ == "__main__":
+    setup()
+    fail = run_all(eval(x) for x in dir() if x.startswith("test_") and enabled(x))
+    exit(fail)
diff --git a/test/python_tests/pickling_test.py b/test/python_tests/pickling_test.py
new file mode 100644
index 0000000..7a3572d
--- /dev/null
+++ b/test/python_tests/pickling_test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+import mapnik, pickle
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_color_pickle():
+    c = mapnik.Color('blue')
+
+    eq_(pickle.loads(pickle.dumps(c)), c)
+
+    c = mapnik.Color(0, 64, 128)
+
+    eq_(pickle.loads(pickle.dumps(c)), c)
+
+    c = mapnik.Color(0, 64, 128, 192)
+
+    eq_(pickle.loads(pickle.dumps(c)), c)
+
+def test_envelope_pickle():
+    e = mapnik.Box2d(100, 100, 200, 200)
+
+    eq_(pickle.loads(pickle.dumps(e)), e)
+
+def test_parameters_pickle():
+    params = mapnik.Parameters()
+    params.append(mapnik.Parameter('oh',str('yeah')))
+
+    params2 = pickle.loads(pickle.dumps(params,pickle.HIGHEST_PROTOCOL))
+
+    eq_(params[0][0],params2[0][0])
+    eq_(params[0][1],params2[0][1])
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/png_encoding_test.py b/test/python_tests/png_encoding_test.py
new file mode 100644
index 0000000..568edfd
--- /dev/null
+++ b/test/python_tests/png_encoding_test.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if mapnik.has_png():
+    tmp_dir = '/tmp/mapnik-png/'
+    if not os.path.exists(tmp_dir):
+       os.makedirs(tmp_dir)
+
+    opts = [
+    'png32',
+    'png32:t=0',
+    'png8:m=o',
+    'png8:m=o:c=1',
+    'png8:m=o:t=0',
+    'png8:m=o:c=1:t=0',
+    'png8:m=o:t=1',
+    'png8:m=o:t=2',
+    'png8:m=h',
+    'png8:m=h:c=1',
+    'png8:m=h:t=0',
+    'png8:m=h:c=1:t=0',
+    'png8:m=h:t=1',
+    'png8:m=h:t=2',
+    'png32:e=miniz',
+    'png8:e=miniz'
+    ]
+
+    # Todo - use itertools.product
+    #z_opts = range(1,9+1)
+    #t_opts = range(0,2+1)
+
+    def gen_filepath(name,format):
+        return os.path.join('images/support/encoding-opts',name+'-'+format.replace(":","+")+'.png')
+
+    generate = os.environ.get('UPDATE')
+
+    def test_expected_encodings():
+        # blank image
+        im = mapnik.Image(256,256)
+        for opt in opts:
+            expected = gen_filepath('solid',opt)
+            actual = os.path.join(tmp_dir,os.path.basename(expected))
+            if generate or not os.path.exists(expected):
+              print 'generating expected image %s' % expected
+              im.save(expected,opt)
+            else:
+              im.save(actual,opt)
+              eq_(mapnik.Image.open(actual).tostring('png32'),
+                mapnik.Image.open(expected).tostring('png32'),
+                '%s (actual) not == to %s (expected)' % (actual,expected))
+
+        # solid image
+        im.fill(mapnik.Color('green'))
+        for opt in opts:
+            expected = gen_filepath('blank',opt)
+            actual = os.path.join(tmp_dir,os.path.basename(expected))
+            if generate or not os.path.exists(expected):
+              print 'generating expected image %s' % expected
+              im.save(expected,opt)
+            else:
+              im.save(actual,opt)
+              eq_(mapnik.Image.open(actual).tostring('png32'),
+                mapnik.Image.open(expected).tostring('png32'),
+                '%s (actual) not == to %s (expected)' % (actual,expected))
+
+        # aerial
+        im = mapnik.Image.open('./images/support/transparency/aerial_rgba.png')
+        for opt in opts:
+            expected = gen_filepath('aerial_rgba',opt)
+            actual = os.path.join(tmp_dir,os.path.basename(expected))
+            if generate or not os.path.exists(expected):
+              print 'generating expected image %s' % expected
+              im.save(expected,opt)
+            else:
+              im.save(actual,opt)
+              eq_(mapnik.Image.open(actual).tostring('png32'),
+                mapnik.Image.open(expected).tostring('png32'),
+                '%s (actual) not == to %s (expected)' % (actual,expected))
+
+    def test_transparency_levels():
+        # create partial transparency image
+        im = mapnik.Image(256,256)
+        im.fill(mapnik.Color('rgba(255,255,255,.5)'))
+        c2 = mapnik.Color('rgba(255,255,0,.2)')
+        c3 = mapnik.Color('rgb(0,255,255)')
+        for y in range(0,im.height()/2):
+            for x in range(0,im.width()/2):
+                im.set_pixel(x,y,c2)
+        for y in range(im.height()/2,im.height()):
+            for x in range(im.width()/2,im.width()):
+                im.set_pixel(x,y,c3)
+
+        t0 = tmp_dir + 'white0.png'
+        t2 = tmp_dir + 'white2.png'
+        t1 = tmp_dir + 'white1.png'
+
+        # octree
+        format = 'png8:m=o:t=0'
+        im.save(t0,format)
+        im_in = mapnik.Image.open(t0)
+        t0_len = len(im_in.tostring(format))
+        eq_(t0_len,len(mapnik.Image.open('images/support/transparency/white0.png').tostring(format)))
+        format = 'png8:m=o:t=1'
+        im.save(t1,format)
+        im_in = mapnik.Image.open(t1)
+        t1_len = len(im_in.tostring(format))
+        eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white1.png').tostring(format)))
+        format = 'png8:m=o:t=2'
+        im.save(t2,format)
+        im_in = mapnik.Image.open(t2)
+        t2_len = len(im_in.tostring(format))
+        eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white2.png').tostring(format)))
+
+        eq_(t0_len < t1_len < t2_len,True)
+
+        # hextree
+        format = 'png8:m=h:t=0'
+        im.save(t0,format)
+        im_in = mapnik.Image.open(t0)
+        t0_len = len(im_in.tostring(format))
+        eq_(t0_len,len(mapnik.Image.open('images/support/transparency/white0.png').tostring(format)))
+        format = 'png8:m=h:t=1'
+        im.save(t1,format)
+        im_in = mapnik.Image.open(t1)
+        t1_len = len(im_in.tostring(format))
+        eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white1.png').tostring(format)))
+        format = 'png8:m=h:t=2'
+        im.save(t2,format)
+        im_in = mapnik.Image.open(t2)
+        t2_len = len(im_in.tostring(format))
+        eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white2.png').tostring(format)))
+
+        eq_(t0_len < t1_len < t2_len,True)
+
+    def test_transparency_levels_aerial():
+        im = mapnik.Image.open('../data/images/12_654_1580.png')
+        im_in = mapnik.Image.open('./images/support/transparency/aerial_rgba.png')
+        eq_(len(im.tostring('png8')),len(im_in.tostring('png8')))
+        eq_(len(im.tostring('png32')),len(im_in.tostring('png32')))
+
+        im_in = mapnik.Image.open('./images/support/transparency/aerial_rgb.png')
+        eq_(len(im.tostring('png32')),len(im_in.tostring('png32')))
+        eq_(len(im.tostring('png32:t=0')),len(im_in.tostring('png32:t=0')))
+        eq_(len(im.tostring('png32:t=0')) == len(im_in.tostring('png32')), False)
+        eq_(len(im.tostring('png8')),len(im_in.tostring('png8')))
+        eq_(len(im.tostring('png8:t=0')),len(im_in.tostring('png8:t=0')))
+        # unlike png32 paletted images without alpha will look the same even if no alpha is forced
+        eq_(len(im.tostring('png8:t=0')) == len(im_in.tostring('png8')), True)
+        eq_(len(im.tostring('png8:t=0:m=o')) == len(im_in.tostring('png8:m=o')), True)
+
+    def test_9_colors_hextree():
+        expected = './images/support/encoding-opts/png8-9cols.png'
+        im = mapnik.Image.open(expected)
+        t0 = tmp_dir + 'png-encoding-9-colors.result-hextree.png'
+        im.save(t0, 'png8:m=h')
+        eq_(mapnik.Image.open(t0).tostring(),
+            mapnik.Image.open(expected).tostring(),
+            '%s (actual) not == to %s (expected)' % (t0, expected))
+
+    def test_9_colors_octree():
+        expected = './images/support/encoding-opts/png8-9cols.png'
+        im = mapnik.Image.open(expected)
+        t0 = tmp_dir + 'png-encoding-9-colors.result-octree.png'
+        im.save(t0, 'png8:m=o')
+        eq_(mapnik.Image.open(t0).tostring(),
+            mapnik.Image.open(expected).tostring(),
+            '%s (actual) not == to %s (expected)' % (t0, expected))
+
+    def test_17_colors_hextree():
+        expected = './images/support/encoding-opts/png8-17cols.png'
+        im = mapnik.Image.open(expected)
+        t0 = tmp_dir + 'png-encoding-17-colors.result-hextree.png'
+        im.save(t0, 'png8:m=h')
+        eq_(mapnik.Image.open(t0).tostring(),
+            mapnik.Image.open(expected).tostring(),
+            '%s (actual) not == to %s (expected)' % (t0, expected))
+
+    def test_17_colors_octree():
+        expected = './images/support/encoding-opts/png8-17cols.png'
+        im = mapnik.Image.open(expected)
+        t0 = tmp_dir + 'png-encoding-17-colors.result-octree.png'
+        im.save(t0, 'png8:m=o')
+        eq_(mapnik.Image.open(t0).tostring(),
+            mapnik.Image.open(expected).tostring(),
+            '%s (actual) not == to %s (expected)' % (t0, expected))
+
+    def test_2px_regression_hextree():
+        im = mapnik.Image.open('./images/support/encoding-opts/png8-2px.A.png')
+        expected = './images/support/encoding-opts/png8-2px.png'
+
+        t0 = tmp_dir + 'png-encoding-2px.result-hextree.png'
+        im.save(t0, 'png8:m=h')
+        eq_(mapnik.Image.open(t0).tostring(),
+            mapnik.Image.open(expected).tostring(),
+            '%s (actual) not == to %s (expected)' % (t0, expected))
+
+    def test_2px_regression_octree():
+        im = mapnik.Image.open('./images/support/encoding-opts/png8-2px.A.png')
+        expected = './images/support/encoding-opts/png8-2px.png'
+        t0 = tmp_dir + 'png-encoding-2px.result-octree.png'
+        im.save(t0, 'png8:m=o')
+        eq_(mapnik.Image.open(t0).tostring(),
+            mapnik.Image.open(expected).tostring(),
+            '%s (actual) not == to %s (expected)' % (t0, expected))
+
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/pngsuite_test.py b/test/python_tests/pngsuite_test.py
new file mode 100644
index 0000000..4c933eb
--- /dev/null
+++ b/test/python_tests/pngsuite_test.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+import os
+import mapnik
+from nose.tools import assert_raises
+from utilities import execution_path, run_all
+
+datadir = '../data/pngsuite'
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def assert_broken_file(fname):
+    assert_raises(RuntimeError, lambda: mapnik.Image.open(fname))
+
+def assert_good_file(fname):
+    assert mapnik.Image.open(fname)
+
+def get_pngs(good):
+    files = [ x for x in os.listdir(datadir) if x.endswith('.png') ]
+    return [ os.path.join(datadir, x) for x in files if good != x.startswith('x') ]
+
+def test_good_pngs():
+    for x in get_pngs(True):
+        yield assert_good_file, x
+
+def test_broken_pngs():
+    for x in get_pngs(False):
+        yield assert_broken_file, x
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/postgis_test.py b/test/python_tests/postgis_test.py
new file mode 100644
index 0000000..42e40cc
--- /dev/null
+++ b/test/python_tests/postgis_test.py
@@ -0,0 +1,1177 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,raises
+import atexit
+from utilities import execution_path, run_all
+from subprocess import Popen, PIPE
+import os, mapnik
+import threading
+
+
+MAPNIK_TEST_DBNAME = 'mapnik-tmp-postgis-test-db'
+POSTGIS_TEMPLATE_DBNAME = 'template_postgis'
+SHAPEFILE = os.path.join(execution_path('.'),'../data/shp/world_merc.shp')
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def call(cmd,silent=False):
+    stdin, stderr = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+    if not stderr:
+        return stdin.strip()
+    elif not silent and 'error' in stderr.lower() \
+        or 'not found' in stderr.lower() \
+        or 'could not connect' in stderr.lower() \
+        or 'bad connection' in stderr.lower() \
+        or 'not recognized as an internal' in stderr.lower():
+        raise RuntimeError(stderr.strip())
+
+def psql_can_connect():
+    """Test ability to connect to a postgis template db with no options.
+
+    Basically, to run these tests your user must have full read
+    access over unix sockets without supplying a password. This
+    keeps these tests simple and focused on postgis not on postgres
+    auth issues.
+    """
+    try:
+        call('psql %s -c "select postgis_version()"' % POSTGIS_TEMPLATE_DBNAME)
+        return True
+    except RuntimeError:
+        print 'Notice: skipping postgis tests (connection)'
+        return False
+
+def shp2pgsql_on_path():
+    """Test for presence of shp2pgsql on the user path.
+
+    We require this program to load test data into a temporarily database.
+    """
+    try:
+        call('shp2pgsql')
+        return True
+    except RuntimeError:
+        print 'Notice: skipping postgis tests (shp2pgsql)'
+        return False
+
+def createdb_and_dropdb_on_path():
+    """Test for presence of dropdb/createdb on user path.
+
+    We require these programs to setup and teardown the testing db.
+    """
+    try:
+        call('createdb --help')
+        call('dropdb --help')
+        return True
+    except RuntimeError:
+        print 'Notice: skipping postgis tests (createdb/dropdb)'
+        return False
+
+insert_table_1 = """
+CREATE TABLE test(gid serial PRIMARY KEY, geom geometry);
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POINT(-2 2)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTIPOINT(2 1,1 2)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;LINESTRING(0 0,1 1,1 2)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTILINESTRING((1 0,0 1,3 2),(3 2,5 4))'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTIPOLYGON(((1 1,3 1,3 3,1 3,1 1),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1, 2 1, 2 2, 1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))'));
+"""
+
+insert_table_2 = """
+CREATE TABLE test2(manual_id int4 PRIMARY KEY, geom geometry);
+INSERT INTO test2(manual_id, geom) values (0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (-1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (2147483647, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (-2147483648, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_3 = """
+CREATE TABLE test3(non_id bigint, manual_id int4, geom geometry);
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, -1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 2147483647, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, -2147483648, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_4 = """
+CREATE TABLE test4(non_id int4, manual_id int8 PRIMARY KEY, geom geometry);
+INSERT INTO test4(non_id, manual_id, geom) values (0, 0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, 1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, 1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, -1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, 2147483647, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, -2147483648, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_5 = """
+CREATE TABLE test5(non_id int4, manual_id numeric PRIMARY KEY, geom geometry);
+INSERT INTO test5(non_id, manual_id, geom) values (0, -1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test5(non_id, manual_id, geom) values (0, 1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_5b = '''
+CREATE TABLE "tableWithMixedCase"(gid serial PRIMARY KEY, geom geometry);
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(0,0));
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(0,1));
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(1,0));
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(1,1));
+'''
+
+insert_table_6 = '''
+CREATE TABLE test6(first_id int4, second_id int4,PRIMARY KEY (first_id,second_id), geom geometry);
+INSERT INTO test6(first_id, second_id, geom) values (0, 0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+'''
+
+insert_table_7 = '''
+CREATE TABLE test7(gid serial PRIMARY KEY, geom geometry);
+INSERT INTO test7(gid, geom) values (1, GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(MULTILINESTRING((10 10,20 20,10 40),(40 40,30 30,40 20,30 10)),LINESTRING EMPTY)'));
+'''
+
+insert_table_8 = '''
+CREATE TABLE test8(gid serial PRIMARY KEY,int_field bigint, geom geometry);
+INSERT INTO test8(gid, int_field, geom) values (1, 2147483648, ST_MakePoint(1,1));
+INSERT INTO test8(gid, int_field, geom) values (2, 922337203685477580, ST_MakePoint(1,1));
+'''
+
+insert_table_9 = '''
+CREATE TABLE test9(gid serial PRIMARY KEY, name varchar, geom geometry);
+INSERT INTO test9(gid, name, geom) values (1, 'name', ST_MakePoint(1,1));
+INSERT INTO test9(gid, name, geom) values (2, '', ST_MakePoint(1,1));
+INSERT INTO test9(gid, name, geom) values (3, null, ST_MakePoint(1,1));
+'''
+
+insert_table_10 = '''
+CREATE TABLE test10(gid serial PRIMARY KEY, bool_field boolean, geom geometry);
+INSERT INTO test10(gid, bool_field, geom) values (1, TRUE, ST_MakePoint(1,1));
+INSERT INTO test10(gid, bool_field, geom) values (2, FALSE, ST_MakePoint(1,1));
+INSERT INTO test10(gid, bool_field, geom) values (3, null, ST_MakePoint(1,1));
+'''
+
+insert_table_11 = """
+CREATE TABLE test11(gid serial PRIMARY KEY, label varchar(40), geom geometry);
+INSERT INTO test11(label,geom) values ('label_1',GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test11(label,geom) values ('label_2',GeomFromEWKT('SRID=4326;POINT(-2 2)'));
+INSERT INTO test11(label,geom) values ('label_3',GeomFromEWKT('SRID=4326;MULTIPOINT(2 1,1 2)'));
+INSERT INTO test11(label,geom) values ('label_4',GeomFromEWKT('SRID=4326;LINESTRING(0 0,1 1,1 2)'));
+INSERT INTO test11(label,geom) values ('label_5',GeomFromEWKT('SRID=4326;MULTILINESTRING((1 0,0 1,3 2),(3 2,5 4))'));
+INSERT INTO test11(label,geom) values ('label_6',GeomFromEWKT('SRID=4326;POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))'));
+INSERT INTO test11(label,geom) values ('label_7',GeomFromEWKT('SRID=4326;MULTIPOLYGON(((1 1,3 1,3 3,1 3,1 1),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))'));
+INSERT INTO test11(label,geom) values ('label_8',GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1, 2 1, 2 2, 1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))'));
+"""
+
+insert_table_12 = """
+CREATE TABLE test12(gid serial PRIMARY KEY, name varchar(40), geom geometry);
+INSERT INTO test12(name,geom) values ('Point',GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test12(name,geom) values ('PointZ',GeomFromEWKT('SRID=4326;POINTZ(0 0 0)'));
+INSERT INTO test12(name,geom) values ('PointM',GeomFromEWKT('SRID=4326;POINTM(0 0 0)'));
+INSERT INTO test12(name,geom) values ('PointZM',GeomFromEWKT('SRID=4326;POINTZM(0 0 0 0)'));
+INSERT INTO test12(name,geom) values ('MultiPoint',GeomFromEWKT('SRID=4326;MULTIPOINT(0 0, 1 1)'));
+INSERT INTO test12(name,geom) values ('MultiPointZ',GeomFromEWKT('SRID=4326;MULTIPOINTZ(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('MultiPointM',GeomFromEWKT('SRID=4326;MULTIPOINTM(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('MultiPointZM',GeomFromEWKT('SRID=4326;MULTIPOINTZM(0 0 0 0, 1 1 1 1)'));
+INSERT INTO test12(name,geom) values ('LineString',GeomFromEWKT('SRID=4326;LINESTRING(0 0, 1 1)'));
+INSERT INTO test12(name,geom) values ('LineStringZ',GeomFromEWKT('SRID=4326;LINESTRINGZ(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('LineStringM',GeomFromEWKT('SRID=4326;LINESTRINGM(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('LineStringZM',GeomFromEWKT('SRID=4326;LINESTRINGZM(0 0 0 0, 1 1 1 1)'));
+INSERT INTO test12(name,geom) values ('Polygon',GeomFromEWKT('SRID=4326;POLYGON((0 0, 1 1, 2 2, 0 0))'));
+INSERT INTO test12(name,geom) values ('PolygonZ',GeomFromEWKT('SRID=4326;POLYGONZ((0 0 0, 1 1 1, 2 2 2, 0 0 0))'));
+INSERT INTO test12(name,geom) values ('PolygonM',GeomFromEWKT('SRID=4326;POLYGONZ((0 0 0, 1 1 1, 2 2 2, 0 0 0))'));
+INSERT INTO test12(name,geom) values ('PolygonZM',GeomFromEWKT('SRID=4326;POLYGONZM((0 0 0 0, 1 1 1 1, 2 2 2 2, 0 0 0 0))'));
+INSERT INTO test12(name,geom) values ('MultiLineString',GeomFromEWKT('SRID=4326;MULTILINESTRING((0 0, 1 1),(2 2, 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiLineStringZ',GeomFromEWKT('SRID=4326;MULTILINESTRINGZ((0 0 0, 1 1 1),(2 2 2, 3 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiLineStringM',GeomFromEWKT('SRID=4326;MULTILINESTRINGM((0 0 0, 1 1 1),(2 2 2, 3 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiLineStringZM',GeomFromEWKT('SRID=4326;MULTILINESTRINGZM((0 0 0 0, 1 1 1 1),(2 2 2 2, 3 3 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiPolygon',GeomFromEWKT('SRID=4326;MULTIPOLYGON(((0 0, 1 1, 2 2, 0 0)),((0 0, 1 1, 2 2, 0 0)))'));
+INSERT INTO test12(name,geom) values ('MultiPolygonZ',GeomFromEWKT('SRID=4326;MULTIPOLYGONZ(((0 0 0, 1 1 1, 2 2 2, 0 0 0)),((0 0 0, 1 1 1, 2 2 2, 0 0 0)))'));
+INSERT INTO test12(name,geom) values ('MultiPolygonM',GeomFromEWKT('SRID=4326;MULTIPOLYGONM(((0 0 0, 1 1 1, 2 2 2, 0 0 0)),((0 0 0, 1 1 1, 2 2 2, 0 0 0)))'));
+INSERT INTO test12(name,geom) values ('MultiPolygonZM',GeomFromEWKT('SRID=4326;MULTIPOLYGONZM(((0 0 0 0, 1 1 1 1, 2 2 2 2, 0 0 0 0)),((0 0 0 0, 1 1 1 1, 2 2 2 2, 0 0 0 0)))'));
+"""
+
+
+def postgis_setup():
+    call('dropdb %s' % MAPNIK_TEST_DBNAME,silent=True)
+    call('createdb -T %s %s' % (POSTGIS_TEMPLATE_DBNAME,MAPNIK_TEST_DBNAME),silent=False)
+    call('shp2pgsql -s 3857 -g geom -W LATIN1 %s world_merc | psql -q %s' % (SHAPEFILE,MAPNIK_TEST_DBNAME), silent=True)
+    call('''psql -q %s -c "CREATE TABLE \"empty\" (key serial);SELECT AddGeometryColumn('','empty','geom','-1','GEOMETRY',4);"''' % MAPNIK_TEST_DBNAME,silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_1),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_2),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_3),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_4),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_5),silent=False)
+    call("""psql -q %s -c '%s'""" % (MAPNIK_TEST_DBNAME,insert_table_5b),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_6),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_7),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_8),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_9),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_10),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_11),silent=False)
+    call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_12),silent=False)
+
+def postgis_takedown():
+    pass
+    # fails as the db is in use: https://github.com/mapnik/mapnik/issues/960
+    #call('dropdb %s' % MAPNIK_TEST_DBNAME)
+
+if 'postgis' in mapnik.DatasourceCache.plugin_names() \
+        and createdb_and_dropdb_on_path() \
+        and psql_can_connect() \
+        and shp2pgsql_on_path():
+
+    # initialize test database
+    postgis_setup()
+
+    def test_feature():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='world_merc')
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['gid'],1)
+        eq_(feature['fips'],u'AC')
+        eq_(feature['iso2'],u'AG')
+        eq_(feature['iso3'],u'ATG')
+        eq_(feature['un'],28)
+        eq_(feature['name'],u'Antigua and Barbuda')
+        eq_(feature['area'],44)
+        eq_(feature['pop2005'],83039)
+        eq_(feature['region'],19)
+        eq_(feature['subregion'],29)
+        eq_(feature['lon'],-61.783)
+        eq_(feature['lat'],17.078)
+        meta = ds.describe()
+        eq_(meta['srid'],3857)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['encoding'],u'UTF8')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+
+    def test_subquery():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(select * from world_merc) as w')
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['gid'],1)
+        eq_(feature['fips'],u'AC')
+        eq_(feature['iso2'],u'AG')
+        eq_(feature['iso3'],u'ATG')
+        eq_(feature['un'],28)
+        eq_(feature['name'],u'Antigua and Barbuda')
+        eq_(feature['area'],44)
+        eq_(feature['pop2005'],83039)
+        eq_(feature['region'],19)
+        eq_(feature['subregion'],29)
+        eq_(feature['lon'],-61.783)
+        eq_(feature['lat'],17.078)
+        meta = ds.describe()
+        eq_(meta['srid'],3857)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['encoding'],u'UTF8')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(select gid,geom,fips as _fips from world_merc) as w')
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['gid'],1)
+        eq_(feature['_fips'],u'AC')
+        eq_(len(feature),2)
+        meta = ds.describe()
+        eq_(meta['srid'],3857)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['encoding'],u'UTF8')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+
+    def test_bad_connection():
+        try:
+            ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+                                table='test',
+                                max_size=20,
+                                geometry_field='geom',
+                                user="rolethatdoesnotexist")
+        except Exception, e:
+            assert 'role "rolethatdoesnotexist" does not exist' in str(e)
+
+    def test_empty_db():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='empty')
+        fs = ds.featureset()
+        feature = None
+        try:
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['encoding'],u'UTF8')
+        eq_(meta['geometry_type'],None)
+
+    def test_manual_srid():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,srid=99, table='empty')
+        fs = ds.featureset()
+        feature = None
+        try:
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+        meta = ds.describe()
+        eq_(meta['srid'],99)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['encoding'],u'UTF8')
+        eq_(meta['geometry_type'],None)
+
+    def test_geometry_detection():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test',
+                            geometry_field='geom')
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+        # will fail with postgis 2.0 because it automatically adds a geometry_columns entry
+        #ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test',
+        #                   geometry_field='geom',
+        #                    row_limit=1)
+        #eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+
+    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='empty')
+        eq_(len(ds.fields()),1)
+        eq_(ds.fields(),['key'])
+        eq_(ds.field_types(),['int'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        # also add an invalid one, triggering throw
+        query.add_property_name('bogus')
+        ds.features(query)
+
+    def test_auto_detection_of_unique_feature_id_32_bit():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test2',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        eq_(fs.next()['manual_id'],0)
+        eq_(fs.next()['manual_id'],1)
+        eq_(fs.next()['manual_id'],1000)
+        eq_(fs.next()['manual_id'],-1000)
+        eq_(fs.next()['manual_id'],2147483647)
+        eq_(fs.next()['manual_id'],-2147483648)
+
+        fs = ds.featureset()
+        eq_(fs.next().id(),0)
+        eq_(fs.next().id(),1)
+        eq_(fs.next().id(),1000)
+        eq_(fs.next().id(),-1000)
+        eq_(fs.next().id(),2147483647)
+        eq_(fs.next().id(),-2147483648)
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),u'manual_id')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_auto_detection_will_fail_since_no_primary_key():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test3',
+                            geometry_field='geom',
+                            autodetect_key_field=False)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat['manual_id'],0)
+        # will fail: https://github.com/mapnik/mapnik/issues/895
+        #eq_(feat['non_id'],9223372036854775807)
+        eq_(fs.next()['manual_id'],1)
+        eq_(fs.next()['manual_id'],1000)
+        eq_(fs.next()['manual_id'],-1000)
+        eq_(fs.next()['manual_id'],2147483647)
+        eq_(fs.next()['manual_id'],-2147483648)
+
+        # since no valid primary key will be detected the fallback
+        # is auto-incrementing counter
+        fs = ds.featureset()
+        eq_(fs.next().id(),1)
+        eq_(fs.next().id(),2)
+        eq_(fs.next().id(),3)
+        eq_(fs.next().id(),4)
+        eq_(fs.next().id(),5)
+        eq_(fs.next().id(),6)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    @raises(RuntimeError)
+    def test_auto_detection_will_fail_and_should_throw():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test3',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        ds.featureset()
+
+    def test_auto_detection_of_unique_feature_id_64_bit():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test4',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        eq_(fs.next()['manual_id'],0)
+        eq_(fs.next()['manual_id'],1)
+        eq_(fs.next()['manual_id'],1000)
+        eq_(fs.next()['manual_id'],-1000)
+        eq_(fs.next()['manual_id'],2147483647)
+        eq_(fs.next()['manual_id'],-2147483648)
+
+        fs = ds.featureset()
+        eq_(fs.next().id(),0)
+        eq_(fs.next().id(),1)
+        eq_(fs.next().id(),1000)
+        eq_(fs.next().id(),-1000)
+        eq_(fs.next().id(),2147483647)
+        eq_(fs.next().id(),-2147483648)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),u'manual_id')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_disabled_auto_detection_and_subquery():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select geom, 'a'::varchar as name from test2) as t''',
+                            geometry_field='geom',
+                            autodetect_key_field=False)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat.id(),1)
+        eq_(feat['name'],'a')
+        feat = fs.next()
+        eq_(feat.id(),2)
+        eq_(feat['name'],'a')
+        feat = fs.next()
+        eq_(feat.id(),3)
+        eq_(feat['name'],'a')
+        feat = fs.next()
+        eq_(feat.id(),4)
+        eq_(feat['name'],'a')
+        feat = fs.next()
+        eq_(feat.id(),5)
+        eq_(feat['name'],'a')
+        feat = fs.next()
+        eq_(feat.id(),6)
+        eq_(feat['name'],'a')
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_auto_detection_and_subquery_including_key():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select geom, manual_id from test2) as t''',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        eq_(fs.next()['manual_id'],0)
+        eq_(fs.next()['manual_id'],1)
+        eq_(fs.next()['manual_id'],1000)
+        eq_(fs.next()['manual_id'],-1000)
+        eq_(fs.next()['manual_id'],2147483647)
+        eq_(fs.next()['manual_id'],-2147483648)
+
+        fs = ds.featureset()
+        eq_(fs.next().id(),0)
+        eq_(fs.next().id(),1)
+        eq_(fs.next().id(),1000)
+        eq_(fs.next().id(),-1000)
+        eq_(fs.next().id(),2147483647)
+        eq_(fs.next().id(),-2147483648)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),u'manual_id')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    @raises(RuntimeError)
+    def test_auto_detection_of_invalid_numeric_primary_key():
+        mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select geom, manual_id::numeric from test2) as t''',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+
+    @raises(RuntimeError)
+    def test_auto_detection_of_invalid_multiple_keys():
+        mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''test6''',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+
+    @raises(RuntimeError)
+    def test_auto_detection_of_invalid_multiple_keys_subquery():
+        mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select first_id,second_id,geom from test6) as t''',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+
+    def test_manually_specified_feature_id_field():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test4',
+                            geometry_field='geom',
+                            key_field='manual_id',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        eq_(fs.next()['manual_id'],0)
+        eq_(fs.next()['manual_id'],1)
+        eq_(fs.next()['manual_id'],1000)
+        eq_(fs.next()['manual_id'],-1000)
+        eq_(fs.next()['manual_id'],2147483647)
+        eq_(fs.next()['manual_id'],-2147483648)
+
+        fs = ds.featureset()
+        eq_(fs.next().id(),0)
+        eq_(fs.next().id(),1)
+        eq_(fs.next().id(),1000)
+        eq_(fs.next().id(),-1000)
+        eq_(fs.next().id(),2147483647)
+        eq_(fs.next().id(),-2147483648)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),u'manual_id')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_numeric_type_feature_id_field():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test5',
+                            geometry_field='geom',
+                            autodetect_key_field=False)
+        fs = ds.featureset()
+        eq_(fs.next()['manual_id'],-1)
+        eq_(fs.next()['manual_id'],1)
+
+        fs = ds.featureset()
+        eq_(fs.next().id(),1)
+        eq_(fs.next().id(),2)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_querying_table_with_mixed_case():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='"tableWithMixedCase"',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),u'gid')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_querying_subquery_with_mixed_case():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(SeLeCt * FrOm "tableWithMixedCase") as MixedCaseQuery',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),u'gid')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_bbox_token_in_subquery1():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+           (SeLeCt * FrOm "tableWithMixedCase" where geom && !bbox! ) as MixedCaseQuery''',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),u'gid')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_bbox_token_in_subquery2():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+           (SeLeCt * FrOm "tableWithMixedCase" where ST_Intersects(geom,!bbox!) ) as MixedCaseQuery''',
+                            geometry_field='geom',
+                            autodetect_key_field=True)
+        fs = ds.featureset()
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),u'gid')
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_empty_geom():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test7',
+                            geometry_field='geom')
+        fs = ds.featureset()
+        eq_(fs.next()['gid'],1)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+    def create_ds():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+                            table='test',
+                            max_size=20,
+                            geometry_field='geom')
+        fs = ds.all_features()
+        eq_(len(fs),8)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+    def test_threaded_create(NUM_THREADS=100):
+        # run one to start before thread loop
+        # to ensure that a throw stops the test
+        # from running all threads
+        create_ds()
+        runs = 0
+        for i in range(NUM_THREADS):
+            t = threading.Thread(target=create_ds)
+            t.start()
+            t.join()
+            runs +=1
+        eq_(runs,NUM_THREADS)
+
+    def create_ds_and_error():
+        try:
+            ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+                                table='asdfasdfasdfasdfasdf',
+                                max_size=20)
+            ds.all_features()
+        except Exception, e:
+            eq_('in executeQuery' in str(e),True)
+
+    def test_threaded_create2(NUM_THREADS=10):
+        for i in range(NUM_THREADS):
+            t = threading.Thread(target=create_ds_and_error)
+            t.start()
+            t.join()
+
+    def test_that_64bit_int_fields_work():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+                            table='test8',
+                            geometry_field='geom')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['gid','int_field'])
+        eq_(ds.field_types(),['int','int'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat.id(),1)
+        eq_(feat['gid'],1)
+        eq_(feat['int_field'],2147483648)
+        feat = fs.next()
+        eq_(feat.id(),2)
+        eq_(feat['gid'],2)
+        eq_(feat['int_field'],922337203685477580)
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_persist_connection_off():
+        # NOTE: max_size should be equal or greater than
+        #       the pool size. There's currently no API to
+        #       check nor set that size, but the current
+        #       default is 20, so we use that value. See
+        #       http://github.com/mapnik/mapnik/issues/863
+        max_size = 20
+        for i in range(0, max_size+1):
+            ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+                              max_size=1, # unused
+                              persist_connection=False,
+                              table='(select ST_MakePoint(0,0) as g, pg_backend_pid() as p, 1 as v) as w',
+                              geometry_field='g')
+            fs = ds.featureset()
+            eq_(fs.next()['v'], 1)
+
+            meta = ds.describe()
+            eq_(meta['srid'],-1)
+            eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    def test_null_comparision():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test9',
+                            geometry_field='geom')
+        fs = ds.featureset()
+        feat = fs.next()
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(feat['gid'],1)
+        eq_(feat['name'],'name')
+        eq_(mapnik.Expression("[name] = 'name'").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] = ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = null").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = true").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = false").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != 'name'").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != ''").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != null").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != true").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != false").evaluate(feat),True)
+
+        feat = fs.next()
+        eq_(feat['gid'],2)
+        eq_(feat['name'],'')
+        eq_(mapnik.Expression("[name] = 'name'").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = ''").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] = null").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = true").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = false").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != 'name'").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != null").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != true").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != false").evaluate(feat),True)
+
+        feat = fs.next()
+        eq_(feat['gid'],3)
+        eq_(feat['name'],None) # null
+        eq_(mapnik.Expression("[name] = 'name'").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = null").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] = true").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] = false").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != 'name'").evaluate(feat),True)
+        # https://github.com/mapnik/mapnik/issues/1859
+        eq_(mapnik.Expression("[name] != ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != null").evaluate(feat),False)
+        eq_(mapnik.Expression("[name] != true").evaluate(feat),True)
+        eq_(mapnik.Expression("[name] != false").evaluate(feat),True)
+
+    def test_null_comparision2():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test10',
+                            geometry_field='geom')
+        fs = ds.featureset()
+        feat = fs.next()
+
+        meta = ds.describe()
+        eq_(meta['srid'],-1)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(feat['gid'],1)
+        eq_(feat['bool_field'],True)
+        eq_(mapnik.Expression("[bool_field] = 'name'").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = null").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = true").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] = false").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] != 'name'").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] != ''").evaluate(feat),True) # in 2.1.x used to be False
+        eq_(mapnik.Expression("[bool_field] != null").evaluate(feat),True) # in 2.1.x used to be False
+        eq_(mapnik.Expression("[bool_field] != true").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] != false").evaluate(feat),True)
+
+        feat = fs.next()
+        eq_(feat['gid'],2)
+        eq_(feat['bool_field'],False)
+        eq_(mapnik.Expression("[bool_field] = 'name'").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = null").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = true").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = false").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] != 'name'").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] != ''").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] != null").evaluate(feat),True) # in 2.1.x used to be False
+        eq_(mapnik.Expression("[bool_field] != true").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] != false").evaluate(feat),False)
+
+        feat = fs.next()
+        eq_(feat['gid'],3)
+        eq_(feat['bool_field'],None) # null
+        eq_(mapnik.Expression("[bool_field] = 'name'").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = null").evaluate(feat),True)
+        eq_(mapnik.Expression("[bool_field] = true").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] = false").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] != 'name'").evaluate(feat),True)  # in 2.1.x used to be False
+        # https://github.com/mapnik/mapnik/issues/1859
+        eq_(mapnik.Expression("[bool_field] != ''").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] != null").evaluate(feat),False)
+        eq_(mapnik.Expression("[bool_field] != true").evaluate(feat),True) # in 2.1.x used to be False
+        eq_(mapnik.Expression("[bool_field] != false").evaluate(feat),True) # in 2.1.x used to be False
+
+    # https://github.com/mapnik/mapnik/issues/1816
+    def test_exception_message_reporting():
+        try:
+            mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='doesnotexist')
+        except Exception, e:
+            eq_(e.message != 'unidentifiable C++ exception', True)
+
+    def test_null_id_field():
+        opts = {'type':'postgis',
+                'dbname':MAPNIK_TEST_DBNAME,
+                'geometry_field':'geom',
+                'table':"(select null::bigint as osm_id, GeomFromEWKT('SRID=4326;POINT(0 0)') as geom) as tmp"}
+        ds = mapnik.Datasource(**opts)
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat.id(),1L)
+        eq_(feat['osm_id'],None)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+    @raises(StopIteration)
+    def test_null_key_field():
+        opts = {'type':'postgis',
+                "key_field": 'osm_id',
+                'dbname':MAPNIK_TEST_DBNAME,
+                'geometry_field':'geom',
+                'table':"(select null::bigint as osm_id, GeomFromEWKT('SRID=4326;POINT(0 0)') as geom) as tmp"}
+        ds = mapnik.Datasource(**opts)
+        fs = ds.featureset()
+        fs.next() ## should throw since key_field is null: StopIteration: No more features.
+
+    def test_psql_error_should_not_break_connection_pool():
+        # Bad request, will trigger an error when returning result
+        ds_bad = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table="""(SELECT geom as geom,label::int from test11) as failure_table""",
+                            max_async_connection=5,geometry_field='geom',srid=4326)
+
+        # Good request
+        ds_good = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table="test",
+                            max_async_connection=5,geometry_field='geom',srid=4326)
+
+        # This will/should trigger a PSQL error
+        failed = False
+        try:
+            fs = ds_bad.featureset()
+            for feature in fs:
+                pass
+        except RuntimeError, e:
+            assert 'invalid input syntax for integer' in str(e)
+            failed = True
+
+        eq_(failed,True)
+
+        # Should be ok
+        fs = ds_good.featureset()
+        count = 0
+        for feature in fs:
+            count += 1
+        eq_(count,8)
+
+
+    def test_psql_error_should_give_back_connections_opened_for_lower_layers_to_the_pool():
+        map1 = mapnik.Map(600,300)
+        s = mapnik.Style()
+        r = mapnik.Rule()
+        r.symbols.append(mapnik.PolygonSymbolizer())
+        s.rules.append(r)
+        map1.append_style('style',s)
+
+        # This layer will fail after a while
+        buggy_s = mapnik.Style()
+        buggy_r = mapnik.Rule()
+        buggy_r.symbols.append(mapnik.PolygonSymbolizer())
+        buggy_r.filter = mapnik.Filter("[fips] = 'FR'")
+        buggy_s.rules.append(buggy_r)
+        map1.append_style('style for buggy layer',buggy_s)
+        buggy_layer = mapnik.Layer('this layer is buggy at runtime')
+        # We ensure the query wille be long enough
+        buggy_layer.datasource = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(SELECT geom as geom, pg_sleep(0.1), fips::int from world_merc) as failure_tabl',
+            max_async_connection=2, max_size=2,asynchronous_request = True, geometry_field='geom')
+        buggy_layer.styles.append('style for buggy layer')
+
+        # The query for this layer will be sent, then the previous layer will raise an exception before results are read
+        forced_canceled_layer = mapnik.Layer('this layer will be canceled when an exception stops map rendering')
+        forced_canceled_layer.datasource = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='world_merc',
+            max_async_connection=2, max_size=2, asynchronous_request = True, geometry_field='geom')
+        forced_canceled_layer.styles.append('style')
+
+        map1.layers.append(buggy_layer)
+        map1.layers.append(forced_canceled_layer)
+        map1.zoom_all()
+        map2 = mapnik.Map(600,300)
+        map2.background = mapnik.Color('steelblue')
+        s = mapnik.Style()
+        r = mapnik.Rule()
+        r.symbols.append(mapnik.LineSymbolizer())
+        r.symbols.append(mapnik.LineSymbolizer())
+        s.rules.append(r)
+        map2.append_style('style',s)
+        layer1 = mapnik.Layer('layer1')
+        layer1.datasource = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='world_merc',
+            max_async_connection=2, max_size=2, asynchronous_request = True, geometry_field='geom')
+        layer1.styles.append('style')
+        map2.layers.append(layer1)
+        map2.zoom_all()
+
+        # We expect this to trigger a PSQL error
+        try:
+            mapnik.render_to_file(map1,'/tmp/mapnik-postgis-test-map1.png', 'png')
+            # Test must fail if error was not raised just above
+            eq_(False,True)
+        except RuntimeError, e:
+            assert 'invalid input syntax for integer' in str(e)
+            pass
+        # This used to raise an exception before correction of issue 2042
+        mapnik.render_to_file(map2,'/tmp/mapnik-postgis-test-map2.png', 'png')
+
+    def test_handling_of_zm_dimensions():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+                            table='(select gid,ST_CoordDim(geom) as dim,name,geom from test12) as tmp',
+                            geometry_field='geom')
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['gid', 'dim', 'name'])
+        eq_(ds.field_types(),['int', 'int', 'str'])
+        fs = ds.featureset()
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),None)
+        # Note: this is incorrect because we only check first couple geoms
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+        # Point (2d)
+        feat = fs.next()
+        eq_(feat.id(),1)
+        eq_(feat['gid'],1)
+        eq_(feat['dim'],2)
+        eq_(feat['name'],'Point')
+        eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+        # PointZ
+        feat = fs.next()
+        eq_(feat.id(),2)
+        eq_(feat['gid'],2)
+        eq_(feat['dim'],3)
+        eq_(feat['name'],'PointZ')
+        eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+        # PointM
+        feat = fs.next()
+        eq_(feat.id(),3)
+        eq_(feat['gid'],3)
+        eq_(feat['dim'],3)
+        eq_(feat['name'],'PointM')
+        eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+        # PointZM
+        feat = fs.next()
+        eq_(feat.id(),4)
+        eq_(feat['gid'],4)
+        eq_(feat['dim'],4)
+        eq_(feat['name'],'PointZM')
+
+        eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+        # MultiPoint
+        feat = fs.next()
+        eq_(feat.id(),5)
+        eq_(feat['gid'],5)
+        eq_(feat['dim'],2)
+        eq_(feat['name'],'MultiPoint')
+        eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+        # MultiPointZ
+        feat = fs.next()
+        eq_(feat.id(),6)
+        eq_(feat['gid'],6)
+        eq_(feat['dim'],3)
+        eq_(feat['name'],'MultiPointZ')
+        eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+        # MultiPointM
+        feat = fs.next()
+        eq_(feat.id(),7)
+        eq_(feat['gid'],7)
+        eq_(feat['dim'],3)
+        eq_(feat['name'],'MultiPointM')
+        eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+        # MultiPointZM
+        feat = fs.next()
+        eq_(feat.id(),8)
+        eq_(feat['gid'],8)
+        eq_(feat['dim'],4)
+        eq_(feat['name'],'MultiPointZM')
+        eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+        # LineString
+        feat = fs.next()
+        eq_(feat.id(),9)
+        eq_(feat['gid'],9)
+        eq_(feat['dim'],2)
+        eq_(feat['name'],'LineString')
+        eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+        # LineStringZ
+        feat = fs.next()
+        eq_(feat.id(),10)
+        eq_(feat['gid'],10)
+        eq_(feat['dim'],3)
+        eq_(feat['name'],'LineStringZ')
+        eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+        # LineStringM
+        feat = fs.next()
+        eq_(feat.id(),11)
+        eq_(feat['gid'],11)
+        eq_(feat['dim'],3)
+        eq_(feat['name'],'LineStringM')
+        eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+        # LineStringZM
+        feat = fs.next()
+        eq_(feat.id(),12)
+        eq_(feat['gid'],12)
+        eq_(feat['dim'],4)
+        eq_(feat['name'],'LineStringZM')
+        eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+        # Polygon
+        feat = fs.next()
+        eq_(feat.id(),13)
+        eq_(feat['gid'],13)
+        eq_(feat['name'],'Polygon')
+        eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+        # PolygonZ
+        feat = fs.next()
+        eq_(feat.id(),14)
+        eq_(feat['gid'],14)
+        eq_(feat['name'],'PolygonZ')
+        eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+        # PolygonM
+        feat = fs.next()
+        eq_(feat.id(),15)
+        eq_(feat['gid'],15)
+        eq_(feat['name'],'PolygonM')
+        eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+        # PolygonZM
+        feat = fs.next()
+        eq_(feat.id(),16)
+        eq_(feat['gid'],16)
+        eq_(feat['name'],'PolygonZM')
+        eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+        # MultiLineString
+        feat = fs.next()
+        eq_(feat.id(),17)
+        eq_(feat['gid'],17)
+        eq_(feat['name'],'MultiLineString')
+        eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+        # MultiLineStringZ
+        feat = fs.next()
+        eq_(feat.id(),18)
+        eq_(feat['gid'],18)
+        eq_(feat['name'],'MultiLineStringZ')
+        eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+        # MultiLineStringM
+        feat = fs.next()
+        eq_(feat.id(),19)
+        eq_(feat['gid'],19)
+        eq_(feat['name'],'MultiLineStringM')
+        eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+        # MultiLineStringZM
+        feat = fs.next()
+        eq_(feat.id(),20)
+        eq_(feat['gid'],20)
+        eq_(feat['name'],'MultiLineStringZM')
+        eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+        # MultiPolygon
+        feat = fs.next()
+        eq_(feat.id(),21)
+        eq_(feat['gid'],21)
+        eq_(feat['name'],'MultiPolygon')
+        eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+        # MultiPolygonZ
+        feat = fs.next()
+        eq_(feat.id(),22)
+        eq_(feat['gid'],22)
+        eq_(feat['name'],'MultiPolygonZ')
+        eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+        # MultiPolygonM
+        feat = fs.next()
+        eq_(feat.id(),23)
+        eq_(feat['gid'],23)
+        eq_(feat['name'],'MultiPolygonM')
+        eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+        # MultiPolygonZM
+        feat = fs.next()
+        eq_(feat.id(),24)
+        eq_(feat['gid'],24)
+        eq_(feat['name'],'MultiPolygonZM')
+        eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+    def test_variable_in_subquery1():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+           (select * from test where @zoom = 30 ) as tmp''',
+                            geometry_field='geom', srid=4326,
+                            autodetect_key_field=True)
+        fs = ds.featureset(variables={'zoom':30})
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta.get('key_field'),"gid")
+        eq_(meta['geometry_type'],None)
+
+    # currently needs manual `geometry_table` passed
+    # to avoid misparse of `geometry_table`
+    # in the future ideally this would not need manual  `geometry_table`
+    # https://github.com/mapnik/mapnik/issues/2718
+    # currently `bogus` would be picked automatically for geometry_table
+    def test_broken_parsing_of_comments():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+             (select * FROM test) AS data
+             -- select this from bogus''',
+                            geometry_table='test')
+        fs = ds.featureset()
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+    # same
+    # to avoid misparse of `geometry_table`
+    # in the future ideally this would not need manual  `geometry_table`
+    # https://github.com/mapnik/mapnik/issues/2718
+    # currently nothing would be picked automatically for geometry_table
+    def test_broken_parsing_of_comments():
+        ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+             (select * FROM test) AS data
+             -- select this from bogus.''',
+                            geometry_table='test')
+        fs = ds.featureset()
+        for id in range(1,5):
+            eq_(fs.next().id(),id)
+
+        meta = ds.describe()
+        eq_(meta['srid'],4326)
+        eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+
+    atexit.register(postgis_takedown)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/projection_test.py b/test/python_tests/projection_test.py
new file mode 100644
index 0000000..a7bdc14
--- /dev/null
+++ b/test/python_tests/projection_test.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,assert_almost_equal
+
+import mapnik
+import math
+from utilities import run_all, assert_box2d_almost_equal
+
+# Tests that exercise map projections.
+
+def test_normalizing_definition():
+    p = mapnik.Projection('+init=epsg:4326')
+    expanded = p.expanded()
+    eq_('+proj=longlat' in expanded,True)
+
+
+# Trac Ticket #128
+def test_wgs84_inverse_forward():
+    p = mapnik.Projection('+init=epsg:4326')
+
+    c = mapnik.Coord(3.01331418311, 43.3333092669)
+    e = mapnik.Box2d(-122.54345245, 45.12312553, 68.2335581353, 48.231231233)
+
+    # It appears that the y component changes very slightly, is this OK?
+    # so we test for 'almost equal float values'
+
+    assert_almost_equal(p.inverse(c).y, c.y)
+    assert_almost_equal(p.inverse(c).x, c.x)
+
+    assert_almost_equal(p.forward(c).y, c.y)
+    assert_almost_equal(p.forward(c).x, c.x)
+
+    assert_almost_equal(p.inverse(e).center().y, e.center().y)
+    assert_almost_equal(p.inverse(e).center().x, e.center().x)
+
+    assert_almost_equal(p.forward(e).center().y, e.center().y)
+    assert_almost_equal(p.forward(e).center().x, e.center().x)
+
+    assert_almost_equal(c.inverse(p).y, c.y)
+    assert_almost_equal(c.inverse(p).x, c.x)
+
+    assert_almost_equal(c.forward(p).y, c.y)
+    assert_almost_equal(c.forward(p).x, c.x)
+
+    assert_almost_equal(e.inverse(p).center().y, e.center().y)
+    assert_almost_equal(e.inverse(p).center().x, e.center().x)
+
+    assert_almost_equal(e.forward(p).center().y, e.center().y)
+    assert_almost_equal(e.forward(p).center().x, e.center().x)
+
+def wgs2merc(lon,lat):
+    x = lon * 20037508.34 / 180;
+    y = math.log(math.tan((90 + lat) * math.pi / 360)) / (math.pi / 180);
+    y = y * 20037508.34 / 180;
+    return [x,y];
+
+def merc2wgs(x,y):
+    x = (x / 20037508.34) * 180;
+    y = (y / 20037508.34) * 180;
+    y = 180 / math.pi * (2 * math.atan(math.exp(y * math.pi/180)) - math.pi/2);
+    if x > 180: x = 180;
+    if x < -180: x = -180;
+    if y > 85.0511: y = 85.0511;
+    if y < -85.0511: y = -85.0511;
+    return [x,y]
+
+#echo -109 37 | cs2cs -f "%.10f" +init=epsg:4326 +to +init=epsg:3857
+#-12133824.4964668211    4439106.7872505859 0.0000000000
+
+## todo
+# benchmarks
+# better well known detection
+# better srs matching with strip/trim
+# python copy to avoid crash
+
+def test_proj_transform_between_init_and_literal():
+    one = mapnik.Projection('+init=epsg:4326')
+    two = mapnik.Projection('+init=epsg:3857')
+    tr1 = mapnik.ProjTransform(one,two)
+    tr1b = mapnik.ProjTransform(two,one)
+    wgs84 = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+    merc = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'
+    src = mapnik.Projection(wgs84)
+    dest = mapnik.Projection(merc)
+    tr2 = mapnik.ProjTransform(src,dest)
+    tr2b = mapnik.ProjTransform(dest,src)
+    for x in xrange(-180,180,10):
+        for y in xrange(-60,60,10):
+            coord = mapnik.Coord(x,y)
+            merc_coord1 = tr1.forward(coord)
+            merc_coord2 = tr1b.backward(coord)
+            merc_coord3 = tr2.forward(coord)
+            merc_coord4 = tr2b.backward(coord)
+            eq_(math.fabs(merc_coord1.x - merc_coord1.x) < 1,True)
+            eq_(math.fabs(merc_coord1.x - merc_coord2.x) < 1,True)
+            eq_(math.fabs(merc_coord1.x - merc_coord3.x) < 1,True)
+            eq_(math.fabs(merc_coord1.x - merc_coord4.x) < 1,True)
+            eq_(math.fabs(merc_coord1.y - merc_coord1.y) < 1,True)
+            eq_(math.fabs(merc_coord1.y - merc_coord2.y) < 1,True)
+            eq_(math.fabs(merc_coord1.y - merc_coord3.y) < 1,True)
+            eq_(math.fabs(merc_coord1.y - merc_coord4.y) < 1,True)
+            lon_lat_coord1 = tr1.backward(merc_coord1)
+            lon_lat_coord2 = tr1b.forward(merc_coord2)
+            lon_lat_coord3 = tr2.backward(merc_coord3)
+            lon_lat_coord4 = tr2b.forward(merc_coord4)
+            eq_(math.fabs(coord.x - lon_lat_coord1.x) < 1,True)
+            eq_(math.fabs(coord.x - lon_lat_coord2.x) < 1,True)
+            eq_(math.fabs(coord.x - lon_lat_coord3.x) < 1,True)
+            eq_(math.fabs(coord.x - lon_lat_coord4.x) < 1,True)
+            eq_(math.fabs(coord.y - lon_lat_coord1.y) < 1,True)
+            eq_(math.fabs(coord.y - lon_lat_coord2.y) < 1,True)
+            eq_(math.fabs(coord.y - lon_lat_coord3.y) < 1,True)
+            eq_(math.fabs(coord.y - lon_lat_coord4.y) < 1,True)
+
+
+# Github Issue #2648
+def test_proj_antimeridian_bbox():
+    # this is logic from feature_style_processor::prepare_layer()
+    PROJ_ENVELOPE_POINTS = 20  # include/mapnik/config.hpp
+
+    prjGeog = mapnik.Projection('+init=epsg:4326')
+    prjProj = mapnik.Projection('+init=epsg:2193')
+    prj_trans_fwd = mapnik.ProjTransform(prjProj, prjGeog)
+    prj_trans_rev = mapnik.ProjTransform(prjGeog, prjProj)
+
+    # bad = mapnik.Box2d(-177.31453250437079, -62.33374815225163, 178.02778363316355, -24.584597490955804)
+    better = mapnik.Box2d(-180.0, -62.33374815225163, 180.0, -24.584597490955804)
+
+    buffered_query_ext = mapnik.Box2d(274000, 3087000, 3327000, 7173000)
+    fwd_ext = prj_trans_fwd.forward(buffered_query_ext, PROJ_ENVELOPE_POINTS)
+    assert_box2d_almost_equal(fwd_ext, better)
+
+    # check the same logic works for .backward()
+    ext = mapnik.Box2d(274000, 3087000, 3327000, 7173000)
+    rev_ext = prj_trans_rev.backward(ext, PROJ_ENVELOPE_POINTS)
+    assert_box2d_almost_equal(rev_ext, better)
+
+    # checks for not being snapped (ie. not antimeridian)
+    normal = mapnik.Box2d(148.766759749,-60.1222810238,159.95484893,-24.9771195151)
+    buffered_query_ext = mapnik.Box2d(274000, 3087000, 276000, 7173000)
+    fwd_ext = prj_trans_fwd.forward(buffered_query_ext, PROJ_ENVELOPE_POINTS)
+    assert_box2d_almost_equal(fwd_ext, normal)
+
+    # check the same logic works for .backward()
+    ext = mapnik.Box2d(274000, 3087000, 276000, 7173000)
+    rev_ext = prj_trans_rev.backward(ext, PROJ_ENVELOPE_POINTS)
+    assert_box2d_almost_equal(rev_ext, normal)
+
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/python_plugin_test.py b/test/python_tests/python_plugin_test.py
new file mode 100644
index 0000000..a39272f
--- /dev/null
+++ b/test/python_tests/python_plugin_test.py
@@ -0,0 +1,160 @@
+# #!/usr/bin/env python
+# # -*- coding: utf-8 -*-
+
+# import os
+# import math
+# import mapnik
+# import sys
+# from utilities import execution_path, run_all
+# from nose.tools import *
+
+# def setup():
+#     # All of the paths used are relative, if we run the tests
+#     # from another directory we need to chdir()
+#     os.chdir(execution_path('.'))
+
+# class PointDatasource(mapnik.PythonDatasource):
+#     def __init__(self):
+#         super(PointDatasource, self).__init__(
+#                 geometry_type = mapnik.DataGeometryType.Point,
+#                 envelope = mapnik.Box2d(0,-10,100,110),
+#                 data_type = mapnik.DataType.Vector
+#         )
+
+#     def features(self, query):
+#         return mapnik.PythonDatasource.wkt_features(
+#             keys = ('label',),
+#             features = (
+#                 ( 'POINT (5 6)', { 'label': 'foo-bar'} ),
+#                 ( 'POINT (60 50)', { 'label': 'buzz-quux'} ),
+#             )
+#         )
+
+# class ConcentricCircles(object):
+#     def __init__(self, centre, bounds, step=1):
+#         self.centre = centre
+#         self.bounds = bounds
+#         self.step = step
+
+#     class Iterator(object):
+#         def __init__(self, container):
+#             self.container = container
+
+#             centre = self.container.centre
+#             bounds = self.container.bounds
+#             step = self.container.step
+
+#             self.radius = step
+
+#         def next(self):
+#             points = []
+#             for alpha in xrange(0, 361, 5):
+#                 x = math.sin(math.radians(alpha)) * self.radius + self.container.centre[0]
+#                 y = math.cos(math.radians(alpha)) * self.radius + self.container.centre[1]
+#                 points.append('%s %s' % (x,y))
+#             circle = 'POLYGON ((' + ','.join(points) + '))'
+
+#             # has the circle grown so large that the boundary is entirely within it?
+#             tl = (self.container.bounds.maxx, self.container.bounds.maxy)
+#             tr = (self.container.bounds.maxx, self.container.bounds.maxy)
+#             bl = (self.container.bounds.minx, self.container.bounds.miny)
+#             br = (self.container.bounds.minx, self.container.bounds.miny)
+#             def within_circle(p):
+#                 delta_x = p[0] - self.container.centre[0]
+#                 delta_y = p[0] - self.container.centre[0]
+#                 return delta_x*delta_x + delta_y*delta_y < self.radius*self.radius
+
+#             if all(within_circle(p) for p in (tl,tr,bl,br)):
+#                 raise StopIteration()
+
+#             self.radius += self.container.step
+#             return ( circle, { } )
+
+#     def __iter__(self):
+#         return ConcentricCircles.Iterator(self)
+
+# class CirclesDatasource(mapnik.PythonDatasource):
+#     def __init__(self, centre_x=-20, centre_y=0, step=10):
+#         super(CirclesDatasource, self).__init__(
+#                 geometry_type = mapnik.DataGeometryType.Polygon,
+#                 envelope = mapnik.Box2d(-180, -90, 180, 90),
+#                 data_type = mapnik.DataType.Vector
+#         )
+
+#         # note that the plugin loader will set all arguments to strings and will not try to parse them
+#         centre_x = int(centre_x)
+#         centre_y = int(centre_y)
+#         step = int(step)
+
+#         self.centre_x = centre_x
+#         self.centre_y = centre_y
+#         self.step = step
+
+#     def features(self, query):
+#         centre = (self.centre_x, self.centre_y)
+
+#         return mapnik.PythonDatasource.wkt_features(
+#             keys = (),
+#             features = ConcentricCircles(centre, query.bbox, self.step)
+#         )
+
+# if 'python' in mapnik.DatasourceCache.plugin_names():
+#     # make sure we can load from ourself as a module
+#     sys.path.append(execution_path('.'))
+
+#     def test_python_point_init():
+#         ds = mapnik.Python(factory='python_plugin_test:PointDatasource')
+#         e = ds.envelope()
+
+#         assert_almost_equal(e.minx, 0, places=7)
+#         assert_almost_equal(e.miny, -10, places=7)
+#         assert_almost_equal(e.maxx, 100, places=7)
+#         assert_almost_equal(e.maxy, 110, places=7)
+
+#     def test_python_circle_init():
+#         ds = mapnik.Python(factory='python_plugin_test:CirclesDatasource')
+#         e = ds.envelope()
+
+#         assert_almost_equal(e.minx, -180, places=7)
+#         assert_almost_equal(e.miny, -90, places=7)
+#         assert_almost_equal(e.maxx, 180, places=7)
+#         assert_almost_equal(e.maxy, 90, places=7)
+
+#     def test_python_circle_init_with_args():
+#         ds = mapnik.Python(factory='python_plugin_test:CirclesDatasource', centre_x=40, centre_y=7)
+#         e = ds.envelope()
+
+#         assert_almost_equal(e.minx, -180, places=7)
+#         assert_almost_equal(e.miny, -90, places=7)
+#         assert_almost_equal(e.maxx, 180, places=7)
+#         assert_almost_equal(e.maxy, 90, places=7)
+
+#     def test_python_point_rendering():
+#         m = mapnik.Map(512,512)
+#         mapnik.load_map(m,'../data/python_plugin/python_point_datasource.xml')
+#         m.zoom_all()
+#         im = mapnik.Image(512,512)
+#         mapnik.render(m,im)
+#         actual = '/tmp/mapnik-python-point-render1.png'
+#         expected = 'images/support/mapnik-python-point-render1.png'
+#         im.save(actual)
+#         expected_im = mapnik.Image.open(expected)
+#         eq_(im.tostring('png32'),expected_im.tostring('png32'),
+#                 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+#     def test_python_circle_rendering():
+#         m = mapnik.Map(512,512)
+#         mapnik.load_map(m,'../data/python_plugin/python_circle_datasource.xml')
+#         m.zoom_all()
+#         im = mapnik.Image(512,512)
+#         mapnik.render(m,im)
+#         actual = '/tmp/mapnik-python-circle-render1.png'
+#         expected = 'images/support/mapnik-python-circle-render1.png'
+#         im.save(actual)
+#         expected_im = mapnik.Image.open(expected)
+#         eq_(im.tostring('png32'),expected_im.tostring('png32'),
+#                 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+# if __name__ == "__main__":
+#     setup()
+#     run_all(eval(x) for x in dir() if x.startswith("test_"))
diff --git a/test/python_tests/query_test.py b/test/python_tests/query_test.py
new file mode 100644
index 0000000..8da3534
--- /dev/null
+++ b/test/python_tests/query_test.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+
+from nose.tools import eq_,assert_almost_equal,raises
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_query_init():
+    bbox = (-180, -90, 180, 90)
+    query = mapnik.Query(mapnik.Box2d(*bbox))
+    r = query.resolution
+    assert_almost_equal(r[0], 1.0, places=7)
+    assert_almost_equal(r[1], 1.0, places=7)
+    # https://github.com/mapnik/mapnik/issues/1762
+    eq_(query.property_names,[])
+    query.add_property_name('migurski')
+    eq_(query.property_names,['migurski'])
+
+# Converting *from* tuples *to* resolutions is not yet supported
+ at raises(TypeError)
+def test_query_resolution():
+    bbox = (-180, -90, 180, 90)
+    init_res = (4.5, 6.7)
+    query = mapnik.Query(mapnik.Box2d(*bbox), init_res)
+    r = query.resolution
+    assert_almost_equal(r[0], init_res[0], places=7)
+    assert_almost_equal(r[1], init_res[1], places=7)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/query_tolerance_test.py b/test/python_tests/query_tolerance_test.py
new file mode 100644
index 0000000..97c1b3e
--- /dev/null
+++ b/test/python_tests/query_tolerance_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+    def test_query_tolerance():
+        srs = '+init=epsg:4326'
+        lyr = mapnik.Layer('test')
+        ds = mapnik.Shapefile(file='../data/shp/arrows.shp')
+        lyr.datasource = ds
+        lyr.srs = srs
+        _width = 256
+        _map = mapnik.Map(_width,_width, srs)
+        _map.layers.append(lyr)
+        # zoom determines tolerance
+        _map.zoom_all()
+        _map_env = _map.envelope()
+        tol = (_map_env.maxx - _map_env.minx) / _width * 3
+        # 0.046875 for arrows.shp and zoom_all
+        eq_(tol,0.046875)
+        # check point really exists
+        x, y = 2.0, 4.0
+        features = _map.query_point(0,x,y).features
+        eq_(len(features),1)
+        # check inside tolerance limit
+        x = 2.0 + tol * 0.9
+        features = _map.query_point(0,x,y).features
+        eq_(len(features),1)
+        # check outside tolerance limit
+        x = 2.0 + tol * 1.1
+        features = _map.query_point(0,x,y).features
+        eq_(len(features),0)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/raster_colorizer_test.py b/test/python_tests/raster_colorizer_test.py
new file mode 100644
index 0000000..6fb0102
--- /dev/null
+++ b/test/python_tests/raster_colorizer_test.py
@@ -0,0 +1,90 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+#test discrete colorizer mode
+def test_get_color_discrete():
+    #setup
+    colorizer = mapnik.RasterColorizer();
+    colorizer.default_color = mapnik.Color(0,0,0,0);
+    colorizer.default_mode = mapnik.COLORIZER_DISCRETE;
+
+    colorizer.add_stop(10, mapnik.Color(100,100,100,100));
+    colorizer.add_stop(20, mapnik.Color(200,200,200,200));
+
+    #should be default colour
+    eq_(colorizer.get_color(-50), mapnik.Color(0,0,0,0));
+    eq_(colorizer.get_color(0), mapnik.Color(0,0,0,0));
+
+    #now in stop 1
+    eq_(colorizer.get_color(10), mapnik.Color(100,100,100,100));
+    eq_(colorizer.get_color(19), mapnik.Color(100,100,100,100));
+
+    #now in stop 2
+    eq_(colorizer.get_color(20), mapnik.Color(200,200,200,200));
+    eq_(colorizer.get_color(1000), mapnik.Color(200,200,200,200));
+
+#test exact colorizer mode
+def test_get_color_exact():
+    #setup
+    colorizer = mapnik.RasterColorizer();
+    colorizer.default_color = mapnik.Color(0,0,0,0);
+    colorizer.default_mode = mapnik.COLORIZER_EXACT;
+
+    colorizer.add_stop(10, mapnik.Color(100,100,100,100));
+    colorizer.add_stop(20, mapnik.Color(200,200,200,200));
+
+    #should be default colour
+    eq_(colorizer.get_color(-50), mapnik.Color(0,0,0,0));
+    eq_(colorizer.get_color(11), mapnik.Color(0,0,0,0));
+    eq_(colorizer.get_color(20.001), mapnik.Color(0,0,0,0));
+
+    #should be stop 1
+    eq_(colorizer.get_color(10), mapnik.Color(100,100,100,100));
+
+    #should be stop 2
+    eq_(colorizer.get_color(20), mapnik.Color(200,200,200,200));
+
+#test linear colorizer mode
+def test_get_color_linear():
+    #setup
+    colorizer = mapnik.RasterColorizer();
+    colorizer.default_color = mapnik.Color(0,0,0,0);
+    colorizer.default_mode = mapnik.COLORIZER_LINEAR;
+
+    colorizer.add_stop(10, mapnik.Color(100,100,100,100));
+    colorizer.add_stop(20, mapnik.Color(200,200,200,200));
+
+    #should be default colour
+    eq_(colorizer.get_color(-50), mapnik.Color(0,0,0,0));
+    eq_(colorizer.get_color(9.9), mapnik.Color(0,0,0,0));
+
+    #should be stop 1
+    eq_(colorizer.get_color(10), mapnik.Color(100,100,100,100));
+
+    #should be stop 2
+    eq_(colorizer.get_color(20), mapnik.Color(200,200,200,200));
+
+    #half way between stops 1 and 2
+    eq_(colorizer.get_color(15), mapnik.Color(150,150,150,150));
+
+    #after stop 2
+    eq_(colorizer.get_color(100), mapnik.Color(200,200,200,200));
+
+def test_stop_label():
+    stop = mapnik.ColorizerStop(1, mapnik.COLORIZER_LINEAR, mapnik.Color('red'))
+    assert not stop.label
+    label = u"32º C".encode('utf8')
+    stop.label = label
+    assert stop.label == label, stop.label
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/raster_symbolizer_test.py b/test/python_tests/raster_symbolizer_test.py
new file mode 100644
index 0000000..9092118
--- /dev/null
+++ b/test/python_tests/raster_symbolizer_test.py
@@ -0,0 +1,217 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all, get_unique_colors
+
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+
+def test_dataraster_coloring():
+    srs = '+init=epsg:32630'
+    lyr = mapnik.Layer('dataraster')
+    if 'gdal' in mapnik.DatasourceCache.plugin_names():
+        lyr.datasource = mapnik.Gdal(
+            file = '../data/raster/dataraster.tif',
+            band = 1,
+            )
+        lyr.srs = srs
+        _map = mapnik.Map(256,256, srs)
+        style = mapnik.Style()
+        rule = mapnik.Rule()
+        sym = mapnik.RasterSymbolizer()
+        # Assigning a colorizer to the RasterSymbolizer tells the later
+        # that it should use it to colorize the raw data raster
+        colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_DISCRETE, mapnik.Color("transparent"))
+
+        for value, color in [
+            (  0, "#0044cc"),
+            ( 10, "#00cc00"),
+            ( 20, "#ffff00"),
+            ( 30, "#ff7f00"),
+            ( 40, "#ff0000"),
+            ( 50, "#ff007f"),
+            ( 60, "#ff00ff"),
+            ( 70, "#cc00cc"),
+            ( 80, "#990099"),
+            ( 90, "#660066"),
+            ( 200, "transparent"),
+        ]:
+            colorizer.add_stop(value, mapnik.Color(color))
+        sym.colorizer = colorizer;
+        rule.symbols.append(sym)
+        style.rules.append(rule)
+        _map.append_style('foo', style)
+        lyr.styles.append('foo')
+        _map.layers.append(lyr)
+        _map.zoom_to_box(lyr.envelope())
+
+        im = mapnik.Image(_map.width,_map.height)
+        mapnik.render(_map, im)
+        expected_file = './images/support/dataraster_coloring.png'
+        actual_file = '/tmp/' + os.path.basename(expected_file)
+        im.save(actual_file,'png32')
+        if not os.path.exists(expected_file) or os.environ.get('UPDATE'):
+            im.save(expected_file,'png32')
+        actual = mapnik.Image.open(actual_file)
+        expected = mapnik.Image.open(expected_file)
+        eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+def test_dataraster_query_point():
+    srs = '+init=epsg:32630'
+    lyr = mapnik.Layer('dataraster')
+    if 'gdal' in mapnik.DatasourceCache.plugin_names():
+        lyr.datasource = mapnik.Gdal(
+            file = '../data/raster/dataraster.tif',
+            band = 1,
+            )
+        lyr.srs = srs
+        _map = mapnik.Map(256,256, srs)
+        _map.layers.append(lyr)
+
+        x, y = 556113.0,4381428.0 # center of extent of raster
+        _map.zoom_all()
+        features = _map.query_point(0,x,y).features
+        assert len(features) == 1
+        feat = features[0]
+        center = feat.envelope().center()
+        assert center.x==x and center.y==y, center
+        value = feat['value']
+        assert value == 18.0, value
+
+        # point inside map extent but outside raster extent
+        current_box = _map.envelope()
+        current_box.expand_to_include(-427417,4477517)
+        _map.zoom_to_box(current_box)
+        features = _map.query_point(0,-427417,4477517).features
+        assert len(features) == 0
+
+        # point inside raster extent with nodata
+        features = _map.query_point(0,126850,4596050).features
+        assert len(features) == 0
+
+def test_load_save_map():
+    map = mapnik.Map(256,256)
+    in_map = "../data/good_maps/raster_symbolizer.xml"
+    try:
+        mapnik.load_map(map, in_map)
+
+        out_map = mapnik.save_map_to_string(map)
+        assert 'RasterSymbolizer' in out_map
+        assert 'RasterColorizer' in out_map
+        assert 'stop' in out_map
+    except RuntimeError, e:
+        # only test datasources that we have installed
+        if not 'Could not create datasource' in str(e):
+            raise RuntimeError(str(e))
+
+def test_raster_with_alpha_blends_correctly_with_background():
+    WIDTH = 500
+    HEIGHT = 500
+
+    map = mapnik.Map(WIDTH, HEIGHT)
+    WHITE = mapnik.Color(255, 255, 255)
+    map.background = WHITE
+
+    style = mapnik.Style()
+    rule = mapnik.Rule()
+    symbolizer = mapnik.RasterSymbolizer()
+    symbolizer.scaling = mapnik.scaling_method.BILINEAR
+
+    rule.symbols.append(symbolizer)
+    style.rules.append(rule)
+
+    map.append_style('raster_style', style)
+
+    map_layer = mapnik.Layer('test_layer')
+    filepath = '../data/raster/white-alpha.png'
+    if 'gdal' in mapnik.DatasourceCache.plugin_names():
+        map_layer.datasource = mapnik.Gdal(file=filepath)
+        map_layer.styles.append('raster_style')
+        map.layers.append(map_layer)
+
+        map.zoom_all()
+
+        mim = mapnik.Image(WIDTH, HEIGHT)
+
+        mapnik.render(map, mim)
+        mim.tostring()
+        # All white is expected
+        eq_(get_unique_colors(mim),['rgba(254,254,254,255)'])
+
+def test_raster_warping():
+    lyrSrs = "+init=epsg:32630"
+    mapSrs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+    lyr = mapnik.Layer('dataraster', lyrSrs)
+    if 'gdal' in mapnik.DatasourceCache.plugin_names():
+        lyr.datasource = mapnik.Gdal(
+            file = '../data/raster/dataraster.tif',
+            band = 1,
+            )
+        sym = mapnik.RasterSymbolizer()
+        sym.colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_DISCRETE, mapnik.Color(255,255,0))
+        rule = mapnik.Rule()
+        rule.symbols.append(sym)
+        style = mapnik.Style()
+        style.rules.append(rule)
+        _map = mapnik.Map(256,256, mapSrs)
+        _map.append_style('foo', style)
+        lyr.styles.append('foo')
+        _map.layers.append(lyr)
+        map_proj = mapnik.Projection(mapSrs)
+        layer_proj = mapnik.Projection(lyrSrs)
+        prj_trans = mapnik.ProjTransform(map_proj,
+                                         layer_proj)
+        _map.zoom_to_box(prj_trans.backward(lyr.envelope()))
+
+        im = mapnik.Image(_map.width,_map.height)
+        mapnik.render(_map, im)
+        expected_file = './images/support/raster_warping.png'
+        actual_file = '/tmp/' + os.path.basename(expected_file)
+        im.save(actual_file,'png32')
+        if not os.path.exists(expected_file) or os.environ.get('UPDATE'):
+            im.save(expected_file,'png32')
+        actual = mapnik.Image.open(actual_file)
+        expected = mapnik.Image.open(expected_file)
+        eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+def test_raster_warping_does_not_overclip_source():
+    lyrSrs = "+init=epsg:32630"
+    mapSrs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+    lyr = mapnik.Layer('dataraster', lyrSrs)
+    if 'gdal' in mapnik.DatasourceCache.plugin_names():
+        lyr.datasource = mapnik.Gdal(
+            file = '../data/raster/dataraster.tif',
+            band = 1,
+            )
+        sym = mapnik.RasterSymbolizer()
+        sym.colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_DISCRETE, mapnik.Color(255,255,0))
+        rule = mapnik.Rule()
+        rule.symbols.append(sym)
+        style = mapnik.Style()
+        style.rules.append(rule)
+        _map = mapnik.Map(256,256, mapSrs)
+        _map.background=mapnik.Color('white')
+        _map.append_style('foo', style)
+        lyr.styles.append('foo')
+        _map.layers.append(lyr)
+        _map.zoom_to_box(mapnik.Box2d(3,42,4,43))
+
+        im = mapnik.Image(_map.width,_map.height)
+        mapnik.render(_map, im)
+        expected_file = './images/support/raster_warping_does_not_overclip_source.png'
+        actual_file = '/tmp/' + os.path.basename(expected_file)
+        im.save(actual_file,'png32')
+        if not os.path.exists(expected_file) or os.environ.get('UPDATE'):
+            im.save(expected_file,'png32')
+        actual = mapnik.Image.open(actual_file)
+        expected = mapnik.Image.open(expected_file)
+        eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/rasterlite_test.py b/test/python_tests/rasterlite_test.py
new file mode 100644
index 0000000..b15b157
--- /dev/null
+++ b/test/python_tests/rasterlite_test.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,assert_almost_equal
+from utilities import execution_path, run_all
+
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+
+if 'rasterlite' in mapnik.DatasourceCache.plugin_names():
+
+    def test_rasterlite():
+        ds = mapnik.Rasterlite(
+            file = '../data/rasterlite/globe.sqlite',
+            table = 'globe'
+            )
+        e = ds.envelope()
+
+        assert_almost_equal(e.minx,-180, places=5)
+        assert_almost_equal(e.miny, -90, places=5)
+        assert_almost_equal(e.maxx, 180, places=5)
+        assert_almost_equal(e.maxy,  90, places=5)
+        eq_(len(ds.fields()),0)
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        fs = ds.features(query)
+        feat = fs.next()
+        eq_(feat.id(),1)
+        eq_(feat.attributes,{})
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/render_grid_test.py b/test/python_tests/render_grid_test.py
new file mode 100644
index 0000000..85c7401
--- /dev/null
+++ b/test/python_tests/render_grid_test.py
@@ -0,0 +1,356 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+from utilities import execution_path, run_all
+import os, mapnik
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if mapnik.has_grid_renderer():
+    def show_grids(name,g1,g2):
+        g1_file = '/tmp/mapnik-%s-actual.json' % name
+        open(g1_file,'w').write(json.dumps(g1,sort_keys=True))
+        g2_file = '/tmp/mapnik-%s-expected.json' % name
+        open(g2_file,'w').write(json.dumps(g2,sort_keys=True))
+        val = 'JSON does not match  ->\n'
+        if g1['grid'] != g2['grid']:
+           val += ' X grid does not match\n'
+        else:
+           val += ' ✓ grid matches\n'
+        if g1['data'].keys() != g2['data'].keys():
+           val += ' X data does not match\n'
+        else:
+           val += ' ✓ data matches\n'
+        if g1['keys'] != g2['keys']:
+           val += ' X keys do not\n'
+        else:
+           val += ' ✓ keys match\n'
+        val += '\n\t%s\n\t%s' % (g1_file,g2_file)
+        return val
+
+    def show_grids2(name,g1,g2):
+        g2_expected = '../data/grids/mapnik-%s-actual.json' % name
+        if not os.path.exists(g2_expected):
+            # create test fixture based on actual results
+            open(g2_expected,'a+').write(json.dumps(g1,sort_keys=True))
+            return
+        g1_file = '/tmp/mapnik-%s-actual.json' % name
+        open(g1_file,'w').write(json.dumps(g1,sort_keys=True))
+        val = 'JSON does not match  ->\n'
+        if g1['grid'] != g2['grid']:
+           val += ' X grid does not match\n'
+        else:
+           val += ' ✓ grid matches\n'
+        if g1['data'].keys() != g2['data'].keys():
+           val += ' X data does not match\n'
+        else:
+           val += ' ✓ data matches\n'
+        if g1['keys'] != g2['keys']:
+           val += ' X keys do not\n'
+        else:
+           val += ' ✓ keys match\n'
+        val += '\n\t%s\n\t%s' % (g1_file,g2_expected)
+        return val
+    
+    # previous rendering using agg ellipse directly
+    grid_correct_new = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": ["                                                                ", "                                                                ", "                                                                ", "                                                                ", "                         [...]
+
+    # newer rendering using svg
+    grid_correct_new2 = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": ["                                                                ", "                                                                ", "                                                                ", "                                                                ", "                        [...]
+
+    grid_correct_new3 = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": ["                                                                ", "                                                                ", "                                                                ", "                                                                ", "                        [...]
+
+    def resolve(grid,row,col):
+        """ Resolve the attributes for a given pixel in a grid.
+        """
+        row = grid['grid'][row]
+        utf_val = row[col]
+        #http://docs.python.org/library/functions.html#ord
+        codepoint = ord(utf_val)
+        if (codepoint >= 93):
+            codepoint-=1
+        if (codepoint >= 35):
+            codepoint-=1
+        codepoint -= 32
+        key = grid['keys'][codepoint]
+        return grid['data'].get(key)
+
+
+    def create_grid_map(width,height,sym):
+        ds = mapnik.MemoryDatasource()
+        context = mapnik.Context()
+        context.push('Name')
+        f = mapnik.Feature(context,1)
+        f['Name'] = 'South East'
+        f.geometry = mapnik.Geometry.from_wkt('POINT (143.10 -38.60)')
+        ds.add_feature(f)
+
+        f = mapnik.Feature(context,2)
+        f['Name'] = 'South West'
+        f.geometry = mapnik.Geometry.from_wkt('POINT (142.48 -38.60)')
+        ds.add_feature(f)
+
+        f = mapnik.Feature(context,3)
+        f['Name'] = 'North West'
+        f.geometry = mapnik.Geometry.from_wkt('POINT (142.48 -38.38)')
+        ds.add_feature(f)
+
+        f = mapnik.Feature(context,4)
+        f['Name'] = 'North East'
+        f.geometry = mapnik.Geometry.from_wkt('POINT (143.10 -38.38)')
+        ds.add_feature(f)
+        s = mapnik.Style()
+        r = mapnik.Rule()
+        sym.allow_overlap = True
+        r.symbols.append(sym)
+        s.rules.append(r)
+        lyr = mapnik.Layer('Places')
+        lyr.datasource = ds
+        lyr.styles.append('places_labels')
+        m = mapnik.Map(width,height)
+        m.append_style('places_labels',s)
+        m.layers.append(lyr)
+        return m
+
+
+    def test_render_grid():
+        """ test render_grid method"""
+        width,height = 256,256
+        sym = mapnik.MarkersSymbolizer()
+        sym.width = mapnik.Expression('10')
+        sym.height = mapnik.Expression('10')
+        m = create_grid_map(width,height,sym)
+        ul_lonlat = mapnik.Coord(142.30,-38.20)
+        lr_lonlat = mapnik.Coord(143.40,-38.80)
+        m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+
+        # new method
+        grid = mapnik.Grid(m.width,m.height,key='Name')
+        mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1,grid_correct_new3,show_grids('new-markers',utf1,grid_correct_new3))
+
+        # check a full view is the same as a full image
+        grid_view = grid.view(0,0,width,height)
+        # for kicks check at full res too
+        utf3 = grid.encode('utf',resolution=1)
+        utf4 = grid_view.encode('utf',resolution=1)
+        eq_(utf3['grid'],utf4['grid'])
+        eq_(utf3['keys'],utf4['keys'])
+        eq_(utf3['data'],utf4['data'])
+
+        eq_(resolve(utf4,0,0),None)
+
+        # resolve some center points in the
+        # resampled view
+        utf5 = grid_view.encode('utf',resolution=4)
+        eq_(resolve(utf5,25,10),{"Name": "North West"})
+        eq_(resolve(utf5,25,46),{"Name": "North East"})
+        eq_(resolve(utf5,38,10),{"Name": "South West"})
+        eq_(resolve(utf5,38,46),{"Name": "South East"})
+
+
+    grid_feat_id = {'keys': ['', '3', '4', '2', '1'], 'data': {'1': {'Name': 'South East'}, '3': {'Name': u'North West'}, '2': {'Name': 'South West'}, '4': {'Name': 'North East'}}, 'grid': ['                                                                ', '                                                                ', '                                                                ', '                                                                ', '                              [...]
+
+    grid_feat_id2 = {"data": {"1": {"Name": "South East"}, "2": {"Name": "South West"}, "3": {"Name": "North West"}, "4": {"Name": "North East"}}, "grid": ["                                                                ", "                                                                ", "                                                                ", "                                                                ", "                                                                [...]
+
+    grid_feat_id3 = {"data": {"1": {"Name": "South East", "__id__": 1}, "2": {"Name": "South West", "__id__": 2}, "3": {"Name": "North West", "__id__": 3}, "4": {"Name": "North East", "__id__": 4}}, "grid": ["                                                                ", "                                                                ", "                                                                ", "                                                                ", "            [...]
+
+    def test_render_grid3():
+        """ test using feature id"""
+        width,height = 256,256
+        sym = mapnik.MarkersSymbolizer()
+        sym.width = mapnik.Expression('10')
+        sym.height = mapnik.Expression('10')
+        m = create_grid_map(width,height,sym)
+        ul_lonlat = mapnik.Coord(142.30,-38.20)
+        lr_lonlat = mapnik.Coord(143.40,-38.80)
+        m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+
+        grid = mapnik.Grid(m.width,m.height,key='__id__')
+        mapnik.render_layer(m,grid,layer=0,fields=['__id__','Name'])
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1,grid_feat_id3,show_grids('id-markers',utf1,grid_feat_id3))
+        # check a full view is the same as a full image
+        grid_view = grid.view(0,0,width,height)
+        # for kicks check at full res too
+        utf3 = grid.encode('utf',resolution=1)
+        utf4 = grid_view.encode('utf',resolution=1)
+        eq_(utf3['grid'],utf4['grid'])
+        eq_(utf3['keys'],utf4['keys'])
+        eq_(utf3['data'],utf4['data'])
+
+        eq_(resolve(utf4,0,0),None)
+
+        # resolve some center points in the
+        # resampled view
+        utf5 = grid_view.encode('utf',resolution=4)
+        eq_(resolve(utf5,25,10),{"Name": "North West","__id__": 3})
+        eq_(resolve(utf5,25,46),{"Name": "North East","__id__": 4})
+        eq_(resolve(utf5,38,10),{"Name": "South West","__id__": 2})
+        eq_(resolve(utf5,38,46),{"Name": "South East","__id__": 1})
+
+
+    def gen_grid_for_id(pixel_key):
+        ds = mapnik.MemoryDatasource()
+        context = mapnik.Context()
+        context.push('Name')
+        f = mapnik.Feature(context,pixel_key)
+        f['Name'] = str(pixel_key)
+        f.geometry = mapnik.Geometry.from_wkt('POLYGON ((0 0, 0 256, 256 256, 256 0, 0 0))')
+        ds.add_feature(f)
+        s = mapnik.Style()
+        r = mapnik.Rule()
+        symb = mapnik.PolygonSymbolizer()
+        r.symbols.append(symb)
+        s.rules.append(r)
+        lyr = mapnik.Layer('Places')
+        lyr.datasource = ds
+        lyr.styles.append('places_labels')
+        width,height = 256,256
+        m = mapnik.Map(width,height)
+        m.append_style('places_labels',s)
+        m.layers.append(lyr)
+        m.zoom_all()
+        grid = mapnik.Grid(m.width,m.height,key='__id__')
+        mapnik.render_layer(m,grid,layer=0,fields=['__id__','Name'])
+        return grid
+
+    def test_negative_id():
+        grid = gen_grid_for_id(-1)
+        eq_(grid.get_pixel(128,128),-1)
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1['keys'],['-1'])
+
+    def test_32bit_int_id():
+        int32 = 2147483647
+        grid = gen_grid_for_id(int32)
+        eq_(grid.get_pixel(128,128),int32)
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1['keys'],[str(int32)])
+        max_neg = -(int32)
+        grid = gen_grid_for_id(max_neg)
+        eq_(grid.get_pixel(128,128),max_neg)
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1['keys'],[str(max_neg)])
+
+    def test_64bit_int_id():
+        int64 = 0x7FFFFFFFFFFFFFFF
+        grid = gen_grid_for_id(int64)
+        eq_(grid.get_pixel(128,128),int64)
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1['keys'],[str(int64)])
+        max_neg = -(int64)
+        grid = gen_grid_for_id(max_neg)
+        eq_(grid.get_pixel(128,128),max_neg)
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1['keys'],[str(max_neg)])
+
+    def test_id_zero():
+        grid = gen_grid_for_id(0)
+        eq_(grid.get_pixel(128,128),0)
+        utf1 = grid.encode('utf',resolution=4)
+        eq_(utf1['keys'],['0'])
+
+    line_expected = {"keys": ["", "1"], "data": {"1": {"Name": "1"}}, "grid": ["                                                               !", "                                                            !!  ", "                                                         !!     ", "                                                      !!        ", "                                                   !!           ", "                                                !!              ", "     [...]
+
+    def test_line_rendering():
+        ds = mapnik.MemoryDatasource()
+        context = mapnik.Context()
+        context.push('Name')
+        pixel_key = 1
+        f = mapnik.Feature(context,pixel_key)
+        f['Name'] = str(pixel_key)
+        f.geometry = mapnik.Geometry.from_wkt('LINESTRING (30 10, 10 30, 40 40)')
+        ds.add_feature(f)
+        s = mapnik.Style()
+        r = mapnik.Rule()
+        symb = mapnik.LineSymbolizer()
+        r.symbols.append(symb)
+        s.rules.append(r)
+        lyr = mapnik.Layer('Places')
+        lyr.datasource = ds
+        lyr.styles.append('places_labels')
+        width,height = 256,256
+        m = mapnik.Map(width,height)
+        m.append_style('places_labels',s)
+        m.layers.append(lyr)
+        m.zoom_all()
+        #mapnik.render_to_file(m,'test.png')
+        grid = mapnik.Grid(m.width,m.height,key='__id__')
+        mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+        utf1 = grid.encode()
+        eq_(utf1,line_expected,show_grids('line',utf1,line_expected))
+
+    point_expected = {"data": {"1": {"Name": "South East"}, "2": {"Name": "South West"}, "3": {"Name": "North West"}, "4": {"Name": "North East"}}, "grid": ["                                                                ", "                                                                ", "                                                                ", "                                                                ", "                                                               [...]
+
+    def test_point_symbolizer_grid():
+        width,height = 256,256
+        sym = mapnik.PointSymbolizer()
+        sym.file = '../data/images/dummy.png'
+        m = create_grid_map(width,height,sym)
+        ul_lonlat = mapnik.Coord(142.30,-38.20)
+        lr_lonlat = mapnik.Coord(143.40,-38.80)
+        m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+        grid = mapnik.Grid(m.width,m.height)
+        mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+        utf1 = grid.encode()
+        eq_(utf1,point_expected,show_grids('point-sym',utf1,point_expected))
+
+    test_point_symbolizer_grid.requires_data = True
+
+    # should throw because this is a mis-usage
+    # https://github.com/mapnik/mapnik/issues/1325
+    @raises(RuntimeError)
+    def test_render_to_grid_multiple_times():
+        # create map with two layers
+        m = mapnik.Map(256,256)
+        s = mapnik.Style()
+        r = mapnik.Rule()
+        sym = mapnik.MarkersSymbolizer()
+        sym.allow_overlap = True
+        r.symbols.append(sym)
+        s.rules.append(r)
+        m.append_style('points',s)
+
+        # NOTE: we use a csv datasource here
+        # because the memorydatasource fails silently for
+        # queries requesting fields that do not exist in the datasource
+        ds1 = mapnik.Datasource(**{"type":"csv","inline":'''
+          wkt,Name
+          "POINT (143.10 -38.60)",South East'''})
+        lyr1 = mapnik.Layer('One')
+        lyr1.datasource = ds1
+        lyr1.styles.append('points')
+        m.layers.append(lyr1)
+
+        ds2 = mapnik.Datasource(**{"type":"csv","inline":'''
+          wkt,Value
+          "POINT (142.48 -38.60)",South West'''})
+        lyr2 = mapnik.Layer('Two')
+        lyr2.datasource = ds2
+        lyr2.styles.append('points')
+        m.layers.append(lyr2)
+
+        ul_lonlat = mapnik.Coord(142.30,-38.20)
+        lr_lonlat = mapnik.Coord(143.40,-38.80)
+        m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+        grid = mapnik.Grid(m.width,m.height)
+        mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+        # should throw right here since Name will be a property now on the `grid` object
+        # and it is not found on the second layer
+        mapnik.render_layer(m,grid,layer=1,fields=['Value'])
+        grid.encode()
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/render_test.py b/test/python_tests/render_test.py
new file mode 100644
index 0000000..197d010
--- /dev/null
+++ b/test/python_tests/render_test.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+import tempfile
+import os, mapnik
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_simplest_render():
+    m = mapnik.Map(256, 256)
+    im = mapnik.Image(m.width, m.height)
+    eq_(im.painted(),False)
+    eq_(im.is_solid(),True)
+    mapnik.render(m, im)
+    eq_(im.painted(),False)
+    eq_(im.is_solid(),True)
+    s = im.tostring()
+    eq_(s, 256 * 256 * '\x00\x00\x00\x00')
+
+def test_render_image_to_string():
+    im = mapnik.Image(256, 256)
+    im.fill(mapnik.Color('black'))
+    eq_(im.painted(),False)
+    eq_(im.is_solid(),True)
+    s = im.tostring()
+    eq_(s, 256 * 256 * '\x00\x00\x00\xff')
+
+def test_non_solid_image():
+    im = mapnik.Image(256, 256)
+    im.fill(mapnik.Color('black'))
+    eq_(im.painted(),False)
+    eq_(im.is_solid(),True)
+    # set one pixel to a different color
+    im.set_pixel(0,0,mapnik.Color('white'))
+    eq_(im.painted(),False)
+    eq_(im.is_solid(),False)
+
+def test_non_solid_image_view():
+    im = mapnik.Image(256, 256)
+    im.fill(mapnik.Color('black'))
+    view = im.view(0,0,256,256)
+    eq_(view.is_solid(),True)
+    # set one pixel to a different color
+    im.set_pixel(0,0,mapnik.Color('white'))
+    eq_(im.is_solid(),False)
+    # view, since it is the exact dimensions of the image
+    # should also be non-solid
+    eq_(view.is_solid(),False)
+    # but not a view that excludes the single diff pixel
+    view2 = im.view(1,1,256,256)
+    eq_(view2.is_solid(),True)
+
+def test_setting_alpha():
+    w,h = 256,256
+    im1 = mapnik.Image(w,h)
+    # white, half transparent
+    c1 = mapnik.Color('rgba(255,255,255,.5)')
+    im1.fill(c1)
+    eq_(im1.painted(),False)
+    eq_(im1.is_solid(),True)
+    # pure white
+    im2 = mapnik.Image(w,h)
+    c2 = mapnik.Color('rgba(255,255,255,1)')
+    im2.fill(c2)
+    im2.apply_opacity(c1.a/255.0)
+    eq_(im2.painted(),False)
+    eq_(im2.is_solid(),True)
+    eq_(len(im1.tostring('png32')), len(im2.tostring('png32')))
+
+def test_render_image_to_file():
+    im = mapnik.Image(256, 256)
+    im.fill(mapnik.Color('black'))
+    if mapnik.has_jpeg():
+        im.save('test.jpg')
+    im.save('test.png', 'png')
+    if os.path.exists('test.jpg'):
+        os.remove('test.jpg')
+    else:
+        return False
+    if os.path.exists('test.png'):
+        os.remove('test.png')
+    else:
+        return False
+
+def get_paired_images(w,h,mapfile):
+    tmp_map = 'tmp_map.xml'
+    m = mapnik.Map(w,h)
+    mapnik.load_map(m,mapfile)
+    im = mapnik.Image(w,h)
+    m.zoom_all()
+    mapnik.render(m,im)
+    mapnik.save_map(m,tmp_map)
+    m2 = mapnik.Map(w,h)
+    mapnik.load_map(m2,tmp_map)
+    im2 = mapnik.Image(w,h)
+    m2.zoom_all()
+    mapnik.render(m2,im2)
+    os.remove(tmp_map)
+    return im,im2
+
+def test_render_from_serialization():
+    try:
+        im,im2 = get_paired_images(100,100,'../data/good_maps/building_symbolizer.xml')
+        eq_(im.tostring('png32'),im2.tostring('png32'))
+
+        im,im2 = get_paired_images(100,100,'../data/good_maps/polygon_symbolizer.xml')
+        eq_(im.tostring('png32'),im2.tostring('png32'))
+    except RuntimeError, e:
+        # only test datasources that we have installed
+        if not 'Could not create datasource' in str(e):
+            raise RuntimeError(e)
+
+def test_render_points():
+    if not mapnik.has_cairo(): return
+    # create and populate point datasource (WGS84 lat-lon coordinates)
+    ds = mapnik.MemoryDatasource()
+    context = mapnik.Context()
+    context.push('Name')
+    f = mapnik.Feature(context,1)
+    f['Name'] = 'Westernmost Point'
+    f.geometry = mapnik.Geometry.from_wkt('POINT (142.48 -38.38)')
+    ds.add_feature(f)
+
+    f = mapnik.Feature(context,2)
+    f['Name'] = 'Southernmost Point'
+    f.geometry = mapnik.Geometry.from_wkt('POINT (143.10 -38.60)')
+    ds.add_feature(f)
+
+    # create layer/rule/style
+    s = mapnik.Style()
+    r = mapnik.Rule()
+    symb = mapnik.PointSymbolizer()
+    symb.allow_overlap = True
+    r.symbols.append(symb)
+    s.rules.append(r)
+    lyr = mapnik.Layer('Places','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+    lyr.datasource = ds
+    lyr.styles.append('places_labels')
+    # latlon bounding box corners
+    ul_lonlat = mapnik.Coord(142.30,-38.20)
+    lr_lonlat = mapnik.Coord(143.40,-38.80)
+    # render for different projections
+    projs = {
+        'google': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over',
+        'latlon': '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs',
+        'merc': '+proj=merc +datum=WGS84 +k=1.0 +units=m +over +no_defs',
+        'utm': '+proj=utm +zone=54 +datum=WGS84'
+        }
+    for projdescr in projs.iterkeys():
+        m = mapnik.Map(1000, 500, projs[projdescr])
+        m.append_style('places_labels',s)
+        m.layers.append(lyr)
+        dest_proj = mapnik.Projection(projs[projdescr])
+        src_proj = mapnik.Projection('+init=epsg:4326')
+        tr = mapnik.ProjTransform(src_proj,dest_proj)
+        m.zoom_to_box(tr.forward(mapnik.Box2d(ul_lonlat,lr_lonlat)))
+        # Render to SVG so that it can be checked how many points are there with string comparison
+        svg_file = os.path.join(tempfile.gettempdir(), 'mapnik-render-points-%s.svg' % projdescr)
+        mapnik.render_to_file(m, svg_file)
+        num_points_present = len(ds.all_features())
+        svg = open(svg_file,'r').read()
+        num_points_rendered = svg.count('<image ')
+        eq_(num_points_present, num_points_rendered, "Not all points were rendered (%d instead of %d) at projection %s" % (num_points_rendered, num_points_present, projdescr))
+
+ at raises(RuntimeError)
+def test_render_with_scale_factor_zero_throws():
+    m = mapnik.Map(256,256)
+    im = mapnik.Image(256, 256)
+    mapnik.render(m,im,0.0)
+
+def test_render_with_detector():
+    ds = mapnik.MemoryDatasource()
+    context = mapnik.Context()
+    geojson  = '{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 0, 0 ] } }'
+    ds.add_feature(mapnik.Feature.from_geojson(geojson,context))
+    s = mapnik.Style()
+    r = mapnik.Rule()
+    lyr = mapnik.Layer('point')
+    lyr.datasource = ds
+    lyr.styles.append('point')
+    symb = mapnik.MarkersSymbolizer()
+    symb.allow_overlap = False
+    r.symbols.append(symb)
+    s.rules.append(r)
+    m = mapnik.Map(256,256)
+    m.append_style('point',s)
+    m.layers.append(lyr)
+    m.zoom_to_box(mapnik.Box2d(-180,-85,180,85))
+    im = mapnik.Image(256, 256)
+    mapnik.render(m,im)
+    expected_file = './images/support/marker-in-center.png'
+    actual_file = '/tmp/' + os.path.basename(expected_file)
+    #im.save(expected_file,'png8')
+    im.save(actual_file,'png8')
+    actual = mapnik.Image.open(expected_file)
+    expected = mapnik.Image.open(expected_file)
+    eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+    # now render will a collision detector that should
+    # block out the placement of this point
+    detector = mapnik.LabelCollisionDetector(m)
+    eq_(detector.extent(),mapnik.Box2d(-0.0,-0.0,m.width,m.height))
+    eq_(detector.extent(),mapnik.Box2d(-0.0,-0.0,256.0,256.0))
+    eq_(detector.boxes(),[])
+    detector.insert(detector.extent())
+    eq_(detector.boxes(),[detector.extent()])
+    im2 = mapnik.Image(256, 256)
+    mapnik.render_with_detector(m, im2, detector)
+    expected_file_collision = './images/support/marker-in-center-not-placed.png'
+    #im2.save(expected_file_collision,'png8')
+    actual_file = '/tmp/' + os.path.basename(expected_file_collision)
+    im2.save(actual_file,'png8')
+
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+    def test_render_with_scale_factor():
+        m = mapnik.Map(256,256)
+        mapnik.load_map(m,'../data/good_maps/marker-text-line.xml')
+        m.zoom_all()
+        sizes = [.00001,.005,.1,.899,1,1.5,2,5,10,100]
+        for size in sizes:
+            im = mapnik.Image(256, 256)
+            mapnik.render(m,im,size)
+            expected_file = './images/support/marker-text-line-scale-factor-%s.png' % size
+            actual_file = '/tmp/' + os.path.basename(expected_file)
+            im.save(actual_file,'png32')
+            if os.environ.get('UPDATE'):
+                im.save(expected_file,'png32')
+            # we save and re-open here so both png8 images are ready as full color png
+            actual = mapnik.Image.open(actual_file)
+            expected = mapnik.Image.open(expected_file)
+            eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/reprojection_test.py b/test/python_tests/reprojection_test.py
new file mode 100644
index 0000000..1382db5
--- /dev/null
+++ b/test/python_tests/reprojection_test.py
@@ -0,0 +1,92 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+    #@raises(RuntimeError)
+    def test_zoom_all_will_fail():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+        m.zoom_all()
+
+    def test_zoom_all_will_work_with_max_extent():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+        merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+        m.maximum_extent = merc_bounds
+        m.zoom_all()
+        # note - fixAspectRatio is being called, then re-clipping to maxextent
+        # which makes this hard to predict
+        #eq_(m.envelope(),merc_bounds)
+
+        #m = mapnik.Map(512,512)
+        #mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+        #merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+        #m.zoom_to_box(merc_bounds)
+        #eq_(m.envelope(),merc_bounds)
+
+
+    def test_visual_zoom_all_rendering1():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+        merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+        m.maximum_extent = merc_bounds
+        m.zoom_all()
+        im = mapnik.Image(512,512)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-wgs842merc-reprojection-render.png'
+        expected = 'images/support/mapnik-wgs842merc-reprojection-render.png'
+        im.save(actual,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+    def test_visual_zoom_all_rendering2():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/merc2wgs84_reprojection.xml')
+        m.zoom_all()
+        im = mapnik.Image(512,512)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-merc2wgs84-reprojection-render.png'
+        expected = 'images/support/mapnik-merc2wgs84-reprojection-render.png'
+        im.save(actual,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+    # maximum-extent read from map.xml
+    def test_visual_zoom_all_rendering3():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/bounds_clipping.xml')
+        m.zoom_all()
+        im = mapnik.Image(512,512)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-merc2merc-reprojection-render1.png'
+        expected = 'images/support/mapnik-merc2merc-reprojection-render1.png'
+        im.save(actual,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+    # no maximum-extent
+    def test_visual_zoom_all_rendering4():
+        m = mapnik.Map(512,512)
+        mapnik.load_map(m,'../data/good_maps/bounds_clipping.xml')
+        m.maximum_extent = None
+        m.zoom_all()
+        im = mapnik.Image(512,512)
+        mapnik.render(m,im)
+        actual = '/tmp/mapnik-merc2merc-reprojection-render2.png'
+        expected = 'images/support/mapnik-merc2merc-reprojection-render2.png'
+        im.save(actual,'png32')
+        expected_im = mapnik.Image.open(expected)
+        eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/save_map_test.py b/test/python_tests/save_map_test.py
new file mode 100644
index 0000000..d7c1f03
--- /dev/null
+++ b/test/python_tests/save_map_test.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import tempfile
+
+import os, glob, mapnik
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+    # make the tests silent to suppress unsupported params from harfbuzz tests
+    # TODO: remove this after harfbuzz branch merges
+    mapnik.logger.set_severity(mapnik.severity_type.None)
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def teardown():
+    mapnik.logger.set_severity(default_logging_severity)
+
+def compare_map(xml):
+    m = mapnik.Map(256, 256)
+    absolute_base = os.path.abspath(os.path.dirname(xml))
+    try:
+        mapnik.load_map(m, xml, False, absolute_base)
+    except RuntimeError, e:
+        # only test datasources that we have installed
+        if not 'Could not create datasource' in str(e) \
+           and not 'could not connect' in str(e):
+            raise RuntimeError(str(e))
+        return
+    (handle, test_map) = tempfile.mkstemp(suffix='.xml', prefix='mapnik-temp-map1-')
+    os.close(handle)
+    (handle, test_map2) = tempfile.mkstemp(suffix='.xml', prefix='mapnik-temp-map2-')
+    os.close(handle)
+    if os.path.exists(test_map):
+        os.remove(test_map)
+    mapnik.save_map(m, test_map)
+    new_map = mapnik.Map(256, 256)
+    mapnik.load_map(new_map, test_map,False,absolute_base)
+    open(test_map2,'w').write(mapnik.save_map_to_string(new_map))
+    diff = ' diff -u %s %s' % (os.path.abspath(test_map),os.path.abspath(test_map2))
+    try:
+        eq_(open(test_map).read(),open(test_map2).read())
+    except AssertionError, e:
+        raise AssertionError('serialized map "%s" not the same after being reloaded, \ncompare with command:\n\n$%s' % (xml,diff))
+
+    if os.path.exists(test_map):
+        os.remove(test_map)
+    else:
+        # Fail, the map wasn't written
+        return False
+
+def test_compare_map():
+    good_maps = glob.glob("../data/good_maps/*.xml")
+    good_maps = [os.path.normpath(p) for p in good_maps]
+    # remove one map that round trips CDATA differently, but this is okay
+    ignorable = os.path.join('..','data','good_maps','empty_parameter2.xml')
+    good_maps.remove(ignorable)
+    for m in good_maps:
+        compare_map(m)
+
+    for m in glob.glob("../visual_tests/styles/*.xml"):
+        compare_map(m)
+
+# TODO - enforce that original xml does not equal first saved xml
+def test_compare_map_deprecations():
+    dep = glob.glob("../data/deprecated_maps/*.xml")
+    dep = [os.path.normpath(p) for p in dep]
+    for m in dep:
+        compare_map(m)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/shapefile_test.py b/test/python_tests/shapefile_test.py
new file mode 100644
index 0000000..eccf30c
--- /dev/null
+++ b/test/python_tests/shapefile_test.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal,raises
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+    # Shapefile initialization
+    def test_shapefile_init():
+        s = mapnik.Shapefile(file='../data/shp/boundaries')
+
+        e = s.envelope()
+
+        assert_almost_equal(e.minx, -11121.6896651, places=7)
+        assert_almost_equal(e.miny, -724724.216526, places=6)
+        assert_almost_equal(e.maxx, 2463000.67866, places=5)
+        assert_almost_equal(e.maxy, 1649661.267, places=3)
+
+    # Shapefile properties
+    def test_shapefile_properties():
+        s = mapnik.Shapefile(file='../data/shp/boundaries', encoding='latin1')
+        f = s.features_at_point(s.envelope().center()).features[0]
+
+        eq_(f['CGNS_FID'], u'6f733341ba2011d892e2080020a0f4c9')
+        eq_(f['COUNTRY'], u'CAN')
+        eq_(f['F_CODE'], u'FA001')
+        eq_(f['NAME_EN'], u'Quebec')
+        # this seems to break if icu data linking is not working
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+        eq_(f['Shape_Area'], 1512185733150.0)
+        eq_(f['Shape_Leng'], 19218883.724300001)
+
+    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.Shapefile(file='../data/shp/world_merc')
+        eq_(len(ds.fields()),11)
+        eq_(ds.fields(),['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT'])
+        eq_(ds.field_types(),['str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        # also add an invalid one, triggering throw
+        query.add_property_name('bogus')
+        ds.features(query)
+
+    def test_dbf_logical_field_is_boolean():
+        ds = mapnik.Shapefile(file='../data/shp/long_lat')
+        eq_(len(ds.fields()),7)
+        eq_(ds.fields(),['LONG', 'LAT', 'LOGICAL_TR', 'LOGICAL_FA', 'CHARACTER', 'NUMERIC', 'DATE'])
+        eq_(ds.field_types(),['str', 'str', 'bool', 'bool', 'str', 'float', 'str'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        feat = ds.all_features()[0]
+        eq_(feat.id(),1)
+        eq_(feat['LONG'],'0')
+        eq_(feat['LAT'],'0')
+        eq_(feat['LOGICAL_TR'],True)
+        eq_(feat['LOGICAL_FA'],False)
+        eq_(feat['CHARACTER'],'254')
+        eq_(feat['NUMERIC'],32)
+        eq_(feat['DATE'],'20121202')
+
+    # created by hand in qgis 1.8.0
+    def test_shapefile_point2d_from_qgis():
+        ds = mapnik.Shapefile(file='../data/shp/points/qgis.shp')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['id','name'])
+        eq_(ds.field_types(),['int','str'])
+        eq_(len(ds.all_features()),3)
+
+    # ogr2ogr tests/data/shp/3dpoint/ogr_zfield.shp tests/data/shp/3dpoint/qgis.shp -zfield id
+    def test_shapefile_point_z_from_qgis():
+        ds = mapnik.Shapefile(file='../data/shp/points/ogr_zfield.shp')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['id','name'])
+        eq_(ds.field_types(),['int','str'])
+        eq_(len(ds.all_features()),3)
+
+    def test_shapefile_multipoint_from_qgis():
+        ds = mapnik.Shapefile(file='../data/shp/points/qgis_multi.shp')
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['id','name'])
+        eq_(ds.field_types(),['int','str'])
+        eq_(len(ds.all_features()),1)
+
+    # pointzm from arcinfo
+    def test_shapefile_point_zm_from_arcgis():
+        ds = mapnik.Shapefile(file='../data/shp/points/poi.shp')
+        eq_(len(ds.fields()),7)
+        eq_(ds.fields(),['interst_id', 'state_d', 'cnty_name', 'latitude', 'longitude', 'Name', 'Website'])
+        eq_(ds.field_types(),['str', 'str', 'str', 'float', 'float', 'str', 'str'])
+        eq_(len(ds.all_features()),17)
+
+    # copy of the above with ogr2ogr that makes m record 14 instead of 18
+    def test_shapefile_point_zm_from_ogr():
+        ds = mapnik.Shapefile(file='../data/shp/points/poi_ogr.shp')
+        eq_(len(ds.fields()),7)
+        eq_(ds.fields(),['interst_id', 'state_d', 'cnty_name', 'latitude', 'longitude', 'Name', 'Website'])
+        eq_(ds.field_types(),['str', 'str', 'str', 'float', 'float', 'str', 'str'])
+        eq_(len(ds.all_features()),17)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/shapeindex_test.py b/test/python_tests/shapeindex_test.py
new file mode 100644
index 0000000..4de19a5
--- /dev/null
+++ b/test/python_tests/shapeindex_test.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+from subprocess import Popen, PIPE
+import shutil
+import os
+import fnmatch
+import mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def test_shapeindex():
+    # first copy shapefiles to tmp directory
+    source_dir = '../data/shp/'
+    working_dir = '/tmp/mapnik-shp-tmp/'
+    if os.path.exists(working_dir):
+      shutil.rmtree(working_dir)
+    shutil.copytree(source_dir,working_dir)
+    matches = []
+    for root, dirnames, filenames in os.walk('%s' % source_dir):
+      for filename in fnmatch.filter(filenames, '*.shp'):
+          matches.append(os.path.join(root, filename))
+    for shp in matches:
+      source_file = os.path.join(source_dir,os.path.relpath(shp,source_dir))
+      dest_file = os.path.join(working_dir,os.path.relpath(shp,source_dir))
+      ds = mapnik.Shapefile(file=source_file)
+      count = 0;
+      fs = ds.featureset()
+      try:
+        while (fs.next()):
+          count = count+1
+      except StopIteration:
+        pass
+      stdin, stderr = Popen('shapeindex %s' % dest_file, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+      ds2 = mapnik.Shapefile(file=dest_file)
+      count2 = 0;
+      fs = ds.featureset()
+      try:
+        while (fs.next()):
+          count2 = count2+1
+      except StopIteration:
+        pass
+      eq_(count,count2)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/sqlite_rtree_test.py b/test/python_tests/sqlite_rtree_test.py
new file mode 100644
index 0000000..3036e29
--- /dev/null
+++ b/test/python_tests/sqlite_rtree_test.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import threading
+
+import os, mapnik
+import sqlite3
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+NUM_THREADS = 10
+TOTAL = 245
+
+def create_ds(test_db,table):
+    ds = mapnik.SQLite(file=test_db,table=table)
+    ds.all_features()
+    del ds
+
+if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+
+    def test_rtree_creation():
+        test_db = '../data/sqlite/world.sqlite'
+        index = test_db +'.index'
+        table = 'world_merc'
+
+        if os.path.exists(index):
+            os.unlink(index)
+
+        threads = []
+        for i in range(NUM_THREADS):
+            t = threading.Thread(target=create_ds,args=(test_db,table))
+            t.start()
+            threads.append(t)
+
+        for i in threads:
+            i.join()
+
+        eq_(os.path.exists(index),True)
+        conn = sqlite3.connect(index)
+        cur = conn.cursor()
+        try:
+            cur.execute("Select count(*) from idx_%s_GEOMETRY" % table.replace("'",""))
+            conn.commit()
+            eq_(cur.fetchone()[0],TOTAL)
+        except sqlite3.OperationalError:
+            # don't worry about testing # of index records if
+            # python's sqlite module does not support rtree
+            pass
+        cur.close()
+        conn.close()
+
+        ds = mapnik.SQLite(file=test_db,table=table)
+        fs = ds.all_features()
+        del ds
+        eq_(len(fs),TOTAL)
+        os.unlink(index)
+        ds = mapnik.SQLite(file=test_db,table=table,use_spatial_index=False)
+        fs = ds.all_features()
+        del ds
+        eq_(len(fs),TOTAL)
+        eq_(os.path.exists(index),False)
+
+        ds = mapnik.SQLite(file=test_db,table=table,use_spatial_index=True)
+        fs = ds.all_features()
+        #TODO - this loop is not releasing something
+        # because it causes the unlink below to fail on windows
+        # as the file is still open
+        #for feat in fs:
+        #    query = mapnik.Query(feat.envelope())
+        #    selected = ds.features(query)
+        #    eq_(len(selected.features)>=1,True)
+        del ds
+
+        eq_(os.path.exists(index),True)
+        os.unlink(index)
+    
+    test_rtree_creation.requires_data = True
+
+    def test_geometry_round_trip():
+        test_db = '/tmp/mapnik-sqlite-point.db'
+        ogr_metadata = True
+
+        # create test db
+        conn = sqlite3.connect(test_db)
+        cur = conn.cursor()
+        cur.execute('''
+             CREATE TABLE IF NOT EXISTS point_table
+             (id INTEGER PRIMARY KEY AUTOINCREMENT, geometry BLOB, name varchar)
+             ''')
+        # optional: but nice if we want to read with ogr
+        if ogr_metadata:
+            cur.execute('''CREATE TABLE IF NOT EXISTS geometry_columns (
+                        f_table_name VARCHAR,
+                        f_geometry_column VARCHAR,
+                        geometry_type INTEGER,
+                        coord_dimension INTEGER,
+                        srid INTEGER,
+                        geometry_format VARCHAR )''')
+            cur.execute('''INSERT INTO geometry_columns
+                        (f_table_name, f_geometry_column, geometry_format,
+                        geometry_type, coord_dimension, srid) VALUES
+                        ('point_table','geometry','WKB', 1, 1, 4326)''')
+        conn.commit()
+        cur.close()
+
+        # add a point as wkb (using mapnik) to match how an ogr created db looks
+        x = -122 # longitude
+        y = 48 # latitude
+        wkt = 'POINT(%s %s)' % (x,y)
+        # little endian wkb (mapnik will auto-detect and ready either little or big endian (XDR))
+        wkb = mapnik.Geometry.from_wkt(wkt).to_wkb(mapnik.wkbByteOrder.NDR)
+        values = (None,sqlite3.Binary(wkb),"test point")
+        cur = conn.cursor()
+        cur.execute('''INSERT into "point_table" (id,geometry,name) values (?,?,?)''',values)
+        conn.commit()
+        cur.close()
+        conn.close()
+
+        def make_wkb_point(x,y):
+            import struct
+            byteorder = 1; # little endian
+            endianess = ''
+            if byteorder == 1:
+               endianess = '<'
+            else:
+               endianess = '>'
+            geom_type = 1; # for a point
+            return struct.pack('%sbldd' % endianess, byteorder, geom_type, x, y)
+
+        # confirm the wkb matches a manually formed wkb
+        wkb2 = make_wkb_point(x,y)
+        eq_(wkb,wkb2)
+
+        # ensure we can read this data back out properly with mapnik
+        ds = mapnik.Datasource(**{'type':'sqlite','file':test_db, 'table':'point_table'})
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat.id(),1)
+        eq_(feat['name'],'test point')
+        geom = feat.geometry;
+        eq_(geom.to_wkt(),'POINT(-122 48)')
+        del ds
+
+        # ensure it matches data read with just sqlite
+        conn = sqlite3.connect(test_db)
+        cur = conn.cursor()
+        cur.execute('''SELECT * from point_table''')
+        conn.commit()
+        result = cur.fetchone()
+        cur.close()
+        feat_id = result[0]
+        eq_(feat_id,1)
+        name = result[2]
+        eq_(name,'test point')
+        geom_wkb_blob = result[1]
+        eq_(str(geom_wkb_blob),geom.to_wkb(mapnik.wkbByteOrder.NDR))
+        new_geom = mapnik.Geometry.from_wkb(str(geom_wkb_blob))
+        eq_(new_geom.to_wkt(),geom.to_wkt())
+        conn.close()
+        os.unlink(test_db)
+
+if __name__ == "__main__":
+    setup()
+    returncode = run_all(eval(x) for x in dir() if x.startswith("test_"))
+    exit(returncode)
diff --git a/test/python_tests/sqlite_test.py b/test/python_tests/sqlite_test.py
new file mode 100644
index 0000000..69b8a6d
--- /dev/null
+++ b/test/python_tests/sqlite_test.py
@@ -0,0 +1,501 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_, raises
+from utilities import execution_path, run_all
+import os
+import mapnik
+
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+def teardown():
+    index = '../data/sqlite/world.sqlite.index'
+    if os.path.exists(index):
+        os.unlink(index)
+
+if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+
+    def test_attachdb_with_relative_file():
+        # The point table and index is in the qgis_spatiallite.sqlite
+        # database.  If either is not found, then this fails
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='point',
+            attachdb='scratch at qgis_spatiallite.sqlite'
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['pkuid'],1)
+
+    test_attachdb_with_relative_file.requires_data = True
+
+    def test_attachdb_with_multiple_files():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='attachedtest',
+            attachdb='scratch1@:memory:,scratch2@:memory:',
+            initdb='''
+                create table scratch1.attachedtest (the_geom);
+                create virtual table scratch2.idx_attachedtest_the_geom using rtree(pkid,xmin,xmax,ymin,ymax);
+                insert into scratch2.idx_attachedtest_the_geom values (1,-7799225.5,-7778571.0,1393264.125,1417719.375);
+                '''
+            )
+        fs = ds.featureset()
+        feature = None
+        try :
+            feature = fs.next()
+        except StopIteration:
+            pass
+        # the above should not throw but will result in no features
+        eq_(feature,None)
+    
+    test_attachdb_with_multiple_files.requires_data = True
+
+    def test_attachdb_with_absolute_file():
+        # The point table and index is in the qgis_spatiallite.sqlite
+        # database.  If either is not found, then this fails
+        ds = mapnik.SQLite(file=os.getcwd() + '/../data/sqlite/world.sqlite',
+            table='point',
+            attachdb='scratch at qgis_spatiallite.sqlite'
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['pkuid'],1)
+
+    test_attachdb_with_absolute_file.requires_data = True
+
+    def test_attachdb_with_index():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='attachedtest',
+            attachdb='scratch@:memory:',
+            initdb='''
+                create table scratch.attachedtest (the_geom);
+                create virtual table scratch.idx_attachedtest_the_geom using rtree(pkid,xmin,xmax,ymin,ymax);
+                insert into scratch.idx_attachedtest_the_geom values (1,-7799225.5,-7778571.0,1393264.125,1417719.375);
+                '''
+            )
+
+        fs = ds.featureset()
+        feature = None
+        try :
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+    
+    test_attachdb_with_index.requires_data = True
+
+    def test_attachdb_with_explicit_index():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='attachedtest',
+            index_table='myindex',
+            attachdb='scratch@:memory:',
+            initdb='''
+                create table scratch.attachedtest (the_geom);
+                create virtual table scratch.myindex using rtree(pkid,xmin,xmax,ymin,ymax);
+                insert into scratch.myindex values (1,-7799225.5,-7778571.0,1393264.125,1417719.375);
+                '''
+            )
+        fs = ds.featureset()
+        feature = None
+        try:
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+    
+    test_attachdb_with_explicit_index.requires_data = True
+
+    def test_attachdb_with_sql_join():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3 limit 100)',
+            attachdb='busines at business.sqlite'
+            )
+        eq_(len(ds.fields()),29)
+        eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+        eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature.id(),1)
+        expected = {
+          1995:0,
+          1996:0,
+          1997:0,
+          1998:0,
+          1999:0,
+          2000:0,
+          2001:0,
+          2002:0,
+          2003:0,
+          2004:0,
+          2005:0,
+          2006:0,
+          2007:0,
+          2008:0,
+          2009:0,
+          2010:0,
+          # this appears to be sqlites way of
+          # automatically handling clashing column names
+          'ISO3:1':'ATG',
+          'OGC_FID':1,
+          'area':44,
+          'fips':u'AC',
+          'iso2':u'AG',
+          'iso3':u'ATG',
+          'lat':17.078,
+          'lon':-61.783,
+          'name':u'Antigua and Barbuda',
+          'pop2005':83039,
+          'region':19,
+          'subregion':29,
+          'un':28
+        }
+        for k,v in expected.items():
+            try:
+                eq_(feature[str(k)],v)
+            except:
+                #import pdb;pdb.set_trace()
+                print 'invalid key/v %s/%s for: %s' % (k,v,feature)
+    
+    test_attachdb_with_sql_join.requires_data = True
+
+    def test_attachdb_with_sql_join_count():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3 limit 100)',
+            attachdb='busines at business.sqlite'
+            )
+        eq_(len(ds.fields()),29)
+        eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+        eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+        eq_(len(ds.all_features()),100)
+    
+    test_attachdb_with_sql_join_count.requires_data = True
+
+    def test_attachdb_with_sql_join_count2():
+        '''
+        sqlite3 world.sqlite
+        attach database 'business.sqlite' as business;
+        select count(*) from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+        '''
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+            attachdb='busines at business.sqlite'
+            )
+        eq_(len(ds.fields()),29)
+        eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+        eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+        eq_(len(ds.all_features()),192)
+    
+    test_attachdb_with_sql_join_count2.requires_data = True
+
+    def test_attachdb_with_sql_join_count3():
+        '''
+        select count(*) from (select * from world_merc where 1=1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+        '''
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from (select * from world_merc where !intersects!) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+            attachdb='busines at business.sqlite'
+            )
+        eq_(len(ds.fields()),29)
+        eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+        eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+        eq_(len(ds.all_features()),192)
+    
+    test_attachdb_with_sql_join_count3.requires_data = True
+
+    def test_attachdb_with_sql_join_count4():
+        '''
+        select count(*) from (select * from world_merc where 1=1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+        '''
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from (select * from world_merc where !intersects! limit 1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+            attachdb='busines at business.sqlite'
+            )
+        eq_(len(ds.fields()),29)
+        eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+        eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+        eq_(len(ds.all_features()),1)
+    
+    test_attachdb_with_sql_join_count4.requires_data = True
+
+    def test_attachdb_with_sql_join_count5():
+        '''
+        select count(*) from (select * from world_merc where 1=1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+        '''
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from (select * from world_merc where !intersects! and 1=2) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+            attachdb='busines at business.sqlite'
+            )
+        # nothing is able to join to business so we don't pick up business schema
+        eq_(len(ds.fields()),12)
+        eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat'])
+        eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float'])
+        eq_(len(ds.all_features()),0)
+    
+    test_attachdb_with_sql_join_count5.requires_data = True
+
+    def test_subqueries():
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='world_merc',
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['OGC_FID'],1)
+        eq_(feature['fips'],u'AC')
+        eq_(feature['iso2'],u'AG')
+        eq_(feature['iso3'],u'ATG')
+        eq_(feature['un'],28)
+        eq_(feature['name'],u'Antigua and Barbuda')
+        eq_(feature['area'],44)
+        eq_(feature['pop2005'],83039)
+        eq_(feature['region'],19)
+        eq_(feature['subregion'],29)
+        eq_(feature['lon'],-61.783)
+        eq_(feature['lat'],17.078)
+
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select * from world_merc)',
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['OGC_FID'],1)
+        eq_(feature['fips'],u'AC')
+        eq_(feature['iso2'],u'AG')
+        eq_(feature['iso3'],u'ATG')
+        eq_(feature['un'],28)
+        eq_(feature['name'],u'Antigua and Barbuda')
+        eq_(feature['area'],44)
+        eq_(feature['pop2005'],83039)
+        eq_(feature['region'],19)
+        eq_(feature['subregion'],29)
+        eq_(feature['lon'],-61.783)
+        eq_(feature['lat'],17.078)
+
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select OGC_FID,GEOMETRY from world_merc)',
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['OGC_FID'],1)
+        eq_(len(feature),1)
+
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select GEOMETRY,OGC_FID,fips from world_merc)',
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['OGC_FID'],1)
+        eq_(feature['fips'],u'AC')
+
+        # same as above, except with alias like postgres requires
+        # TODO - should we try to make this work?
+        #ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+        #    table='(select GEOMETRY,rowid as aliased_id,fips from world_merc) as table',
+        #    key_field='aliased_id'
+        #    )
+        #fs = ds.featureset()
+        #feature = fs.next()
+        #eq_(feature['aliased_id'],1)
+        #eq_(feature['fips'],u'AC')
+
+        ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+            table='(select GEOMETRY,OGC_FID,OGC_FID as rowid,fips from world_merc)',
+            )
+        fs = ds.featureset()
+        feature = fs.next()
+        eq_(feature['rowid'],1)
+        eq_(feature['fips'],u'AC')
+    
+    test_subqueries.requires_data = True
+
+    def test_empty_db():
+        ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+            table='empty',
+            )
+        fs = ds.featureset()
+        feature = None
+        try:
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+
+    test_empty_db.requires_data = True
+
+    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+            table='empty',
+            )
+        eq_(len(ds.fields()),25)
+        eq_(ds.fields(),['OGC_FID', 'scalerank', 'labelrank', 'featurecla', 'sovereignt', 'sov_a3', 'adm0_dif', 'level', 'type', 'admin', 'adm0_a3', 'geou_dif', 'name', 'abbrev', 'postal', 'name_forma', 'terr_', 'name_sort', 'map_color', 'pop_est', 'gdp_md_est', 'fips_10_', 'iso_a2', 'iso_a3', 'iso_n3'])
+        eq_(ds.field_types(),['int', 'int', 'int', 'str', 'str', 'str', 'float', 'float', 'str', 'str', 'str', 'float', 'str', 'str', 'str', 'str', 'str', 'str', 'float', 'float', 'float', 'float', 'str', 'str', 'float'])
+        query = mapnik.Query(ds.envelope())
+        for fld in ds.fields():
+            query.add_property_name(fld)
+        # also add an invalid one, triggering throw
+        query.add_property_name('bogus')
+        ds.features(query)
+    
+    test_that_nonexistant_query_field_throws.requires_data = True
+
+    def test_intersects_token1():
+        ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+            table='(select * from empty where !intersects!)',
+            )
+        fs = ds.featureset()
+        feature = None
+        try :
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+    
+    test_intersects_token1.requires_data = True
+
+    def test_intersects_token2():
+        ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+            table='(select * from empty where "a"!="b" and !intersects!)',
+            )
+        fs = ds.featureset()
+        feature = None
+        try :
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+    
+    test_intersects_token2.requires_data = True
+
+    def test_intersects_token3():
+        ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+            table='(select * from empty where "a"!="b" and !intersects!)',
+            )
+        fs = ds.featureset()
+        feature = None
+        try :
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+    
+    test_intersects_token3.requires_data = True
+
+    # https://github.com/mapnik/mapnik/issues/1537
+    # this works because key_field is manually set
+    def test_db_with_one_text_column():
+        # form up an in-memory test db
+        wkb = '010100000000000000000000000000000000000000'
+        ds = mapnik.SQLite(file=':memory:',
+            table='test1',
+            initdb='''
+                create table test1 (alias TEXT,geometry BLOB);
+                insert into test1 values ("test",x'%s');
+                ''' % wkb,
+            extent='-180,-60,180,60',
+            use_spatial_index=False,
+            key_field='alias'
+        )
+        eq_(len(ds.fields()),1)
+        eq_(ds.fields(),['alias'])
+        eq_(ds.field_types(),['str'])
+        fs = ds.all_features()
+        eq_(len(fs),1)
+        feat = fs[0]
+        eq_(feat.id(),0) # should be 1?
+        eq_(feat['alias'],'test')
+        eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+    def test_db_with_one_untyped_column():
+        # form up an in-memory test db
+        wkb = '010100000000000000000000000000000000000000'
+        ds = mapnik.SQLite(file=':memory:',
+            table='test1',
+            initdb='''
+                create table test1 (geometry BLOB, untyped);
+                insert into test1 values (x'%s', 'untyped');
+            ''' % wkb,
+            extent='-180,-60,180,60',
+            use_spatial_index=False,
+            key_field='rowid'
+        )
+
+        # ensure the untyped column is found
+        eq_(len(ds.fields()),2)
+        eq_(ds.fields(),['rowid', 'untyped'])
+        eq_(ds.field_types(),['int', 'str'])
+
+    def test_db_with_one_untyped_column_using_subquery():
+        # form up an in-memory test db
+        wkb = '010100000000000000000000000000000000000000'
+        ds = mapnik.SQLite(file=':memory:',
+            table='(SELECT rowid, geometry, untyped FROM test1)',
+            initdb='''
+                create table test1 (geometry BLOB, untyped);
+                insert into test1 values (x'%s', 'untyped');
+            ''' % wkb,
+            extent='-180,-60,180,60',
+            use_spatial_index=False,
+            key_field='rowid'
+        )
+
+        # ensure the untyped column is found
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['rowid', 'untyped', 'rowid'])
+        eq_(ds.field_types(),['int', 'str', 'int'])
+
+
+    def test_that_64bit_int_fields_work():
+        ds = mapnik.SQLite(file='../data/sqlite/64bit_int.sqlite',
+            table='int_table',
+            use_spatial_index=False
+        )
+        eq_(len(ds.fields()),3)
+        eq_(ds.fields(),['OGC_FID','id','bigint'])
+        eq_(ds.field_types(),['int','int','int'])
+        fs = ds.featureset()
+        feat = fs.next()
+        eq_(feat.id(),1)
+        eq_(feat['OGC_FID'],1)
+        eq_(feat['bigint'],2147483648)
+        feat = fs.next()
+        eq_(feat.id(),2)
+        eq_(feat['OGC_FID'],2)
+        eq_(feat['bigint'],922337203685477580)
+
+    test_that_64bit_int_fields_work.requires_data = True
+
+    def test_null_id_field():
+        # silence null key warning: https://github.com/mapnik/mapnik/issues/1889
+        default_logging_severity = mapnik.logger.get_severity()
+        mapnik.logger.set_severity(mapnik.severity_type.None)
+        # form up an in-memory test db
+        wkb = '010100000000000000000000000000000000000000'
+        # note: the osm_id should be declared INTEGER PRIMARY KEY
+        # but in this case we intentionally do not make this a valid pkey
+        # otherwise sqlite would turn the null into a valid, serial id
+        ds = mapnik.SQLite(file=':memory:',
+            table='test1',
+            initdb='''
+                create table test1 (osm_id INTEGER,geometry BLOB);
+                insert into test1 values (null,x'%s');
+                ''' % wkb,
+            extent='-180,-60,180,60',
+            use_spatial_index=False,
+            key_field='osm_id'
+        )
+        fs = ds.featureset()
+        feature = None
+        try :
+            feature = fs.next()
+        except StopIteration:
+            pass
+        eq_(feature,None)
+        mapnik.logger.set_severity(default_logging_severity)
+
+if __name__ == "__main__":
+    setup()
+    result = run_all(eval(x) for x in dir() if x.startswith("test_"))
+    teardown()
+    exit(result)
diff --git a/test/python_tests/style_test.py b/test/python_tests/style_test.py
new file mode 100644
index 0000000..7bc782a
--- /dev/null
+++ b/test/python_tests/style_test.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+
+def test_style_init():
+   s = mapnik.Style()
+   eq_(s.filter_mode,mapnik.filter_mode.ALL)
+   eq_(len(s.rules),0)
+   eq_(s.opacity,1)
+   eq_(s.comp_op,None)
+   eq_(s.image_filters,"")
+   eq_(s.image_filters_inflate,False)
+
+if __name__ == "__main__":
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/topojson_plugin_test.py b/test/python_tests/topojson_plugin_test.py
new file mode 100644
index 0000000..a5f3e57
--- /dev/null
+++ b/test/python_tests/topojson_plugin_test.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal
+from utilities import execution_path, run_all
+import os, mapnik
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if 'topojson' in mapnik.DatasourceCache.plugin_names():
+
+    def test_topojson_init():
+        # topojson tests/data/json/escaped.geojson -o tests/data/json/escaped.topojson --properties
+        # topojson version 1.4.2
+        ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+        e = ds.envelope()
+        assert_almost_equal(e.minx, -81.705583, places=7)
+        assert_almost_equal(e.miny, 41.480573, places=6)
+        assert_almost_equal(e.maxx, -81.705583, places=5)
+        assert_almost_equal(e.maxy, 41.480573, places=3)
+
+    def test_topojson_properties():
+        ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+        f = ds.features_at_point(ds.envelope().center()).features[0]
+        eq_(len(ds.fields()),7)
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+
+        ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+        f = ds.all_features()[0]
+        eq_(len(ds.fields()),7)
+
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+
+    def test_geojson_from_in_memory_string():
+        ds = mapnik.Datasource(type='topojson',inline=open('../data/json/escaped.topojson','r').read())
+        f = ds.all_features()[0]
+        eq_(len(ds.fields()),7)
+
+        desc = ds.describe()
+        eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+        eq_(f['name'], u'Test')
+        eq_(f['int'], 1)
+        eq_(f['description'], u'Test: \u005C')
+        eq_(f['spaces'], u'this has spaces')
+        eq_(f['double'], 1.1)
+        eq_(f['boolean'], True)
+        eq_(f['NOM_FR'], u'Qu\xe9bec')
+        eq_(f['NOM_FR'], u'Québec')
+
+#    @raises(RuntimeError)
+    def test_that_nonexistant_query_field_throws(**kwargs):
+        ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+        eq_(len(ds.fields()),7)
+        # TODO - this sorting is messed up
+        eq_(ds.fields(),['name', 'int', 'description', 'spaces', 'double', 'boolean', 'NOM_FR'])
+        eq_(ds.field_types(),['str', 'int', 'str', 'str', 'float', 'bool', 'str'])
+# TODO - should topojson plugin throw like others?
+#        query = mapnik.Query(ds.envelope())
+#        for fld in ds.fields():
+#            query.add_property_name(fld)
+#        # also add an invalid one, triggering throw
+#        query.add_property_name('bogus')
+#        fs = ds.features(query)
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/utilities.py b/test/python_tests/utilities.py
new file mode 100644
index 0000000..fe02c7d
--- /dev/null
+++ b/test/python_tests/utilities.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin
+from nose.tools import assert_almost_equal
+
+import os, sys, traceback
+import mapnik
+
+HERE = os.path.dirname(__file__)
+
+def execution_path(filename):
+    return os.path.join(os.path.dirname(sys._getframe(1).f_code.co_filename), filename)
+
+class Todo(Exception):
+    pass
+
+class TodoPlugin(ErrorClassPlugin):
+    name = "todo"
+
+    todo = ErrorClass(Todo, label='TODO', isfailure=False)
+
+def contains_word(word, bytestring_):
+    """
+    Checks that a bytestring contains a given word. len(bytestring) should be
+    a multiple of len(word).
+
+    >>> contains_word("abcd", "abcd"*5)
+    True
+
+    >>> contains_word("ab", "ba"*5)
+    False
+
+    >>> contains_word("ab", "ab"*5+"a")
+    Traceback (most recent call last):
+    ...
+    AssertionError: len(bytestring_) not multiple of len(word)
+    """
+    n = len(word)
+    assert len(bytestring_)%n == 0, "len(bytestring_) not multiple of len(word)"
+    chunks = [bytestring_[i:i+n] for i in xrange(0, len(bytestring_), n)]
+    return word in chunks
+
+def pixel2channels(pixel):
+    alpha = (pixel >> 24) & 0xff
+    red = pixel & 0xff
+    green = (pixel >> 8) & 0xff
+    blue = (pixel >> 16) & 0xff
+    return red,green,blue,alpha
+
+def pixel2rgba(pixel):
+    return 'rgba(%s,%s,%s,%s)' % pixel2channels(pixel)
+
+def get_unique_colors(im):
+    pixels = []
+    for x in range(im.width()):
+        for y in range(im.height()):
+            pixel = im.get_pixel(x,y)
+            if pixel not in pixels:
+                 pixels.append(pixel)
+    pixels = sorted(pixels)
+    return map(pixel2rgba,pixels)
+
+def run_all(iterable):
+    failed = 0
+    for test in iterable:
+        try:
+            test()
+            sys.stderr.write("\x1b[32m✓ \x1b[m" + test.__name__ + "\x1b[m\n")
+        except:
+            exc_type, exc_value, exc_tb = sys.exc_info()
+            failed += 1
+            sys.stderr.write("\x1b[31m✘ \x1b[m" + test.__name__ + "\x1b[m\n")
+            for mline in traceback.format_exception_only(exc_type, exc_value):
+                for line in mline.rstrip().split("\n"):
+                    sys.stderr.write("  \x1b[31m" + line + "\x1b[m\n")
+            sys.stderr.write("  Traceback:\n")
+            for mline in traceback.format_tb(exc_tb):
+                for line in mline.rstrip().split("\n"):
+                    if not 'utilities.py' in line and not 'trivial.py' in line and not line.strip() == 'test()':
+                        sys.stderr.write("  " + line + "\n")
+        sys.stderr.flush()
+    return failed
+
+def side_by_side_image(left_im, right_im):
+    width = left_im.width() + 1 + right_im.width()
+    height = max(left_im.height(), right_im.height())
+    im = mapnik.Image(width, height)
+    im.composite(left_im,mapnik.CompositeOp.src_over,1.0,0,0)
+    if width > 80:
+       im.composite(mapnik.Image.open(HERE+'/images/expected.png'),mapnik.CompositeOp.difference,1.0,0,0)
+    im.composite(right_im,mapnik.CompositeOp.src_over,1.0,left_im.width() + 1, 0)
+    if width > 80:
+       im.composite(mapnik.Image.open(HERE+'/images/actual.png'),mapnik.CompositeOp.difference,1.0,left_im.width() + 1, 0)
+    return im
+
+def assert_box2d_almost_equal(a, b, msg=None):
+    msg = msg or ("%r != %r" % (a, b))
+    assert_almost_equal(a.minx, b.minx, msg=msg)
+    assert_almost_equal(a.maxx, b.maxx, msg=msg)
+    assert_almost_equal(a.miny, b.miny, msg=msg)
+    assert_almost_equal(a.maxy, b.maxy, msg=msg)
diff --git a/test/python_tests/webp_encoding_test.py b/test/python_tests/webp_encoding_test.py
new file mode 100644
index 0000000..91e23fc
--- /dev/null
+++ b/test/python_tests/webp_encoding_test.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import raises,eq_
+from utilities import execution_path, run_all
+
+def setup():
+    # All of the paths used are relative, if we run the tests
+    # from another directory we need to chdir()
+    os.chdir(execution_path('.'))
+
+if mapnik.has_webp():
+    tmp_dir = '/tmp/mapnik-webp/'
+    if not os.path.exists(tmp_dir):
+       os.makedirs(tmp_dir)
+
+    opts = [
+        'webp',
+        'webp:method=0',
+        'webp:method=6',
+        'webp:quality=64',
+        'webp:alpha=false',
+        'webp:partitions=3',
+        'webp:preprocessing=1',
+        'webp:partition_limit=50',
+        'webp:pass=10',
+        'webp:alpha_quality=50',
+        'webp:alpha_filtering=2',
+        'webp:alpha_compression=0',
+        'webp:autofilter=0',
+        'webp:filter_type=1:autofilter=1',
+        'webp:filter_sharpness=4',
+        'webp:filter_strength=50',
+        'webp:sns_strength=50',
+        'webp:segments=3',
+        'webp:target_PSNR=.5',
+        'webp:target_size=100'
+    ]
+
+
+    def gen_filepath(name,format):
+        return os.path.join('images/support/encoding-opts',name+'-'+format.replace(":","+")+'.webp')
+
+    def test_quality_threshold():
+        im = mapnik.Image(256,256)
+        im.tostring('webp:quality=99.99000')
+        im.tostring('webp:quality=0')
+        im.tostring('webp:quality=0.001')
+
+    @raises(RuntimeError)
+    def test_quality_threshold_invalid():
+        im = mapnik.Image(256,256)
+        im.tostring('webp:quality=101')
+
+    @raises(RuntimeError)
+    def test_quality_threshold_invalid2():
+        im = mapnik.Image(256,256)
+        im.tostring('webp:quality=-1')
+    
+    @raises(RuntimeError)
+    def test_quality_threshold_invalid3():
+        im = mapnik.Image(256,256)
+        im.tostring('webp:quality=101.1')
+
+    generate = os.environ.get('UPDATE')
+
+    def test_expected_encodings():
+        fails = []
+        try:
+            for opt in opts:
+                im = mapnik.Image(256,256)
+                expected = gen_filepath('blank',opt)
+                actual = os.path.join(tmp_dir,os.path.basename(expected))
+                if generate or not os.path.exists(expected):
+                    print 'generating expected image %s' % expected
+                    im.save(expected,opt)
+                im.save(actual,opt)
+                try:
+                    expected_bytes = mapnik.Image.open(expected).tostring()
+                except RuntimeError:
+                    # this will happen if libweb is old, since it cannot open images created by more recent webp
+                    print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+                    continue
+                if mapnik.Image.open(actual).tostring() != expected_bytes:
+                    fails.append('%s (actual) not == to %s (expected)' % (actual,expected))
+
+            for opt in opts:
+                im = mapnik.Image(256,256)
+                im.fill(mapnik.Color('green'))
+                expected = gen_filepath('solid',opt)
+                actual = os.path.join(tmp_dir,os.path.basename(expected))
+                if generate or not os.path.exists(expected):
+                    print 'generating expected image %s' % expected
+                    im.save(expected,opt)
+                im.save(actual,opt)
+                try:
+                    expected_bytes = mapnik.Image.open(expected).tostring()
+                except RuntimeError:
+                    # this will happen if libweb is old, since it cannot open images created by more recent webp
+                    print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+                    continue
+                if mapnik.Image.open(actual).tostring() != expected_bytes:
+                    fails.append('%s (actual) not == to %s (expected)' % (actual,expected))
+
+            for opt in opts:
+                im = mapnik.Image.open('images/support/transparency/aerial_rgba.png')
+                expected = gen_filepath('aerial_rgba',opt)
+                actual = os.path.join(tmp_dir,os.path.basename(expected))
+                if generate or not os.path.exists(expected):
+                    print 'generating expected image %s' % expected
+                    im.save(expected,opt)
+                im.save(actual,opt)
+                try:
+                    expected_bytes = mapnik.Image.open(expected).tostring()
+                except RuntimeError:
+                    # this will happen if libweb is old, since it cannot open images created by more recent webp
+                    print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+                    continue
+                if mapnik.Image.open(actual).tostring() != expected_bytes:
+                    fails.append('%s (actual) not == to %s (expected)' % (actual,expected))
+            # disabled to avoid failures on ubuntu when using old webp packages
+            #eq_(fails,[],'\n'+'\n'.join(fails))
+        except RuntimeError, e:
+            print e
+
+    def test_transparency_levels():
+        try:
+            # create partial transparency image
+            im = mapnik.Image(256,256)
+            im.fill(mapnik.Color('rgba(255,255,255,.5)'))
+            c2 = mapnik.Color('rgba(255,255,0,.2)')
+            c3 = mapnik.Color('rgb(0,255,255)')
+            for y in range(0,im.height()/2):
+                for x in range(0,im.width()/2):
+                    im.set_pixel(x,y,c2)
+            for y in range(im.height()/2,im.height()):
+                for x in range(im.width()/2,im.width()):
+                    im.set_pixel(x,y,c3)
+
+            t0 = tmp_dir + 'white0-actual.webp'
+
+            # octree
+            format = 'webp'
+            expected = 'images/support/transparency/white0.webp'
+            if generate or not os.path.exists(expected):
+                im.save('images/support/transparency/white0.webp')
+            im.save(t0,format)
+            im_in = mapnik.Image.open(t0)
+            t0_len = len(im_in.tostring(format))
+            try:
+                expected_bytes = mapnik.Image.open(expected).tostring(format)
+            except RuntimeError:
+                # this will happen if libweb is old, since it cannot open images created by more recent webp
+                print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+                return
+            eq_(t0_len,len(expected_bytes))
+        except RuntimeError, e:
+            print e
+
+
+if __name__ == "__main__":
+    setup()
+    exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/run_tests.py b/test/run_tests.py
new file mode 100755
index 0000000..edf7974
--- /dev/null
+++ b/test/run_tests.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+import sys
+
+try:
+    import nose
+except ImportError, e:
+    sys.stderr.write("Unable to run python tests: the third party 'nose' module is required\nTo install 'nose' do:\n\tsudo pip install nose (or on debian systems: apt-get install python-nose): %s\n" % e)
+    sys.exit(1)
+
+import mapnik    
+from python_tests.utilities import TodoPlugin
+from nose.plugins.doctests import Doctest
+
+import nose, sys, os, getopt
+
+def usage():
+    print("test.py -h | --help")
+    print("test.py [-q | -v] [-p | --prefix <path>]")
+
+def main():
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "hvqp:", ["help", "prefix="])
+    except getopt.GetoptError,err:
+        print(str(err))
+        usage()
+        sys.exit(2)
+
+    prefix = None
+    verbose = False
+    quiet = False
+
+    for o, a in opts:
+        if o == "-q":
+            quiet = True
+        elif o == "-v":
+            verbose = True
+        elif o in ("-h", "--help"):
+            usage()
+            sys.exit()
+        elif o in ("-p", "--prefix"):
+            prefix = a
+        else:
+            assert False, "Unhandled option"
+
+    if quiet and verbose:
+        usage()
+        sys.exit(2)
+
+    if prefix:
+        # Allow python to find libraries for testing on the buildbot
+        sys.path.insert(0, os.path.join(prefix, "lib/python%s/site-packages" % sys.version[:3]))
+
+    import mapnik
+
+    if not quiet:
+        print("- mapnik path: %s" % mapnik.__file__)
+        if hasattr(mapnik,'_mapnik'):
+           print("- _mapnik.so path: %s" % mapnik._mapnik.__file__)
+        if hasattr(mapnik,'inputpluginspath'):
+            print ("- Input plugins path: %s" % mapnik.inputpluginspath)
+        if os.environ.has_key('MAPNIK_INPUT_PLUGINS_DIRECTORY'):
+            print ("- MAPNIK_INPUT_PLUGINS_DIRECTORY env: %s" % os.environ.get('MAPNIK_INPUT_PLUGINS_DIRECTORY'))
+        if hasattr(mapnik,'fontscollectionpath'):
+            print("- Font path: %s" % mapnik.fontscollectionpath)
+        if os.environ.has_key('MAPNIK_FONT_DIRECTORY'):
+            print ("- MAPNIK_FONT_DIRECTORY env: %s" % os.environ.get('MAPNIK_FONT_DIRECTORY'))
+        print('')
+        print("- Running nosetests:")
+        print('')
+
+    argv = [__file__, '--exe', '--with-todo', '--with-doctest', '--doctest-tests']
+
+    if not quiet:
+        argv.append('-v')
+
+    if verbose:
+        # 3 * '-v' gets us debugging information from nose
+        argv.append('-v')
+        argv.append('-v')
+
+    dirname = os.path.dirname(sys.argv[0])
+    argv.extend(['-w', os.path.join(dirname,'python_tests')])
+
+    if not nose.run(argv=argv, plugins=[TodoPlugin(), Doctest()]):
+        sys.exit(1)
+    else:
+        sys.exit(0)
+
+if __name__ == "__main__":
+    main()
diff --git a/test/visual.py b/test/visual.py
new file mode 100755
index 0000000..32ad7f4
--- /dev/null
+++ b/test/visual.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import mapnik
+import shutil
+import platform
+import glob
+
+#mapnik.logger.set_severity(mapnik.severity_type.None)
+#mapnik.logger.set_severity(mapnik.severity_type.Debug)
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+visual_output_dir = "/tmp/mapnik-visual-images"
+
+defaults = {
+    'status': True,
+    'sizes': [(500, 100)],
+    'scales':[1.0,2.0],
+    'agg': True,
+    'cairo': mapnik.has_cairo(),
+    'grid': mapnik.has_grid_renderer()
+}
+
+cairo_threshold = 10
+agg_threshold = 0
+grid_threshold = 5
+if 'Linux' == platform.uname()[0]:
+    # we assume if linux then you are running packaged cairo
+    # which is older than the 1.12.14 version we used on OS X
+    # to generate the expected images, so we'll rachet back the threshold
+    # https://github.com/mapnik/mapnik/issues/1868
+    cairo_threshold = 230
+    agg_threshold = 12
+    grid_threshold = 6
+
+def render_cairo(m, output, scale_factor):
+    mapnik.render_to_file(m, output, 'ARGB32', scale_factor)
+    # open and re-save as png8 to save space
+    new_im = mapnik.Image.open(output)
+    new_im.save(output, 'png32')
+
+def render_grid(m, output, scale_factor):
+    grid = mapnik.Grid(m.width, m.height)
+    mapnik.render_layer(m, grid, layer=0, scale_factor=scale_factor)
+    utf1 = grid.encode('utf', resolution=4)
+    open(output,'wb').write(json.dumps(utf1, indent=1))
+
+def render_agg(m, output, scale_factor):
+    mapnik.render_to_file(m, output, 'png32', scale_factor),
+
+renderers = [
+    { 'name': 'agg',
+      'render': render_agg,
+      'compare': lambda actual, reference: compare(actual, reference, alpha=True),
+      'threshold': agg_threshold,
+      'filetype': 'png',
+      'dir': 'images'
+    },
+    { 'name': 'cairo',
+      'render': render_cairo,
+      'compare': lambda actual, reference: compare(actual, reference, alpha=False),
+      'threshold': cairo_threshold,
+      'filetype': 'png',
+      'dir': 'images'
+    },
+    { 'name': 'grid',
+      'render': render_grid,
+      'compare': lambda actual, reference: compare_grids(actual, reference, alpha=False),
+      'threshold': grid_threshold,
+      'filetype': 'json',
+      'dir': 'grids'
+    }
+]
+
+COMPUTE_THRESHOLD = 16
+
+# testcase images are generated on OS X
+# so they should exactly match
+if platform.uname()[0] == 'Darwin':
+    COMPUTE_THRESHOLD = 2
+
+# compare two images and return number of different pixels
+def compare(actual, expected, alpha=True):
+    im1 = mapnik.Image.open(actual)
+    im2 = mapnik.Image.open(expected)
+    return im1.compare(im2,COMPUTE_THRESHOLD, alpha)
+
+def compare_grids(actual, expected, threshold=0, alpha=True):
+    global errors
+    global passed
+    im1 = json.loads(open(actual).read())
+    im2 = json.loads(open(expected).read())
+    # TODO - real diffing
+    if not im1['data'] == im2['data']:
+        return 99999999
+    if not im1['keys'] == im2['keys']:
+        return 99999999
+    grid1 = im1['grid']
+    grid2 = im2['grid']
+    # dimensions must be exact
+    width1 = len(grid1[0])
+    width2 = len(grid2[0])
+    if not width1 == width2:
+        return 99999999
+    height1 = len(grid1)
+    height2 = len(grid2)
+    if not height1 == height2:
+        return 99999999
+    diff = 0;
+    for y in range(0,height1-1):
+        row1 = grid1[y]
+        row2 = grid2[y]
+        width = min(len(row1),len(row2))
+        for w in range(0,width):
+            if row1[w] != row2[w]:
+                diff += 1
+    return diff
+
+dirname = os.path.join(os.path.dirname(__file__),'data-visual')
+
+class Reporting:
+    DIFF = 1
+    NOT_FOUND = 2
+    OTHER = 3
+    REPLACE = 4
+    def __init__(self, quiet, overwrite_failures = False):
+        self.quiet = quiet
+        self.passed = 0
+        self.failed = 0
+        self.overwrite_failures = overwrite_failures
+        self.errors = [ #(type, actual, expected, diff, message)
+         ]
+
+    def result_fail(self, actual, expected, diff):
+        self.failed += 1
+        if self.quiet:
+            if platform.uname()[0] == 'Windows':
+                sys.stderr.write('.')
+            else:
+                sys.stderr.write('\x1b[31m.\x1b[0m')
+        else:
+            print '\x1b[31m✘\x1b[0m (\x1b[34m%u different pixels\x1b[0m)' % diff
+
+        if self.overwrite_failures:
+            self.errors.append((self.REPLACE, actual, expected, diff, None))
+            contents = open(actual, 'r').read()
+            open(expected, 'wb').write(contents)
+        else:
+            self.errors.append((self.DIFF, actual, expected, diff, None))
+
+    def result_pass(self, actual, expected, diff):
+        self.passed += 1
+        if self.quiet:
+            if platform.uname()[0] == 'Windows':
+                sys.stderr.write('.')
+            else:
+                sys.stderr.write('\x1b[32m.\x1b[0m')
+        else:
+            if platform.uname()[0] == 'Windows':
+                print '\x1b[32m✓\x1b[0m'
+            else:
+                print '✓'
+
+    def not_found(self, actual, expected):
+        self.failed += 1
+        self.errors.append((self.NOT_FOUND, actual, expected, 0, None))
+        if self.quiet:
+            sys.stderr.write('\x1b[33m.\x1b[0m')
+        else:
+            print '\x1b[33m?\x1b[0m (\x1b[34mReference file not found, creating\x1b[0m)'
+        contents = open(actual, 'r').read()
+        open(expected, 'wb').write(contents)
+
+    def other_error(self, expected, message):
+        self.failed += 1
+        self.errors.append((self.OTHER, None, expected, 0, message))
+        if self.quiet:
+            sys.stderr.write('\x1b[31m.\x1b[0m')
+        else:
+            print '\x1b[31m✘\x1b[0m (\x1b[34m%s\x1b[0m)' % message
+
+    def make_html_item(self,actual,expected,diff):
+        item = '''
+             <div class="expected">
+               <a href="%s">
+                 <img src="%s" width="100%s">
+               </a>
+             </div>
+              ''' % (expected,expected,'%')
+        item += '<div class="text">%s</div>' % (diff)
+        item += '''
+             <div class="actual">
+               <a href="%s">
+                 <img src="%s" width="100%s">
+               </a>
+             </div>
+              ''' % (actual,actual,'%')
+        return item
+
+    def summary(self):
+        if len(self.errors) == 0:
+            print '\nAll %s visual tests passed: \x1b[1;32m✓ \x1b[0m' % self.passed
+            return 0
+        sortable_errors = []
+        print "\nVisual rendering: %s failed / %s passed" % (len(self.errors), self.passed)
+        for idx, error in enumerate(self.errors):
+            if error[0] == self.OTHER:
+                print str(idx+1) + ") \x1b[31mfailure to run test:\x1b[0m %s (\x1b[34m%s\x1b[0m)" % (error[2],error[4])
+            elif error[0] == self.NOT_FOUND:
+                print str(idx+1) + ") Generating reference image: '%s'" % error[2]
+                continue
+            elif error[0] == self.DIFF:
+                print str(idx+1) + ") \x1b[34m%s different pixels\x1b[0m:\n\t%s (\x1b[31mactual\x1b[0m)\n\t%s (\x1b[32mexpected\x1b[0m)" % (error[3], error[1], error[2])
+                if '.png' in error[1]: # ignore grids
+                    sortable_errors.append((error[3],error))
+            elif error[0] == self.REPLACE:
+                print str(idx+1) + ") \x1b[31mreplaced reference with new version:\x1b[0m %s" % error[2]
+        if len(sortable_errors):
+            # drop failure results in folder
+            vdir = os.path.join(visual_output_dir,'visual-test-results')
+            if not os.path.exists(vdir):
+                os.makedirs(vdir)
+            html_template = open(os.path.join(dirname,'index.html'),'r').read()
+            name = 'index.html'
+            failures_realpath = os.path.join(vdir,name)
+            html_out = open(failures_realpath,'w+')
+            sortable_errors.sort(reverse=True)
+            html_body = ''
+            for item in sortable_errors:
+                # copy images into single directory
+                actual = item[1][1]
+                expected = item[1][2]
+                diff = item[0]
+                actual_new = os.path.join(vdir,os.path.basename(actual))
+                shutil.copy(actual,actual_new)
+                expected_new = os.path.join(vdir,os.path.basename(expected))
+                shutil.copy(expected,expected_new)
+                html_body += self.make_html_item(os.path.relpath(actual_new,vdir),os.path.relpath(expected_new,vdir),diff)
+            html_out.write(html_template.replace('{{RESULTS}}',html_body))
+            print 'View failures by opening %s' % failures_realpath
+        return 1
+
+def render(filename, config, scale_factor, reporting):
+    m = mapnik.Map(*config['sizes'][0])
+
+    try:
+        mapnik.load_map(m, os.path.join(dirname, "styles", filename), True)
+
+        if not (m.parameters['status'] if ('status' in m.parameters) else config['status']):
+            return
+    except Exception, e:
+        if 'Could not create datasource' in str(e) \
+           or 'Bad connection' in str(e):
+            return m
+        reporting.other_error(filename, repr(e))
+        return m
+
+    sizes = config['sizes'];
+    if 'sizes' in m.parameters:
+        sizes = [[int(i) for i in size.split(',')] for size in m.parameters['sizes'].split(';')]
+
+    for size in sizes:
+        m.width, m.height = size
+
+        if 'bbox' in m.parameters:
+            bbox = mapnik.Box2d.from_string(str(m.parameters['bbox']))
+            m.zoom_to_box(bbox)
+        else:
+            m.zoom_all()
+
+        name = filename[0:-4]
+        postfix = "%s-%d-%d-%s" % (name, m.width, m.height, scale_factor)
+        for renderer in renderers:
+            if config.get(renderer['name'], True):
+                expected = os.path.join(dirname, renderer['dir'], '%s-%s-reference.%s' %
+                    (postfix, renderer['name'], renderer['filetype']))
+                actual = os.path.join(visual_output_dir, '%s-%s.%s' %
+                    (postfix, renderer['name'], renderer['filetype']))
+                if not quiet:
+                    print "\"%s\" with %s..." % (postfix, renderer['name']),
+                try:
+                    renderer['render'](m, actual, scale_factor)
+                    if not os.path.exists(expected):
+                        reporting.not_found(actual, expected)
+                    else:
+                        diff = renderer['compare'](actual, expected)
+                        if diff > renderer['threshold']:
+                            reporting.result_fail(actual, expected, diff)
+                        else:
+                            reporting.result_pass(actual, expected, diff)
+                except Exception, e:
+                    reporting.other_error(expected, repr(e))
+    return m
+
+if __name__ == "__main__":
+    if '-q' in sys.argv:
+       quiet = True
+       sys.argv.remove('-q')
+    else:
+       quiet = False
+
+    if '--overwrite' in sys.argv:
+       overwrite_failures = True
+       sys.argv.remove('--overwrite')
+    else:
+       overwrite_failures = False
+
+    files = None
+    if len(sys.argv) > 1:
+        files = [name + ".xml" for name in sys.argv[1:]]
+    else:
+        files = [os.path.basename(file) for file in glob.glob(os.path.join(dirname, "styles/*.xml"))]
+
+    if not os.path.exists(visual_output_dir):
+        os.makedirs(visual_output_dir)
+
+    reporting = Reporting(quiet, overwrite_failures)
+    try:
+        for filename in files:
+            config = dict(defaults)
+            for scale_factor in config['scales']:
+                m = render(filename, config, scale_factor, reporting)
+    except KeyboardInterrupt:
+        pass
+    sys.exit(reporting.summary())

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



More information about the Pkg-grass-devel mailing list