[blockdiag] 16/29: Import Upstream version 1.4.3

Andreas Tille tille at debian.org
Tue Jan 10 21:35:58 UTC 2017


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

tille pushed a commit to branch master
in repository blockdiag.

commit b42615b7b7ef6c80f36acbb430c81e05ca2857aa
Author: Andreas Tille <tille at debian.org>
Date:   Tue Jan 10 11:08:04 2017 +0100

    Import Upstream version 1.4.3
---
 MANIFEST.in                                        |   5 +-
 PKG-INFO                                           |  98 +++-
 README.rst                                         |  94 +++-
 buildout.cfg                                       |  10 +-
 setup.cfg                                          |  10 +
 setup.py                                           |  16 +-
 src/blockdiag.egg-info/PKG-INFO                    |  98 +++-
 src/blockdiag.egg-info/SOURCES.txt                 |  12 +-
 src/blockdiag.egg-info/requires.txt                |  13 +-
 src/blockdiag/__init__.py                          |   2 +-
 src/blockdiag/builder.py                           |   2 +-
 src/blockdiag/drawer.py                            |   3 +-
 src/blockdiag/elements.py                          |  71 +--
 src/blockdiag/imagedraw/__init__.py                |  10 +-
 src/blockdiag/imagedraw/base.py                    |   2 -
 src/blockdiag/imagedraw/pdf.py                     |  38 +-
 src/blockdiag/imagedraw/png.py                     |  82 ++-
 src/blockdiag/imagedraw/simplesvg.py               |   5 +-
 src/blockdiag/imagedraw/svg.py                     |  31 +-
 src/blockdiag/imagedraw/utils/__init__.py          |  19 +-
 .../none.py => imagedraw/utils/pillow.py}          |  27 +-
 src/blockdiag/metrics.py                           |   9 +-
 src/blockdiag/noderenderer/__init__.py             | 120 +----
 src/blockdiag/noderenderer/actor.py                |  30 +-
 .../noderenderer/{__init__.py => base.py}          |  35 +-
 src/blockdiag/noderenderer/beginpoint.py           |   2 +-
 src/blockdiag/noderenderer/box.py                  |   2 +-
 src/blockdiag/noderenderer/circle.py               |  19 +-
 src/blockdiag/noderenderer/cloud.py                |   2 +-
 src/blockdiag/noderenderer/diamond.py              |   2 +-
 src/blockdiag/noderenderer/dots.py                 |   2 +-
 src/blockdiag/noderenderer/ellipse.py              |   2 +-
 src/blockdiag/noderenderer/endpoint.py             |   2 +-
 src/blockdiag/noderenderer/flowchart/database.py   |   2 +-
 src/blockdiag/noderenderer/flowchart/input.py      |   2 +-
 src/blockdiag/noderenderer/flowchart/loopin.py     |   2 +-
 src/blockdiag/noderenderer/flowchart/loopout.py    |   2 +-
 src/blockdiag/noderenderer/flowchart/terminator.py |   2 +-
 src/blockdiag/noderenderer/mail.py                 |   2 +-
 src/blockdiag/noderenderer/minidiamond.py          |   2 +-
 src/blockdiag/noderenderer/none.py                 |   2 +-
 src/blockdiag/noderenderer/note.py                 |   2 +-
 src/blockdiag/noderenderer/roundedbox.py           |   2 +-
 src/blockdiag/noderenderer/square.py               |  16 +-
 src/blockdiag/noderenderer/textbox.py              |   2 +-
 src/blockdiag/parser.py                            |  10 +-
 src/blockdiag/plugins/__init__.py                  |  29 +-
 src/blockdiag/plugins/autoclass.py                 |   4 +-
 src/blockdiag/tests/diagrams/README                |  12 +
 .../tests/diagrams/background_url_image.diag       |   4 +-
 .../diagrams/debian-logo-256color-palettealpha.png | Bin 0 -> 474 bytes
 .../tests/diagrams/diagram_attributes.diag         |   1 +
 .../diagrams/errors/unknown_diagram_type.diag      |   3 +
 src/blockdiag/tests/diagrams/node_attribute.diag   |   1 +
 src/blockdiag/tests/diagrams/node_icon.diag        |   2 +-
 src/blockdiag/tests/rst/__init__.py                |   0
 src/blockdiag/tests/rst/test_base_directives.py    | 129 +++++
 .../test_blockdiag_directives.py}                  | 550 +++++++++++++++------
 src/blockdiag/tests/test_boot_params.py            |   7 +-
 src/blockdiag/tests/test_builder.py                |   1 +
 src/blockdiag/tests/test_builder_errors.py         |   5 +
 src/blockdiag/tests/test_builder_node.py           |  10 +-
 src/blockdiag/tests/test_generate_diagram.py       |  17 +-
 src/blockdiag/tests/test_pep8.py                   |   2 +-
 src/blockdiag/tests/test_utils.py                  |  40 ++
 src/blockdiag/tests/test_utils_fontmap.py          |  18 +-
 src/blockdiag/tests/utils.py                       |   8 +-
 src/blockdiag/utils/__init__.py                    |  38 +-
 src/blockdiag/utils/bootstrap.py                   |  19 +-
 src/blockdiag/utils/compat.py                      |   2 +
 src/blockdiag/utils/fontmap.py                     |  11 +-
 src/blockdiag/utils/images.py                      | 131 +++--
 src/blockdiag/utils/jpeg.py                        |  92 ----
 src/blockdiag/utils/{rst/nodes.py => logging.py}   |  15 +-
 src/blockdiag/utils/rst/directives.py              | 203 +++++---
 src/blockdiag/utils/rst/nodes.py                   |  36 +-
 src/blockdiag/utils/urlutil.py                     |  13 +
 tox.ini                                            |  10 +-
 78 files changed, 1489 insertions(+), 847 deletions(-)

diff --git a/MANIFEST.in b/MANIFEST.in
index 02701f8..a08e362 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,4 +6,7 @@ include LICENSE
 include blockdiag.1
 include tox.ini
 recursive-include examples blockdiagrc *.diag *.png *.svg
-recursive-include src *.py *.diag *.gif
+recursive-include src README *.py *.diag *.gif *.png
+
+exclude .drone.io.sh
+exclude examples/update.sh
diff --git a/PKG-INFO b/PKG-INFO
index be630f3..7358389 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: blockdiag
-Version: 1.3.2
+Version: 1.4.3
 Summary: blockdiag generates block-diagram image from text
 Home-page: http://blockdiag.com/
 Author: Takeshi Komiya
@@ -9,35 +9,42 @@ License: Apache License 2.0
 Download-URL: http://pypi.python.org/pypi/blockdiag
 Description: `blockdiag` generate block-diagram image file from spec-text file.
         
+        .. image:: https://drone.io/bitbucket.org/blockdiag/blockdiag/status.png
+           :target: https://drone.io/bitbucket.org/blockdiag/blockdiag
+           :alt: drone.io CI build status
+        
+        .. image:: https://pypip.in/v/blockdiag/badge.png
+           :target: https://pypi.python.org/pypi/blockdiag/
+           :alt: Latest PyPI version
+        
+        .. image:: https://pypip.in/d/blockdiag/badge.png
+           :target: https://pypi.python.org/pypi/blockdiag/
+           :alt: Number of PyPI downloads
+        
+        
         Features
         ========
         * Generate block-diagram from dot like text (basic feature).
         * Multilingualization for node-label (utf-8 only).
         
-        You can get some examples and generated images on 
+        You can get some examples and generated images on
         `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
         
         Setup
         =====
         
-        by easy_install
-        ----------------
-        Make environment::
+        Use easy_install or pip::
         
-           $ easy_install blockdiag
+           $ sudo easy_install blockdiag
         
-        If you want to export as PDF format, give pdf arguments::
+           Or
         
-           $ easy_install "blockdiag[pdf]"
+           $ sudo pip install blockdiag
         
-        by buildout
-        ------------
-        Make environment::
+        If you want to export as PDF format, give pdf arguments::
+        
+           $ sudo easy_install "blockdiag[pdf]"
         
-           $ hg clone http://bitbucket.org/tk0miya/blockdiag
-           $ cd blockdiag
-           $ python bootstrap.py
-           $ bin/buildout
         
         Copy and modify ini file. example::
         
@@ -51,7 +58,7 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         ========================
         Few examples are available.
         You can get more examples at
-        `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+        `blockdiag.com`_ .
         
         simple.diag
         ------------
@@ -108,9 +115,11 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         
         Requirements
         ============
-        * Python 2.6, 2.7, 3.2, 3.3
-        * Pillow 2.2.1
-        * funcparserlib 0.3.6
+        * Python 2.6, 2.7, 3.2, 3.3, 3.4
+        * Pillow 2.2.1 or later
+        * funcparserlib 0.3.6 or later
+        * reportlab (optional)
+        * wand and imagemagick (optional)
         * setuptools
         
         
@@ -122,6 +131,53 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         History
         =======
         
+        1.4.3 (2014-07-30)
+        ------------------
+        * Show warnings on loading imagedrawers in debug mode
+        * ImageDraw#image() accepts Image objects
+        * Fix bugs
+        
+          - PNG: could not load png imagedrawer if could not access PIL.PILLOW_VERSION
+        
+        
+        1.4.2 (2014-07-12)
+        ------------------
+        * SVG: Adjust text alignment precisely
+        * Add plugin events: node.changing and cleanup
+        * ImageDraw#image() accepts image from IO objects
+        * Fix bugs
+        
+          - PDF: Fix failure text rotating
+          - PDF: Fix failure pasting PNG images (256 palette/transparency)
+          - PNG: Fix background of node was transparent on pasting transparent images
+        
+        1.4.1 (2014-07-02)
+        ------------------
+        * Change interface of docutils node (for sphinxcontrib module)
+        * Fix bugs
+        
+        1.4.0 (2014-06-23)
+        ------------------
+        * Support embedding SVG/EPS images as background
+        * Use wand to paste background images that is not supported by Pillow (if installed)
+        * Add options to blockdiag directive (docutils extension)
+        
+          - \:width:
+          - \:height:
+          - \:scale:
+          - \:align:
+          - \:name:
+          - \:class:
+          - \:figwidth:
+          - \:figclass:
+        
+        * actor shape supports label rendering
+        
+        1.3.3 (2014-04-26)
+        ------------------
+        * Add diagram attribute: default_node_style
+        * Fix bugs
+        
         1.3.2 (2013-11-19)
         ------------------
         * Fix bugs
@@ -283,7 +339,7 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         0.9.0 (2011-09-25)
         ------------------
         * Add icon attribute to node
-        * Make transparency to background of PNG images 
+        * Make transparency to background of PNG images
         * Fix bugs
         
         0.8.9 (2011-08-09)
diff --git a/README.rst b/README.rst
index fa950b3..c21bc23 100644
--- a/README.rst
+++ b/README.rst
@@ -1,34 +1,41 @@
 `blockdiag` generate block-diagram image file from spec-text file.
 
+.. image:: https://drone.io/bitbucket.org/blockdiag/blockdiag/status.png
+   :target: https://drone.io/bitbucket.org/blockdiag/blockdiag
+   :alt: drone.io CI build status
+
+.. image:: https://pypip.in/v/blockdiag/badge.png
+   :target: https://pypi.python.org/pypi/blockdiag/
+   :alt: Latest PyPI version
+
+.. image:: https://pypip.in/d/blockdiag/badge.png
+   :target: https://pypi.python.org/pypi/blockdiag/
+   :alt: Number of PyPI downloads
+
+
 Features
 ========
 * Generate block-diagram from dot like text (basic feature).
 * Multilingualization for node-label (utf-8 only).
 
-You can get some examples and generated images on 
+You can get some examples and generated images on
 `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
 
 Setup
 =====
 
-by easy_install
-----------------
-Make environment::
+Use easy_install or pip::
 
-   $ easy_install blockdiag
+   $ sudo easy_install blockdiag
 
-If you want to export as PDF format, give pdf arguments::
+   Or
 
-   $ easy_install "blockdiag[pdf]"
+   $ sudo pip install blockdiag
 
-by buildout
-------------
-Make environment::
+If you want to export as PDF format, give pdf arguments::
+
+   $ sudo easy_install "blockdiag[pdf]"
 
-   $ hg clone http://bitbucket.org/tk0miya/blockdiag
-   $ cd blockdiag
-   $ python bootstrap.py
-   $ bin/buildout
 
 Copy and modify ini file. example::
 
@@ -42,7 +49,7 @@ spec-text setting sample
 ========================
 Few examples are available.
 You can get more examples at
-`blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+`blockdiag.com`_ .
 
 simple.diag
 ------------
@@ -99,9 +106,11 @@ Execute blockdiag command::
 
 Requirements
 ============
-* Python 2.6, 2.7, 3.2, 3.3
-* Pillow 2.2.1
-* funcparserlib 0.3.6
+* Python 2.6, 2.7, 3.2, 3.3, 3.4
+* Pillow 2.2.1 or later
+* funcparserlib 0.3.6 or later
+* reportlab (optional)
+* wand and imagemagick (optional)
 * setuptools
 
 
@@ -113,6 +122,53 @@ Apache License 2.0
 History
 =======
 
+1.4.3 (2014-07-30)
+------------------
+* Show warnings on loading imagedrawers in debug mode
+* ImageDraw#image() accepts Image objects
+* Fix bugs
+
+  - PNG: could not load png imagedrawer if could not access PIL.PILLOW_VERSION
+
+
+1.4.2 (2014-07-12)
+------------------
+* SVG: Adjust text alignment precisely
+* Add plugin events: node.changing and cleanup
+* ImageDraw#image() accepts image from IO objects
+* Fix bugs
+
+  - PDF: Fix failure text rotating
+  - PDF: Fix failure pasting PNG images (256 palette/transparency)
+  - PNG: Fix background of node was transparent on pasting transparent images
+
+1.4.1 (2014-07-02)
+------------------
+* Change interface of docutils node (for sphinxcontrib module)
+* Fix bugs
+
+1.4.0 (2014-06-23)
+------------------
+* Support embedding SVG/EPS images as background
+* Use wand to paste background images that is not supported by Pillow (if installed)
+* Add options to blockdiag directive (docutils extension)
+
+  - \:width:
+  - \:height:
+  - \:scale:
+  - \:align:
+  - \:name:
+  - \:class:
+  - \:figwidth:
+  - \:figclass:
+
+* actor shape supports label rendering
+
+1.3.3 (2014-04-26)
+------------------
+* Add diagram attribute: default_node_style
+* Fix bugs
+
 1.3.2 (2013-11-19)
 ------------------
 * Fix bugs
@@ -274,7 +330,7 @@ History
 0.9.0 (2011-09-25)
 ------------------
 * Add icon attribute to node
-* Make transparency to background of PNG images 
+* Make transparency to background of PNG images
 * Fix bugs
 
 0.8.9 (2011-08-09)
diff --git a/buildout.cfg b/buildout.cfg
index 69df527..e9627ef 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -1,5 +1,5 @@
 [buildout]
-parts = blockdiag test tox static_analysis
+parts = blockdiag test static_analysis
 
 develop = .
 
@@ -12,16 +12,10 @@ interpreter = py
 recipe = pbp.recipe.noserunner
 eggs =
     blockdiag[rst]
-    blockdiag[test]
+    blockdiag[testing]
     coverage
     unittest-xml-reporting
 
-[tox]
-recipe = zc.recipe.egg
-eggs =
-    tox
-    detox
-
 [static_analysis]
 recipe = zc.recipe.egg
 eggs =
diff --git a/setup.cfg b/setup.cfg
index 18b023c..bf5ba50 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,6 +9,16 @@ build-base = _build
 [sdist]
 formats = gztar
 
+[wheel]
+universal = 1
+
+[aliases]
+release = check -r -s register sdist bdist_wheel upload
+
+[check]
+strict = 1
+restructuredtext = 1
+
 [flake8]
 ignore = _
 
diff --git a/setup.py b/setup.py
index 0a0f1a7..359b825 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-import os
 import sys
 from setuptools import setup, find_packages
 
@@ -24,13 +23,18 @@ requires = ['setuptools',
             'funcparserlib',
             'webcolors',
             'Pillow']
-test_requires = ['Nose',
-                 'pep8>=1.3']
+pdf_requires = ['reportlab']
+test_requires = ['nose',
+                 'mock',
+                 'pep8>=1.3',
+                 'reportlab',
+                 'docutils']
 
 
 # only for Python2.6
 if sys.version_info > (2, 6) and sys.version_info < (2, 7):
     requires.append('OrderedDict')
+    pdf_requires[0] = 'reportlab < 3.0'
     test_requires.append('unittest2')
 
 
@@ -53,10 +57,8 @@ setup(
     include_package_data=True,
     install_requires=requires,
     extras_require=dict(
-        test=test_requires,
-        pdf=[
-            'reportlab',
-        ],
+        testing=test_requires,
+        pdf=pdf_requires,
         rst=[
             'docutils',
         ],
diff --git a/src/blockdiag.egg-info/PKG-INFO b/src/blockdiag.egg-info/PKG-INFO
index be630f3..7358389 100644
--- a/src/blockdiag.egg-info/PKG-INFO
+++ b/src/blockdiag.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: blockdiag
-Version: 1.3.2
+Version: 1.4.3
 Summary: blockdiag generates block-diagram image from text
 Home-page: http://blockdiag.com/
 Author: Takeshi Komiya
@@ -9,35 +9,42 @@ License: Apache License 2.0
 Download-URL: http://pypi.python.org/pypi/blockdiag
 Description: `blockdiag` generate block-diagram image file from spec-text file.
         
+        .. image:: https://drone.io/bitbucket.org/blockdiag/blockdiag/status.png
+           :target: https://drone.io/bitbucket.org/blockdiag/blockdiag
+           :alt: drone.io CI build status
+        
+        .. image:: https://pypip.in/v/blockdiag/badge.png
+           :target: https://pypi.python.org/pypi/blockdiag/
+           :alt: Latest PyPI version
+        
+        .. image:: https://pypip.in/d/blockdiag/badge.png
+           :target: https://pypi.python.org/pypi/blockdiag/
+           :alt: Number of PyPI downloads
+        
+        
         Features
         ========
         * Generate block-diagram from dot like text (basic feature).
         * Multilingualization for node-label (utf-8 only).
         
-        You can get some examples and generated images on 
+        You can get some examples and generated images on
         `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
         
         Setup
         =====
         
-        by easy_install
-        ----------------
-        Make environment::
+        Use easy_install or pip::
         
-           $ easy_install blockdiag
+           $ sudo easy_install blockdiag
         
-        If you want to export as PDF format, give pdf arguments::
+           Or
         
-           $ easy_install "blockdiag[pdf]"
+           $ sudo pip install blockdiag
         
-        by buildout
-        ------------
-        Make environment::
+        If you want to export as PDF format, give pdf arguments::
+        
+           $ sudo easy_install "blockdiag[pdf]"
         
-           $ hg clone http://bitbucket.org/tk0miya/blockdiag
-           $ cd blockdiag
-           $ python bootstrap.py
-           $ bin/buildout
         
         Copy and modify ini file. example::
         
@@ -51,7 +58,7 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         ========================
         Few examples are available.
         You can get more examples at
-        `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+        `blockdiag.com`_ .
         
         simple.diag
         ------------
@@ -108,9 +115,11 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         
         Requirements
         ============
-        * Python 2.6, 2.7, 3.2, 3.3
-        * Pillow 2.2.1
-        * funcparserlib 0.3.6
+        * Python 2.6, 2.7, 3.2, 3.3, 3.4
+        * Pillow 2.2.1 or later
+        * funcparserlib 0.3.6 or later
+        * reportlab (optional)
+        * wand and imagemagick (optional)
         * setuptools
         
         
@@ -122,6 +131,53 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         History
         =======
         
+        1.4.3 (2014-07-30)
+        ------------------
+        * Show warnings on loading imagedrawers in debug mode
+        * ImageDraw#image() accepts Image objects
+        * Fix bugs
+        
+          - PNG: could not load png imagedrawer if could not access PIL.PILLOW_VERSION
+        
+        
+        1.4.2 (2014-07-12)
+        ------------------
+        * SVG: Adjust text alignment precisely
+        * Add plugin events: node.changing and cleanup
+        * ImageDraw#image() accepts image from IO objects
+        * Fix bugs
+        
+          - PDF: Fix failure text rotating
+          - PDF: Fix failure pasting PNG images (256 palette/transparency)
+          - PNG: Fix background of node was transparent on pasting transparent images
+        
+        1.4.1 (2014-07-02)
+        ------------------
+        * Change interface of docutils node (for sphinxcontrib module)
+        * Fix bugs
+        
+        1.4.0 (2014-06-23)
+        ------------------
+        * Support embedding SVG/EPS images as background
+        * Use wand to paste background images that is not supported by Pillow (if installed)
+        * Add options to blockdiag directive (docutils extension)
+        
+          - \:width:
+          - \:height:
+          - \:scale:
+          - \:align:
+          - \:name:
+          - \:class:
+          - \:figwidth:
+          - \:figclass:
+        
+        * actor shape supports label rendering
+        
+        1.3.3 (2014-04-26)
+        ------------------
+        * Add diagram attribute: default_node_style
+        * Fix bugs
+        
         1.3.2 (2013-11-19)
         ------------------
         * Fix bugs
@@ -283,7 +339,7 @@ Description: `blockdiag` generate block-diagram image file from spec-text file.
         0.9.0 (2011-09-25)
         ------------------
         * Add icon attribute to node
-        * Make transparency to background of PNG images 
+        * Make transparency to background of PNG images
         * Fix bugs
         
         0.8.9 (2011-08-09)
diff --git a/src/blockdiag.egg-info/SOURCES.txt b/src/blockdiag.egg-info/SOURCES.txt
index e78203b..ad4ca91 100644
--- a/src/blockdiag.egg-info/SOURCES.txt
+++ b/src/blockdiag.egg-info/SOURCES.txt
@@ -48,8 +48,10 @@ src/blockdiag/imagedraw/filters/__init__.py
 src/blockdiag/imagedraw/filters/linejump.py
 src/blockdiag/imagedraw/utils/__init__.py
 src/blockdiag/imagedraw/utils/ellipse.py
+src/blockdiag/imagedraw/utils/pillow.py
 src/blockdiag/noderenderer/__init__.py
 src/blockdiag/noderenderer/actor.py
+src/blockdiag/noderenderer/base.py
 src/blockdiag/noderenderer/beginpoint.py
 src/blockdiag/noderenderer/box.py
 src/blockdiag/noderenderer/circle.py
@@ -87,9 +89,10 @@ src/blockdiag/tests/test_imagedraw_textfolder.py
 src/blockdiag/tests/test_imagedraw_utils.py
 src/blockdiag/tests/test_parser.py
 src/blockdiag/tests/test_pep8.py
-src/blockdiag/tests/test_rst_directives.py
+src/blockdiag/tests/test_utils.py
 src/blockdiag/tests/test_utils_fontmap.py
 src/blockdiag/tests/utils.py
+src/blockdiag/tests/diagrams/README
 src/blockdiag/tests/diagrams/auto_jumping_edge.diag
 src/blockdiag/tests/diagrams/background_url_image.diag
 src/blockdiag/tests/diagrams/beginpoint_color.diag
@@ -99,6 +102,7 @@ src/blockdiag/tests/diagrams/circular_ref2.diag
 src/blockdiag/tests/diagrams/circular_ref_and_parent_node.diag
 src/blockdiag/tests/diagrams/circular_ref_to_root.diag
 src/blockdiag/tests/diagrams/circular_skipped_edge.diag
+src/blockdiag/tests/diagrams/debian-logo-256color-palettealpha.png
 src/blockdiag/tests/diagrams/define_class.diag
 src/blockdiag/tests/diagrams/diagram_attributes.diag
 src/blockdiag/tests/diagrams/diagram_attributes_order.diag
@@ -213,6 +217,7 @@ src/blockdiag/tests/diagrams/errors/node_follows_group.diag
 src/blockdiag/tests/diagrams/errors/unknown_diagram_default_shape.diag
 src/blockdiag/tests/diagrams/errors/unknown_diagram_edge_layout.diag
 src/blockdiag/tests/diagrams/errors/unknown_diagram_orientation.diag
+src/blockdiag/tests/diagrams/errors/unknown_diagram_type.diag
 src/blockdiag/tests/diagrams/errors/unknown_edge_class.diag
 src/blockdiag/tests/diagrams/errors/unknown_edge_dir.diag
 src/blockdiag/tests/diagrams/errors/unknown_edge_hstyle.diag
@@ -225,13 +230,16 @@ src/blockdiag/tests/diagrams/errors/unknown_node_class.diag
 src/blockdiag/tests/diagrams/errors/unknown_node_shape.diag
 src/blockdiag/tests/diagrams/errors/unknown_node_style.diag
 src/blockdiag/tests/diagrams/errors/unknown_plugin.diag
+src/blockdiag/tests/rst/__init__.py
+src/blockdiag/tests/rst/test_base_directives.py
+src/blockdiag/tests/rst/test_blockdiag_directives.py
 src/blockdiag/utils/__init__.py
 src/blockdiag/utils/bootstrap.py
 src/blockdiag/utils/compat.py
 src/blockdiag/utils/config.py
 src/blockdiag/utils/fontmap.py
 src/blockdiag/utils/images.py
-src/blockdiag/utils/jpeg.py
+src/blockdiag/utils/logging.py
 src/blockdiag/utils/myitertools.py
 src/blockdiag/utils/urlutil.py
 src/blockdiag/utils/uuid.py
diff --git a/src/blockdiag.egg-info/requires.txt b/src/blockdiag.egg-info/requires.txt
index b054c00..ff04388 100644
--- a/src/blockdiag.egg-info/requires.txt
+++ b/src/blockdiag.egg-info/requires.txt
@@ -3,12 +3,15 @@ funcparserlib
 webcolors
 Pillow
 
+[pdf]
+reportlab
+
 [rst]
 docutils
 
-[pdf]
+[testing]
+nose
+mock
+pep8>=1.3
 reportlab
-
-[test]
-Nose
-pep8>=1.3
\ No newline at end of file
+docutils
diff --git a/src/blockdiag/__init__.py b/src/blockdiag/__init__.py
index 09ae5dc..2296c6d 100644
--- a/src/blockdiag/__init__.py
+++ b/src/blockdiag/__init__.py
@@ -13,4 +13,4 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-__version__ = '1.3.2'
+__version__ = '1.4.3'
diff --git a/src/blockdiag/builder.py b/src/blockdiag/builder.py
index 7ab63cf..e0f9527 100644
--- a/src/blockdiag/builder.py
+++ b/src/blockdiag/builder.py
@@ -239,7 +239,7 @@ class DiagramLayoutManager:
                 parents = []
                 for node in circular:
                     for parent in self.get_parent_nodes(node):
-                        if not parent in circular:
+                        if parent not in circular:
                             parents.append(parent)
 
                 for parent in sorted(parents, key=lambda x: x.order):
diff --git a/src/blockdiag/drawer.py b/src/blockdiag/drawer.py
index cae5d15..2d708ed 100644
--- a/src/blockdiag/drawer.py
+++ b/src/blockdiag/drawer.py
@@ -42,7 +42,8 @@ class DiagramDraw(object):
                                        **kwargs)
 
         self.metrics = self.create_metrics(kwargs.get('basediagram', diagram),
-                                           drawer=self.drawer, **kwargs)
+                                           drawer=self.drawer,
+                                           fontmap=kwargs.get('fontmap'))
         if self.scale_ratio == 2:
             self.metrics = AutoScaler(self.metrics,
                                       scale_ratio=self.scale_ratio)
diff --git a/src/blockdiag/elements.py b/src/blockdiag/elements.py
index bc67124..ff1d8c7 100644
--- a/src/blockdiag/elements.py
+++ b/src/blockdiag/elements.py
@@ -15,10 +15,10 @@
 
 import os
 import re
-import sys
 import copy
 from blockdiag.utils import images, unquote, urlutil, uuid, XY
 from blockdiag.utils.compat import u
+from blockdiag.utils.logging import warning
 from blockdiag import noderenderer, plugins
 
 
@@ -35,6 +35,10 @@ class Base(object):
         cls.basecolor = images.color_to_rgb(color)
 
     @classmethod
+    def set_default_style(cls, style):
+        cls.style = style
+
+    @classmethod
     def set_default_text_color(cls, color):
         cls.textcolor = images.color_to_rgb(color)
 
@@ -52,6 +56,7 @@ class Base(object):
         cls.textcolor = (0, 0, 0)
         cls.fontfamily = None
         cls.fontsize = None
+        cls.style = None
 
     def duplicate(self):
         return copy.copy(self)
@@ -87,7 +92,7 @@ class Base(object):
             self.style = value.lower()
         else:
             class_name = self.__class__.__name__
-            msg = "WARNING: unknown %s style: %s\n" % (class_name, value)
+            msg = "unknown %s style: %s" % (class_name, value)
             raise AttributeError(msg)
 
 
@@ -182,8 +187,9 @@ class DiagramNode(Element):
         plugins.fire_node_event(self, 'created')
 
     def set_attribute(self, attr):
-        super(DiagramNode, self).set_attribute(attr)
-        plugins.fire_node_event(self, 'attr_changed', attr)
+        if plugins.fire_node_event(self, 'attr_changing', attr):
+            super(DiagramNode, self).set_attribute(attr)
+            plugins.fire_node_event(self, 'attr_changed', attr)
 
     def set_linecolor(self, color):
         self.linecolor = images.color_to_rgb(color)
@@ -192,22 +198,20 @@ class DiagramNode(Element):
         if noderenderer.get(value):
             self.shape = value
         else:
-            msg = "WARNING: unknown node shape: %s\n" % value
+            msg = "unknown node shape: %s" % value
             raise AttributeError(msg)
 
     def set_icon(self, value):
         if urlutil.isurl(value) or os.path.isfile(value):
             self.icon = value
         else:
-            msg = "WARNING: icon image not found: %s\n" % value
-            sys.stderr.write(msg)
+            warning("icon image not found: %s", value)
 
     def set_background(self, value):
         if urlutil.isurl(value) or os.path.isfile(value):
             self.background = value
         else:
-            msg = "WARNING: background image not found: %s\n" % value
-            sys.stderr.write(msg)
+            warning("background image not found: %s", value)
 
     def set_stacked(self, _):
         self.stacked = True
@@ -217,7 +221,7 @@ class DiagramNode(Element):
         if value in ('horizontal', 'vertical'):
             self.label_orientation = value
         else:
-            msg = "WARNING: unknown label orientation: %s\n" % value
+            msg = "unknown label orientation: %s" % value
             raise AttributeError(msg)
 
     def to_desctable(self):
@@ -333,7 +337,7 @@ class NodeGroup(Element):
         if value in ('landscape', 'portrait'):
             self.orientation = value
         else:
-            msg = "WARNING: unknown diagram orientation: %s\n" % value
+            msg = "unknown diagram orientation: %s" % value
             raise AttributeError(msg)
 
     def set_shape(self, value):
@@ -341,7 +345,7 @@ class NodeGroup(Element):
         if value in ('box', 'line'):
             self.shape = value
         else:
-            msg = "WARNING: unknown group shape: %s\n" % value
+            msg = "unknown group shape: %s" % value
             raise AttributeError(msg)
 
 
@@ -466,7 +470,7 @@ class DiagramEdge(Base):
         elif value == '--':
             self.dir = 'none'
         else:
-            msg = "WARNING: unknown edge dir: %s\n" % value
+            msg = "unknown edge dir: %s" % value
             raise AttributeError(msg)
 
     def set_color(self, color):
@@ -489,7 +493,7 @@ class DiagramEdge(Base):
             self.dir = 'both'
             self.hstyle = value
         else:
-            msg = "WARNING: unknown edge hstyle: %s\n" % value
+            msg = "unknown edge hstyle: %s" % value
             raise AttributeError(msg)
 
     def set_folded(self, _):
@@ -564,13 +568,9 @@ class Diagram(NodeGroup):
         self.edge_layout = None
 
     def set_plugin(self, name, attrs):
-        try:
-            kwargs = dict([str(unquote(attr.name)), unquote(attr.value)]
-                          for attr in attrs)
-            plugins.load([name], diagram=self, **kwargs)
-        except:
-            msg = "WARNING: fail to load plugin: %s\n" % name
-            raise AttributeError(msg)
+        kwargs = dict([str(unquote(attr.name)), unquote(attr.value)]
+                      for attr in attrs)
+        plugins.load([name], diagram=self, **kwargs)
 
     def set_plugins(self, value):
         modules = [name.strip() for name in value.split(',')]
@@ -580,7 +580,7 @@ class Diagram(NodeGroup):
         if noderenderer.get(value):
             DiagramNode.set_default_shape(value)
         else:
-            msg = "WARNING: unknown node shape: %s\n" % value
+            msg = "unknown node shape: %s" % value
             raise AttributeError(msg)
 
     def set_default_label_orientation(self, value):
@@ -588,13 +588,11 @@ class Diagram(NodeGroup):
         if value in ('horizontal', 'vertical'):
             DiagramNode.label_orientation = value
         else:
-            msg = "WARNING: unknown label orientation: %s\n" % value
+            msg = "unknown label orientation: %s" % value
             raise AttributeError(msg)
 
     def set_default_text_color(self, color):
-        msg = "WARNING: default_text_color is obsoleted; " + \
-              "use default_textcolor\n"
-        sys.stderr.write(msg)
+        warning("default_text_color is obsoleted; use default_textcolor")
         self.set_default_textcolor(color)
 
     def set_default_textcolor(self, color):
@@ -607,10 +605,15 @@ class Diagram(NodeGroup):
         color = images.color_to_rgb(color)
         self._DiagramNode.set_default_color(color)
 
+    def set_default_node_style(self, value):
+        if re.search('^(?:none|solid|dotted|dashed|\d+(,\d+)*)$', value, re.I):
+            self._DiagramNode.set_default_style(value)
+        else:
+            msg = "unknown node style: %s" % value
+            raise AttributeError(msg)
+
     def set_default_line_color(self, color):
-        msg = "WARNING: default_line_color is obsoleted; " + \
-              "use default_linecolor\n"
-        sys.stderr.write(msg)
+        warning("default_line_color is obsoleted; use default_linecolor")
         self.set_default_linecolor(color)
 
     def set_default_linecolor(self, color):
@@ -640,21 +643,19 @@ class Diagram(NodeGroup):
         if value in ('solid', 'blur', 'none'):
             self.shadow_style = value
         else:
-            msg = "WARNING: unknown shadow style: %s\n" % value
+            msg = "unknown shadow style: %s" % value
             raise AttributeError(msg)
 
     def set_edge_layout(self, value):
         value = value.lower()
         if value in ('normal', 'flowchart'):
-            msg = u("WARNING: edge_layout is very experimental feature!\n")
-            sys.stderr.write(msg)
+            warning("edge_layout is very experimental feature!")
 
             self.edge_layout = value
         else:
-            msg = "WARNING: unknown edge layout: %s\n" % value
+            msg = "unknown edge layout: %s" % value
             raise AttributeError(msg)
 
     def set_fontsize(self, value):
-        msg = "WARNING: fontsize is obsoleted; use default_fontsize\n"
-        sys.stderr.write(msg)
+        warning("fontsize is obsoleted; use default_fontsize")
         self.set_default_fontsize(int(value))
diff --git a/src/blockdiag/imagedraw/__init__.py b/src/blockdiag/imagedraw/__init__.py
index 7534957..f91d459 100644
--- a/src/blockdiag/imagedraw/__init__.py
+++ b/src/blockdiag/imagedraw/__init__.py
@@ -14,18 +14,20 @@
 #  limitations under the License.
 
 import pkg_resources
+from blockdiag.utils.logging import warning
 
 drawers = {}
 
 
-def init_imagedrawers():
+def init_imagedrawers(debug=False):
     for drawer in pkg_resources.iter_entry_points('blockdiag_imagedrawers'):
         try:
             module = drawer.load()
             if hasattr(module, 'setup'):
                 module.setup(module)
-        except:
-            pass
+        except Exception as exc:
+            if debug:
+                warning('Failed to load %s: %r' % (drawer.module_name, exc))
 
 
 def install_imagedrawer(ext, drawer):
@@ -34,7 +36,7 @@ def install_imagedrawer(ext, drawer):
 
 def create(_format, filename, **kwargs):
     if len(drawers) == 0:
-        init_imagedrawers()
+        init_imagedrawers(debug=kwargs.get('debug'))
 
     _format = _format.lower()
     if _format in drawers:
diff --git a/src/blockdiag/imagedraw/base.py b/src/blockdiag/imagedraw/base.py
index f53b940..9dcec92 100644
--- a/src/blockdiag/imagedraw/base.py
+++ b/src/blockdiag/imagedraw/base.py
@@ -24,8 +24,6 @@ class ImageDraw(object):
     supported_path = False
     baseline_text_rendering = False
 
-    _method_cache = {}
-
     def set_canvas_size(self, size):
         pass
 
diff --git a/src/blockdiag/imagedraw/pdf.py b/src/blockdiag/imagedraw/pdf.py
index a474072..6fe3637 100644
--- a/src/blockdiag/imagedraw/pdf.py
+++ b/src/blockdiag/imagedraw/pdf.py
@@ -14,14 +14,14 @@
 #  limitations under the License.
 
 import re
-import sys
 import math
 from reportlab.pdfgen import canvas
 from reportlab.pdfbase import pdfmetrics
 from reportlab.pdfbase.ttfonts import TTFont
+from reportlab.lib.utils import ImageReader
 from blockdiag.imagedraw import base
-from blockdiag.imagedraw.utils import cached
-from blockdiag.utils import urlutil, Box, Size
+from blockdiag.imagedraw.utils import memoize
+from blockdiag.utils import images, Box, Size
 from blockdiag.utils.fontmap import parse_fontpath
 from blockdiag.utils.compat import string_types
 
@@ -128,7 +128,7 @@ class PDFImageDraw(base.ImageDraw):
         if 'thick' in kwargs:
             self.canvas.setLineWidth(1)
 
-    @cached
+    @memoize
     def textlinesize(self, string, font):
         self.set_font(font)
         width = self.canvas.stringWidth(string, font.path, font.size)
@@ -148,13 +148,13 @@ class PDFImageDraw(base.ImageDraw):
 
             if angle == 90:
                 box = Box(-box.y2, box.x1, -box.y1, box.x1 + box.width)
-                box = box.shift(x=self.size.y, y=self.size.y)
+                box = box.shift(x=self.size.height, y=self.size.height)
             elif angle == 180:
                 box = Box(-box.x2, -box.y2, -box.x1, -box.y2 + box.height)
-                box = box.shift(y=self.size.y * 2)
+                box = box.shift(y=self.size.height * 2)
             elif angle == 270:
                 box = Box(box.y1, -box.x2, box.y2, -box.x1)
-                box = box.shift(x=-self.size.y, y=self.size.y)
+                box = box.shift(x=-self.size.height, y=self.size.height)
 
         self.set_font(font)
         lines = self.textfolder(box, string, font, **kwargs)
@@ -213,18 +213,18 @@ class PDFImageDraw(base.ImageDraw):
         self.canvas.drawPath(pd, **params)
 
     def image(self, box, url):
-        if urlutil.isurl(url):
-            from reportlab.lib.utils import ImageReader
-            try:
-                url = ImageReader(url)
-            except:
-                msg = "WARNING: Could not retrieve: %s\n" % url
-                sys.stderr.write(msg)
-                return
-
-        y = self.size[1] - box[3]
-        self.canvas.drawImage(url, box.x1, y, box.width, box.height,
-                              mask='auto', preserveAspectRatio=True)
+        try:
+            image = images.open(url, mode='pillow')
+            if image.mode not in ('RGBA', 'L', 'RGB', 'CYMYK'):
+                # convert to format that reportlab can recognize
+                image = image.convert('RGBA')
+
+            y = self.size[1] - box[3]
+            data = ImageReader(image)
+            self.canvas.drawImage(data, box.x1, y, box.width, box.height,
+                                  mask='auto', preserveAspectRatio=True)
+        except IOError:
+            pass
 
     def save(self, filename, size, _format):
         # Ignore size and format parameter; compatibility for ImageDrawEx.
diff --git a/src/blockdiag/imagedraw/png.py b/src/blockdiag/imagedraw/png.py
index e7529a6..1026919 100644
--- a/src/blockdiag/imagedraw/png.py
+++ b/src/blockdiag/imagedraw/png.py
@@ -15,7 +15,6 @@
 
 from __future__ import division
 import re
-import sys
 import math
 from itertools import tee
 try:
@@ -25,13 +24,18 @@ except ImportError:
 from functools import partial, wraps
 from PIL import Image, ImageDraw, ImageFont, ImageFilter
 from blockdiag.imagedraw import base
-from blockdiag.imagedraw.utils import cached
+from blockdiag.imagedraw.utils import memoize
 from blockdiag.imagedraw.utils.ellipse import dots as ellipse_dots
-from blockdiag.utils import urlutil, Box, Size, XY
+from blockdiag.utils import images, Box, Size, XY
 from blockdiag.utils.fontmap import parse_fontpath, FontMap
 from blockdiag.utils.myitertools import istep, stepslice
 
 
+# apply monkey patch to pillow
+from blockdiag.imagedraw.utils import pillow
+pillow.apply_patch()
+
+
 def point_pairs(xylist):
     iterable = iter(xylist)
     for pt in iterable:
@@ -271,7 +275,7 @@ class ImageDrawExBase(base.ImageDraw):
         textfolder = super(ImageDrawExBase, self).textfolder
         return partial(textfolder, scale=self.scale_ratio)
 
-    @cached
+    @memoize
     def textlinesize(self, string, font):
         ttfont = ttfont_for(font)
         if ttfont is None:
@@ -281,8 +285,7 @@ class ImageDrawExBase(base.ImageDraw):
             size = Size(int(size[0] * font_ratio),
                         int(size[1] * font_ratio))
         else:
-            size = self.draw.textsize(string, font=ttfont)
-            size = Size(*size)
+            size = Size(*ttfont.getsize(string))
 
         return size
 
@@ -305,7 +308,7 @@ class ImageDrawExBase(base.ImageDraw):
                 text_image = image.resize(basesize, Image.ANTIALIAS)
                 self.paste(text_image, xy, text_image)
         else:
-            size = self.draw.textsize(string, font=ttfont)
+            size = ttfont.getsize(string)
 
             # Generate mask to support BDF(bitmap font)
             mask = Image.new('1', size)
@@ -352,38 +355,36 @@ class ImageDrawExBase(base.ImageDraw):
             self.textarea(box, string, _font, **kwargs)
 
     def image(self, box, url):
-        if urlutil.isurl(url):
-            try:
-                from io import StringIO
-            except ImportError:
-                from cStringIO import StringIO
-            import urllib
-            try:
-                url = StringIO(urllib.urlopen(url).read())
-            except:
-                msg = "WARNING: Could not retrieve: %s\n" % url
-                sys.stderr.write(msg)
-                return
-        image = Image.open(url)
-
-        # resize image.
-        w = min([box.width, image.size[0] * self.scale_ratio])
-        h = min([box.height, image.size[1] * self.scale_ratio])
-        image.thumbnail((w, h), Image.ANTIALIAS)
-
-        # centering image.
-        w, h = image.size
-        if box.width > w:
-            x = box[0] + (box.width - w) // 2
-        else:
-            x = box[0]
+        try:
+            image = images.open(url, mode='pillow')
+
+            # resize image.
+            w = min([box.width, image.size[0] * self.scale_ratio])
+            h = min([box.height, image.size[1] * self.scale_ratio])
+            image.thumbnail((w, h), Image.ANTIALIAS)
+
+            # centering image.
+            w, h = image.size
+            if box.width > w:
+                x = box[0] + (box.width - w) // 2
+            else:
+                x = box[0]
 
-        if box.height > h:
-            y = box[1] + (box.height - h) // 2
-        else:
-            y = box[1]
+            if box.height > h:
+                y = box[1] + (box.height - h) // 2
+            else:
+                y = box[1]
+
+            if image.mode == 'P':
+                # convert P to RGBA to masking transparent pixels
+                image = image.convert('RGBA')
 
-        self.paste(image, (x, y))
+            if image.mode == 'RGBA':
+                self.paste(image, (x, y), mask=image)
+            else:
+                self.paste(image, (x, y))
+        except IOError:
+            pass
 
     def save(self, filename, size, _format):
         if filename:
@@ -400,11 +401,8 @@ class ImageDrawExBase(base.ImageDraw):
             self._image.save(self.filename, _format)
             image = None
         else:
-            try:
-                from io import StringIO
-            except ImportError:
-                from cStringIO import StringIO
-            tmp = StringIO()
+            from io import BytesIO
+            tmp = BytesIO()
             self._image.save(tmp, _format)
             image = tmp.getvalue()
 
diff --git a/src/blockdiag/imagedraw/simplesvg.py b/src/blockdiag/imagedraw/simplesvg.py
index fd5fe0b..61c1268 100644
--- a/src/blockdiag/imagedraw/simplesvg.py
+++ b/src/blockdiag/imagedraw/simplesvg.py
@@ -15,10 +15,7 @@
 
 import re
 from blockdiag.utils.compat import u, string_types
-try:
-    from io import StringIO
-except ImportError:
-    from cStringIO import StringIO
+from io import StringIO
 
 
 def _escape(s):
diff --git a/src/blockdiag/imagedraw/svg.py b/src/blockdiag/imagedraw/svg.py
index c2a159f..5d8af22 100644
--- a/src/blockdiag/imagedraw/svg.py
+++ b/src/blockdiag/imagedraw/svg.py
@@ -13,16 +13,17 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+import os
 import re
-import base64
+from base64 import b64encode
 from blockdiag.imagedraw import base as _base
 from blockdiag.imagedraw.simplesvg import (
     svg, svgclass, filter, title, desc, defs, g, a, text,
     rect, polygon, ellipse, path, pathdata, image
 )
-from blockdiag.imagedraw.utils import cached
+from blockdiag.imagedraw.utils import memoize
 from blockdiag.imagedraw.utils.ellipse import endpoints as ellipse_endpoints
-from blockdiag.utils import urlutil, Box, XY, is_Pillow_available
+from blockdiag.utils import images, Box, XY, is_Pillow_available
 
 feGaussianBlur = svgclass('feGaussianBlur')
 
@@ -103,7 +104,7 @@ class SVGImageDrawElement(_base.ImageDraw):
                  stroke_width=thick, **drawing_params(kwargs))
         self.svg.addElement(r)
 
-    @cached
+    @memoize
     def textlinesize(self, string, font, **kwargs):
         if is_Pillow_available():
             if not hasattr(self, '_pil_drawer'):
@@ -118,9 +119,12 @@ class SVGImageDrawElement(_base.ImageDraw):
     def text(self, point, string, font, **kwargs):
         fill = kwargs.get('fill')
 
+        size = self.textlinesize(string, font)
+        point = point.shift(size.width / 2)
         t = text(point.x, point.y, string, fill=rgb(fill),
                  font_family=font.generic_family, font_size=font.size,
-                 font_weight=font.weight, font_style=font.style)
+                 font_weight=font.weight, font_style=font.style,
+                 text_anchor='middle', textLength=size.width)
         self.svg.addElement(t)
 
     def textarea(self, box, string, font, **kwargs):
@@ -227,9 +231,20 @@ class SVGImageDrawElement(_base.ImageDraw):
         self.svg.addElement(pg)
 
     def image(self, box, url):
-        if not urlutil.isurl(url):
-            string = open(url, 'rb').read()
-            url = "data:;base64," + str(base64.b64encode(string))
+        if hasattr(url, 'read'):
+            url = "data:;base64," + str(b64encode(url.read()))
+        else:
+            ext = os.path.splitext(url)[1].lower()
+            if ext not in ('.jpg', '.png', '.gif'):
+                stream = None
+                try:
+                    stream = images.open(url, mode='png')
+                    url = "data:;base64," + str(b64encode(stream.read()))
+                except IOError:
+                    pass
+                finally:
+                    if stream:
+                        stream.close()
 
         im = image(url, box.x1, box.y1, box.width, box.height)
         self.svg.addElement(im)
diff --git a/src/blockdiag/imagedraw/utils/__init__.py b/src/blockdiag/imagedraw/utils/__init__.py
index 364c6d3..a8f717c 100644
--- a/src/blockdiag/imagedraw/utils/__init__.py
+++ b/src/blockdiag/imagedraw/utils/__init__.py
@@ -15,6 +15,7 @@
 
 import math
 import unicodedata
+from functools import wraps
 from blockdiag.utils import Size
 from blockdiag.utils.compat import u
 
@@ -54,17 +55,15 @@ def textsize(string, font):
     return Size(int(math.ceil(width)), font.size)
 
 
-def cached(fn):
-    def func(self, *args, **kwargs):
-        name = fn.__name__
-        key = args + tuple(kwargs.values())
+def memoize(fn):
+    fn.cache = {}
 
-        if name not in self._method_cache:
-            self._method_cache[name] = {}
+    @wraps(fn)
+    def func(*args, **kwargs):
+        key = str(args) + str(kwargs)
+        if key not in fn.cache:
+            fn.cache[key] = fn(*args, **kwargs)
 
-        if key not in self._method_cache[name]:
-            self._method_cache[name][key] = fn(self, *args, **kwargs)
-
-        return self._method_cache[name][key]
+        return fn.cache[key]
 
     return func
diff --git a/src/blockdiag/noderenderer/none.py b/src/blockdiag/imagedraw/utils/pillow.py
similarity index 50%
copy from src/blockdiag/noderenderer/none.py
copy to src/blockdiag/imagedraw/utils/pillow.py
index 81ef91d..6e6f839 100644
--- a/src/blockdiag/noderenderer/none.py
+++ b/src/blockdiag/imagedraw/utils/pillow.py
@@ -13,23 +13,26 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
-from blockdiag.noderenderer import install_renderer
 
+def patch_FreeTypeFont_getsize():
+    try:
+        from PIL import PILLOW_VERSION
+        from PIL.ImageFont import FreeTypeFont
 
-class NoneShape(NodeShape):
-    def __init__(self, node, metrics=None):
-        super(NoneShape, self).__init__(node, metrics)
+        # Avoid offset problem in Pillow (>= 2.2.0, < 2.6.0)
+        if "2.2.0" <= PILLOW_VERSION < "2.6.0":
+            original_getsize = FreeTypeFont.getsize
 
-        p = metrics.cell(node).center
-        self.connectors = [p, p, p, p]
+            def getsize(self, string):
+                size = original_getsize(self, string)
+                offset = self.getoffset(string)
 
-    def render_label(self, drawer, **kwargs):
-        pass
+                return (size[0] + offset[0], size[1] + offset[1])
 
-    def render_shape(self, drawer, _, **kwargs):
+            FreeTypeFont.getsize = getsize
+    except ImportError:
         pass
 
 
-def setup(self):
-    install_renderer('none', NoneShape)
+def apply_patch():
+    patch_FreeTypeFont_getsize()
diff --git a/src/blockdiag/metrics.py b/src/blockdiag/metrics.py
index dcb671d..c7da902 100644
--- a/src/blockdiag/metrics.py
+++ b/src/blockdiag/metrics.py
@@ -140,8 +140,8 @@ class DiagramMetrics(object):
     span_width = cellsize * 8
     span_height = cellsize * 5
 
-    def __init__(self, diagram, **kwargs):
-        self.drawer = kwargs.get('drawer')
+    def __init__(self, diagram, drawer=None, fontmap=None):
+        self.drawer = drawer
 
         if diagram.node_width is not None:
             self.node_width = diagram.node_width
@@ -155,7 +155,6 @@ class DiagramMetrics(object):
         if diagram.span_height is not None:
             self.span_height = diagram.span_height
 
-        fontmap = kwargs.get('fontmap')
         if fontmap is not None:
             self.fontmap = fontmap
         else:
@@ -350,8 +349,8 @@ class SpreadSheetMetrics(SubMetrics):
         x, y = self._node_bottomright(dummy, use_padding=False)
         x_span = self.span_width[width]
         y_span = self.span_height[height]
-        return XY(x + margin.x + padding[1] + x_span,
-                  y + margin.y + padding[2] + y_span)
+        return Size(x + margin.x + padding[1] + x_span,
+                    y + margin.y + padding[2] + y_span)
 
 
 class NodeMetrics(SubMetrics):
diff --git a/src/blockdiag/noderenderer/__init__.py b/src/blockdiag/noderenderer/__init__.py
index 9bf590f..7495ec4 100644
--- a/src/blockdiag/noderenderer/__init__.py
+++ b/src/blockdiag/noderenderer/__init__.py
@@ -15,7 +15,7 @@
 
 from __future__ import division
 import pkg_resources
-from blockdiag.utils import images, Box, XY
+from blockdiag.noderenderer.base import NodeShape  # NOQA: backward compatibility
 
 renderers = {}
 searchpath = []
@@ -48,121 +48,3 @@ def get(shape):
             return renderers[name]
 
     return renderers.get(shape)
-
-
-class NodeShape(object):
-    def __init__(self, node, metrics=None):
-        self.node = node
-        self.metrics = metrics
-
-        m = self.metrics.cell(self.node)
-        self.textalign = 'center'
-        self.connectors = [m.top, m.right, m.bottom, m.left]
-
-        if node.icon is None:
-            self.iconbox = None
-            self.textbox = m.box
-        else:
-            image_size = images.get_image_size(node.icon)
-            if image_size is None:
-                iconsize = (0, 0)
-            else:
-                boundedbox = [metrics.node_width // 2, metrics.node_height]
-                iconsize = images.calc_image_size(image_size, boundedbox)
-
-            vmargin = (metrics.node_height - iconsize[1]) // 2
-            self.iconbox = Box(m.topleft.x,
-                               m.topleft.y + vmargin,
-                               m.topleft.x + iconsize[0],
-                               m.topleft.y + vmargin + iconsize[1])
-
-            self.textbox = Box(self.iconbox[2], m.top.y,
-                               m.bottomright.x, m.bottomright.y)
-
-    def render(self, drawer, _format, **kwargs):
-        if self.node.stacked and not kwargs.get('stacked'):
-            node = self.node.duplicate()
-            node.label = ""
-            node.background = ""
-            for i in range(2, 0, -1):
-                # use original_metrics FORCE
-                r = self.metrics.original_metrics.cellsize // 2 * i
-                metrics = self.metrics.shift(r, r)
-
-                self.__class__(node, metrics).render(drawer, _format,
-                                                     stacked=True, **kwargs)
-
-        if hasattr(self, 'render_vector_shape') and _format == 'SVG':
-            self.render_vector_shape(drawer, _format, **kwargs)
-        else:
-            self.render_shape(drawer, _format, **kwargs)
-
-        self.render_icon(drawer, **kwargs)
-        self.render_label(drawer, **kwargs)
-        self.render_number_badge(drawer, **kwargs)
-
-    def render_icon(self, drawer, **kwargs):
-        if self.node.icon is not None and kwargs.get('shadow') is not True:
-            drawer.image(self.iconbox, self.node.icon)
-
-    def render_shape(self, drawer, _, **kwargs):
-        pass
-
-    def render_label(self, drawer, **kwargs):
-        if not kwargs.get('shadow'):
-            font = self.metrics.font_for(self.node)
-            drawer.textarea(self.textbox, self.node.label, font,
-                            rotate=self.node.rotate,
-                            fill=self.node.textcolor, halign=self.textalign,
-                            line_spacing=self.metrics.line_spacing,
-                            orientation=self.node.label_orientation)
-
-    def render_number_badge(self, drawer, **kwargs):
-        if self.node.numbered is not None and kwargs.get('shadow') is None:
-            badgeFill = kwargs.get('badgeFill')
-
-            xy = self.metrics.cell(self.node).topleft
-            r = self.metrics.cellsize * 3 // 2
-
-            box = Box(xy.x - r, xy.y - r, xy.x + r, xy.y + r)
-            font = self.metrics.font_for(self.node)
-            drawer.ellipse(box, outline=self.node.linecolor, fill=badgeFill)
-            drawer.textarea(box, self.node.numbered, font,
-                            rotate=self.node.rotate,
-                            fill=self.node.textcolor)
-
-    @property
-    def top(self):
-        return self.connectors[0]
-
-    @property
-    def left(self):
-        return self.connectors[3]
-
-    @property
-    def right(self):
-        point = self.connectors[1]
-        if self.node.stacked:
-            point = XY(point.x + self.metrics.cellsize, point.y)
-        return point
-
-    @property
-    def bottom(self):
-        point = self.connectors[2]
-        if self.node.stacked:
-            point = XY(point.x, point.y + self.metrics.cellsize)
-        return point
-
-    def shift_shadow(self, value):
-        xdiff = self.metrics.shadow_offset.x
-        ydiff = self.metrics.shadow_offset.y
-
-        if isinstance(value, XY):
-            ret = XY(value.x + xdiff, value.y + ydiff)
-        elif isinstance(value, Box):
-            ret = Box(value.x1 + xdiff, value.y1 + ydiff,
-                      value.x2 + xdiff, value.y2 + ydiff)
-        elif isinstance(value, (list, tuple)):
-            ret = [self.shift_shadow(x) for x in value]
-
-        return ret
diff --git a/src/blockdiag/noderenderer/actor.py b/src/blockdiag/noderenderer/actor.py
index 17be67c..9db3850 100644
--- a/src/blockdiag/noderenderer/actor.py
+++ b/src/blockdiag/noderenderer/actor.py
@@ -14,25 +14,38 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
-from blockdiag.utils import XY, Box
+from blockdiag.noderenderer.base import NodeShape
+from blockdiag.utils import XY, Box, Size
 
 
 class Actor(NodeShape):
     def __init__(self, node, metrics=None):
         super(Actor, self).__init__(node, metrics)
 
-        shortside = min(self.node.width or metrics.node_height,
-                        self.node.height or metrics.node_height)
+        m = metrics.cell(node)
+        if node.label:
+            font = metrics.font_for(self.node)
+            textsize = metrics.textsize(node.label, font)
+            shortside = min(m.width, m.height - textsize.height)
+        else:
+            textsize = Size(0, 0)
+            shortside = min(m.width, m.height)
+
         r = self.radius = shortside // 8  # radius of actor's head
         self.center = metrics.cell(node).center
 
         self.connectors[0] = XY(self.center.x, self.center.y - r * 9 // 2)
         self.connectors[1] = XY(self.center.x + r * 4, self.center.y)
-        self.connectors[2] = XY(self.center.x, self.center.y + r * 4)
+        self.connectors[2] = XY(self.center.x,
+                                self.center.y + r * 4 + textsize.height)
         self.connectors[3] = XY(self.center.x - r * 4, self.center.y)
 
+        self.textbox = Box(m.left.x,
+                           self.center.y + r * 4,
+                           m.right.x,
+                           self.connectors[2].y)
+
     def head_part(self):
         r = self.radius * 3 // 2
         pt = self.metrics.cell(self.node).center.shift(y=-self.radius * 3)
@@ -76,10 +89,6 @@ class Actor(NodeShape):
     def render_shape(self, drawer, _, **kwargs):
         fill = kwargs.get('fill')
 
-        # FIXME: Actor does not support
-        #  - background image
-        #  - textarea
-
         # draw body part
         body = self.body_part()
         if kwargs.get('shadow'):
@@ -105,9 +114,6 @@ class Actor(NodeShape):
             drawer.ellipse(head, fill=self.node.color,
                            outline=self.node.linecolor, style=self.node.style)
 
-    def render_label(self, drawer, **kwargs):
-        pass
-
 
 def setup(self):
     install_renderer('actor', Actor)
diff --git a/src/blockdiag/noderenderer/__init__.py b/src/blockdiag/noderenderer/base.py
similarity index 87%
copy from src/blockdiag/noderenderer/__init__.py
copy to src/blockdiag/noderenderer/base.py
index 9bf590f..33bf6c2 100644
--- a/src/blockdiag/noderenderer/__init__.py
+++ b/src/blockdiag/noderenderer/base.py
@@ -14,44 +14,11 @@
 #  limitations under the License.
 
 from __future__ import division
-import pkg_resources
 from blockdiag.utils import images, Box, XY
 
-renderers = {}
-searchpath = []
-
-
-def init_renderers():
-    for plugin in pkg_resources.iter_entry_points('blockdiag_noderenderer'):
-        module = plugin.load()
-        if hasattr(module, 'setup'):
-            module.setup(module)
-
-
-def install_renderer(name, renderer):
-    renderers[name] = renderer
-
-
-def set_default_namespace(path):
-    searchpath[:] = []
-    for path in path.split(','):
-        searchpath.append(path)
-
-
-def get(shape):
-    if not renderers:
-        init_renderers()
-
-    for path in searchpath:
-        name = "%s.%s" % (path, shape)
-        if name in renderers:
-            return renderers[name]
-
-    return renderers.get(shape)
-
 
 class NodeShape(object):
-    def __init__(self, node, metrics=None):
+    def __init__(self, node, metrics):
         self.node = node
         self.metrics = metrics
 
diff --git a/src/blockdiag/noderenderer/beginpoint.py b/src/blockdiag/noderenderer/beginpoint.py
index 3229918..dec5e2e 100644
--- a/src/blockdiag/noderenderer/beginpoint.py
+++ b/src/blockdiag/noderenderer/beginpoint.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import XY, Box
 
 
diff --git a/src/blockdiag/noderenderer/box.py b/src/blockdiag/noderenderer/box.py
index 4f2abb3..ab81814 100644
--- a/src/blockdiag/noderenderer/box.py
+++ b/src/blockdiag/noderenderer/box.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 
 
 class Box(NodeShape):
diff --git a/src/blockdiag/noderenderer/circle.py b/src/blockdiag/noderenderer/circle.py
index da404ab..a4e1577 100644
--- a/src/blockdiag/noderenderer/circle.py
+++ b/src/blockdiag/noderenderer/circle.py
@@ -1,7 +1,21 @@
 # -*- coding: utf-8 -*-
+#  Copyright 2011 Takeshi KOMIYA
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
@@ -9,7 +23,8 @@ class Circle(NodeShape):
     def __init__(self, node, metrics=None):
         super(Circle, self).__init__(node, metrics)
 
-        r = min(metrics.node_width, metrics.node_height) // 2 + \
+        cell = metrics.cell(node)
+        r = min(cell.box.width, cell.box.height) // 2 + \
             metrics.cellsize // 2
         pt = metrics.cell(node).center
         self.connectors = [XY(pt.x, pt.y - r),  # top
diff --git a/src/blockdiag/noderenderer/cloud.py b/src/blockdiag/noderenderer/cloud.py
index 9fca4d4..a218fbe 100644
--- a/src/blockdiag/noderenderer/cloud.py
+++ b/src/blockdiag/noderenderer/cloud.py
@@ -14,8 +14,8 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box
 from blockdiag.imagedraw.simplesvg import pathdata
 
diff --git a/src/blockdiag/noderenderer/diamond.py b/src/blockdiag/noderenderer/diamond.py
index 4567c22..3fdb6b2 100644
--- a/src/blockdiag/noderenderer/diamond.py
+++ b/src/blockdiag/noderenderer/diamond.py
@@ -14,8 +14,8 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/dots.py b/src/blockdiag/noderenderer/dots.py
index d5bf313..e2a145f 100644
--- a/src/blockdiag/noderenderer/dots.py
+++ b/src/blockdiag/noderenderer/dots.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/ellipse.py b/src/blockdiag/noderenderer/ellipse.py
index caecd7b..e0f09c8 100644
--- a/src/blockdiag/noderenderer/ellipse.py
+++ b/src/blockdiag/noderenderer/ellipse.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box
 
 
diff --git a/src/blockdiag/noderenderer/endpoint.py b/src/blockdiag/noderenderer/endpoint.py
index ac1eca3..73657b5 100644
--- a/src/blockdiag/noderenderer/endpoint.py
+++ b/src/blockdiag/noderenderer/endpoint.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import XY, Box
 
 
diff --git a/src/blockdiag/noderenderer/flowchart/database.py b/src/blockdiag/noderenderer/flowchart/database.py
index 2396483..3af4bac 100644
--- a/src/blockdiag/noderenderer/flowchart/database.py
+++ b/src/blockdiag/noderenderer/flowchart/database.py
@@ -14,8 +14,8 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import XY, Box
 from blockdiag.imagedraw.simplesvg import pathdata
 
diff --git a/src/blockdiag/noderenderer/flowchart/input.py b/src/blockdiag/noderenderer/flowchart/input.py
index 6e928c4..5359fc1 100644
--- a/src/blockdiag/noderenderer/flowchart/input.py
+++ b/src/blockdiag/noderenderer/flowchart/input.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/flowchart/loopin.py b/src/blockdiag/noderenderer/flowchart/loopin.py
index ac25d0c..b36b8b6 100644
--- a/src/blockdiag/noderenderer/flowchart/loopin.py
+++ b/src/blockdiag/noderenderer/flowchart/loopin.py
@@ -14,8 +14,8 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/flowchart/loopout.py b/src/blockdiag/noderenderer/flowchart/loopout.py
index 19b962d..3be9451 100644
--- a/src/blockdiag/noderenderer/flowchart/loopout.py
+++ b/src/blockdiag/noderenderer/flowchart/loopout.py
@@ -14,8 +14,8 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/flowchart/terminator.py b/src/blockdiag/noderenderer/flowchart/terminator.py
index 4d77baa..7d9d1f7 100644
--- a/src/blockdiag/noderenderer/flowchart/terminator.py
+++ b/src/blockdiag/noderenderer/flowchart/terminator.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import XY, Box
 from blockdiag.imagedraw.simplesvg import pathdata
 
diff --git a/src/blockdiag/noderenderer/mail.py b/src/blockdiag/noderenderer/mail.py
index b7935c3..91c7c2a 100644
--- a/src/blockdiag/noderenderer/mail.py
+++ b/src/blockdiag/noderenderer/mail.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/minidiamond.py b/src/blockdiag/noderenderer/minidiamond.py
index 39014d1..5ad1d07 100644
--- a/src/blockdiag/noderenderer/minidiamond.py
+++ b/src/blockdiag/noderenderer/minidiamond.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/none.py b/src/blockdiag/noderenderer/none.py
index 81ef91d..77ce79c 100644
--- a/src/blockdiag/noderenderer/none.py
+++ b/src/blockdiag/noderenderer/none.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 
 
 class NoneShape(NodeShape):
diff --git a/src/blockdiag/noderenderer/note.py b/src/blockdiag/noderenderer/note.py
index 5cfc15a..9f13351 100644
--- a/src/blockdiag/noderenderer/note.py
+++ b/src/blockdiag/noderenderer/note.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import XY
 
 
diff --git a/src/blockdiag/noderenderer/roundedbox.py b/src/blockdiag/noderenderer/roundedbox.py
index 90f2351..db074d2 100644
--- a/src/blockdiag/noderenderer/roundedbox.py
+++ b/src/blockdiag/noderenderer/roundedbox.py
@@ -13,8 +13,8 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import XY, Box
 from blockdiag.imagedraw.simplesvg import pathdata
 
diff --git a/src/blockdiag/noderenderer/square.py b/src/blockdiag/noderenderer/square.py
index 6b874e1..cc44dd7 100644
--- a/src/blockdiag/noderenderer/square.py
+++ b/src/blockdiag/noderenderer/square.py
@@ -1,7 +1,21 @@
 # -*- coding: utf-8 -*-
+#  Copyright 2011 Takeshi KOMIYA
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import Box, XY
 
 
diff --git a/src/blockdiag/noderenderer/textbox.py b/src/blockdiag/noderenderer/textbox.py
index 3b58cb1..76bf4a7 100644
--- a/src/blockdiag/noderenderer/textbox.py
+++ b/src/blockdiag/noderenderer/textbox.py
@@ -14,8 +14,8 @@
 #  limitations under the License.
 
 from __future__ import division
-from blockdiag.noderenderer import NodeShape
 from blockdiag.noderenderer import install_renderer
+from blockdiag.noderenderer.base import NodeShape
 from blockdiag.utils import images, Box, XY
 
 
diff --git a/src/blockdiag/parser.py b/src/blockdiag/parser.py
index 33bf44a..7f0a14f 100644
--- a/src/blockdiag/parser.py
+++ b/src/blockdiag/parser.py
@@ -46,7 +46,7 @@ from blockdiag.utils.compat import u
 
 ENCODING = 'utf-8'
 
-Diagram = namedtuple('Diagram', 'type id stmts')
+Diagram = namedtuple('Diagram', 'id stmts')
 Group = namedtuple('Group', 'id stmts')
 Node = namedtuple('Node', 'id attrs')
 Attr = namedtuple('Attr', 'name value')
@@ -209,6 +209,11 @@ def parse(seq):
     #        A;
     #     }
     #
+    diagram_id = (
+        (keyword('diagram') | keyword('blockdiag')) +
+        maybe(_id)
+        >> list
+    )
     diagram_inline_stmt = (
         extension_stmt |
         group_inline_stmt
@@ -217,8 +222,7 @@ def parse(seq):
         many(diagram_inline_stmt + skip(maybe(op(';'))))
     )
     diagram = (
-        maybe(keyword('diagram') | keyword('blockdiag')) +
-        maybe(_id) +
+        maybe(diagram_id) +
         op_('{') +
         diagram_inline_stmt_list +
         op_('}')
diff --git a/src/blockdiag/plugins/__init__.py b/src/blockdiag/plugins/__init__.py
index 8dffd4f..6c0da69 100644
--- a/src/blockdiag/plugins/__init__.py
+++ b/src/blockdiag/plugins/__init__.py
@@ -16,6 +16,7 @@
 from pkg_resources import iter_entry_points
 
 node_handlers = []
+general_handlers = {}
 
 
 def load(plugins, diagram, **kwargs):
@@ -26,27 +27,43 @@ def load(plugins, diagram, **kwargs):
                 module.setup(module, diagram, **kwargs)
             break
         else:
-            msg = "WARNING: unknown plugin: %s\n" % name
+            msg = "unknown plugin: %s" % name
             raise AttributeError(msg)
 
 
+def install_general_handler(name, handler):
+    if name not in general_handlers:
+        general_handlers[name] = []
+
+    general_handlers[name].append(handler)
+
+
+def fire_general_event(name, *args):
+    handlers = general_handlers.get(name, [])
+    return all(handler(*args) for handler in handlers)
+
+
 def install_node_handler(handler):
     if handler not in node_handlers:
         node_handlers.append(handler)
 
 
 def fire_node_event(node, name, *args):
-    method = "on_" + name
-    for handler in node_handlers:
-        getattr(handler, method)(node, *args)
+    return all(handler.fire(name, node, *args) for handler in node_handlers)
 
 
 class NodeHandler(object):
     def __init__(self, diagram, **kwargs):
         self.diagram = diagram
 
+    def fire(self, name, *args):
+        return getattr(self, "on_" + name)(*args)
+
     def on_created(self, node):
-        pass
+        return True
+
+    def on_attr_changing(self, node, attr):
+        return True
 
     def on_attr_changed(self, node, attr):
-        pass
+        return True
diff --git a/src/blockdiag/plugins/autoclass.py b/src/blockdiag/plugins/autoclass.py
index 2e74c1d..676e8ce 100644
--- a/src/blockdiag/plugins/autoclass.py
+++ b/src/blockdiag/plugins/autoclass.py
@@ -20,7 +20,7 @@ from blockdiag import plugins
 class AutoClass(plugins.NodeHandler):
     def on_created(self, node):
         if node.id is None:
-            return
+            return True
 
         for name, klass in self.diagram.classes.items():
             pattern = "_%s$" % re.escape(name)
@@ -29,6 +29,8 @@ class AutoClass(plugins.NodeHandler):
                 node.label = re.sub(pattern, '', node.id)
                 node.set_attributes(klass.attrs)
 
+        return True
+
 
 def setup(self, diagram, **kwargs):
     plugins.install_node_handler(AutoClass(diagram, **kwargs))
diff --git a/src/blockdiag/tests/diagrams/README b/src/blockdiag/tests/diagrams/README
new file mode 100644
index 0000000..e0329be
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/README
@@ -0,0 +1,12 @@
+Copyrights of images
+=====================
+
+debian-logo-256color-palettealpha.png
+--------------------------------------
+
+The Debian Open Use Logo(s) are Copyright (c) 1999 Software in the Public Interest, Inc., and are released under the terms of the GNU Lesser General Public License, version 3 or any later version, or, at your option, of the Creative Commons Attribution-ShareAlike 3.0 Unported License.
+
+
+it is modified format::
+
+  $ convert debian-logo-original.png -type PaletteAlpha -colors 256 -transparent #000000 debian-logo-256color-palettealpha.png
diff --git a/src/blockdiag/tests/diagrams/background_url_image.diag b/src/blockdiag/tests/diagrams/background_url_image.diag
index 6d53242..18183a9 100644
--- a/src/blockdiag/tests/diagrams/background_url_image.diag
+++ b/src/blockdiag/tests/diagrams/background_url_image.diag
@@ -1,5 +1,7 @@
 {
   A [background = "http://python.org/images/python-logo.gif"];
-  B [background = "https://si2.twimg.com/profile_images/1287595787/tk0miya.jpg"];
+  B [background = "http://blockdiag.com/favicon.ico"];
+  C [background = "http://upload.wikimedia.org/wikipedia/commons/9/9b/Scalable_Vector_Graphics_Circle2.svg"];
+  D [background = "http://people.sc.fsu.edu/~jburkardt/data/eps/circle.eps"];
   Z;
 }
diff --git a/src/blockdiag/tests/diagrams/debian-logo-256color-palettealpha.png b/src/blockdiag/tests/diagrams/debian-logo-256color-palettealpha.png
new file mode 100644
index 0000000..6557d74
Binary files /dev/null and b/src/blockdiag/tests/diagrams/debian-logo-256color-palettealpha.png differ
diff --git a/src/blockdiag/tests/diagrams/diagram_attributes.diag b/src/blockdiag/tests/diagrams/diagram_attributes.diag
index 2dae457..997c742 100644
--- a/src/blockdiag/tests/diagrams/diagram_attributes.diag
+++ b/src/blockdiag/tests/diagrams/diagram_attributes.diag
@@ -5,6 +5,7 @@ blockdiag {
   span_height = 32;
   default_fontsize = 16;
   default_shape = diamond
+  default_node_style = dotted
   default_node_color = red
   default_group_color = blue
   default_linecolor = gray
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_diagram_type.diag b/src/blockdiag/tests/diagrams/errors/unknown_diagram_type.diag
new file mode 100644
index 0000000..0d3974f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_diagram_type.diag
@@ -0,0 +1,3 @@
+unknown {
+  A
+}
diff --git a/src/blockdiag/tests/diagrams/node_attribute.diag b/src/blockdiag/tests/diagrams/node_attribute.diag
index f31d784..70881b5 100644
--- a/src/blockdiag/tests/diagrams/node_attribute.diag
+++ b/src/blockdiag/tests/diagrams/node_attribute.diag
@@ -9,4 +9,5 @@ diagram {
   H [fontsize = 16];
   I [linecolor = red];
   J [label="Hello", label_orientation=vertical];
+  K [background = "src/blockdiag/tests/diagrams/debian-logo-256color-palettealpha.png"];
 }
diff --git a/src/blockdiag/tests/diagrams/node_icon.diag b/src/blockdiag/tests/diagrams/node_icon.diag
index fc87f99..5a81785 100644
--- a/src/blockdiag/tests/diagrams/node_icon.diag
+++ b/src/blockdiag/tests/diagrams/node_icon.diag
@@ -2,5 +2,5 @@
   A -> B;
 
   A [label = "aaaaaaaaaaaaaaaaa", icon = "/usr/share/pixmaps/debian-logo.png"];
-  B [label = "aaaaaaaaaaaaaaaaa", icon = "https://si2.twimg.com/profile_images/1287595787/tk0miya.jpg"];
+  B [label = "aaaaaaaaaaaaaaaaa", icon = "http://blockdiag.com/favicon.ico"];
 }
diff --git a/src/blockdiag/tests/rst/__init__.py b/src/blockdiag/tests/rst/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/blockdiag/tests/rst/test_base_directives.py b/src/blockdiag/tests/rst/test_base_directives.py
new file mode 100644
index 0000000..3baedf8
--- /dev/null
+++ b/src/blockdiag/tests/rst/test_base_directives.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+
+import sys
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
+
+import os
+import io
+from blockdiag.tests.utils import capture_stderr, TemporaryDirectory
+
+from docutils import nodes
+from docutils.core import publish_doctree
+from docutils.parsers.rst import directives as docutils
+from blockdiag.utils.rst import directives
+from blockdiag.utils.rst.nodes import blockdiag as blockdiag_node
+
+
+class TestRstDirectives(unittest.TestCase):
+    def setUp(self):
+        docutils.register_directive('blockdiag',
+                                    directives.BlockdiagDirectiveBase)
+        self._tmpdir = TemporaryDirectory()
+
+    def tearDown(self):
+        if 'blockdiag' in docutils._directives:
+            del docutils._directives['blockdiag']
+
+        self._tmpdir.clean()
+
+    @capture_stderr
+    def test_without_args(self):
+        text = ".. blockdiag::"
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    def test_block(self):
+        text = (".. blockdiag::\n"
+                "\n"
+                "   { A -> B }")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(blockdiag_node, type(doctree[0]))
+        self.assertEqual('{ A -> B }', doctree[0]['code'])
+        self.assertEqual(None, doctree[0]['alt'])
+        self.assertEqual({}, doctree[0]['options'])
+
+    @capture_stderr
+    def test_emptyblock(self):
+        text = ".. blockdiag::\n\n   \n"
+        text = (".. blockdiag::\n"
+                "\n"
+                "   ")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    def test_filename(self):
+        dirname = os.path.dirname(__file__)
+        filename = os.path.join(dirname, '../diagrams/node_attribute.diag')
+        text = ".. blockdiag:: %s" % filename
+        doctree = publish_doctree(text)
+
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(blockdiag_node, type(doctree[0]))
+        self.assertEqual(io.open(filename, encoding='utf-8-sig').read(),
+                         doctree[0]['code'])
+        self.assertEqual(None, doctree[0]['alt'])
+        self.assertEqual({}, doctree[0]['options'])
+
+    @capture_stderr
+    def test_filename_not_exists(self):
+        text = ".. blockdiag:: unknown.diag"
+        doctree = publish_doctree(text)
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    @capture_stderr
+    def test_both_block_and_filename(self):
+        text = (".. blockdiag:: unknown.diag\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    def test_full_options(self):
+        text = (".. blockdiag::\n"
+                "   :alt: hello world\n"
+                "   :align: center\n"
+                "   :desctable:\n"
+                "   :width: 200\n"
+                "   :height: 100\n"
+                "   :scale: 50%\n"
+                "   :name: foo\n"
+                "   :class: bar\n"
+                "   :figwidth: 400\n"
+                "   :figclass: baz\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(blockdiag_node, type(doctree[0]))
+        self.assertEqual('A -> B', doctree[0]['code'])
+        self.assertEqual('hello world', doctree[0]['alt'])
+        self.assertEqual('center', doctree[0]['options']['align'])
+        self.assertEqual(None, doctree[0]['options']['desctable'])
+        self.assertEqual('200', doctree[0]['options']['width'])
+        self.assertEqual('100', doctree[0]['options']['height'])
+        self.assertEqual(50, doctree[0]['options']['scale'])
+        self.assertEqual('hello world', doctree[0]['options']['alt'])
+        self.assertEqual('foo', doctree[0]['options']['name'])
+        self.assertEqual(['bar'], doctree[0]['options']['classes'])
+        self.assertEqual('400px', doctree[0]['options']['figwidth'])
+        self.assertEqual(['baz'], doctree[0]['options']['figclass'])
+
+    @capture_stderr
+    def test_maxwidth_option(self):
+        text = (".. blockdiag::\n"
+                "   :maxwidth: 200\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(2, len(doctree))
+        self.assertEqual(blockdiag_node, type(doctree[0]))
+        self.assertEqual('A -> B', doctree[0]['code'])
+        self.assertEqual('200', doctree[0]['options']['width'])
+        self.assertEqual(nodes.system_message, type(doctree[1]))
diff --git a/src/blockdiag/tests/test_rst_directives.py b/src/blockdiag/tests/rst/test_blockdiag_directives.py
similarity index 57%
rename from src/blockdiag/tests/test_rst_directives.py
rename to src/blockdiag/tests/rst/test_blockdiag_directives.py
index c22884e..4348eaf 100644
--- a/src/blockdiag/tests/test_rst_directives.py
+++ b/src/blockdiag/tests/rst/test_blockdiag_directives.py
@@ -7,20 +7,17 @@ else:
     import unittest
 
 import os
-import io
 from blockdiag.utils.compat import u
 from blockdiag.tests.utils import capture_stderr, with_pil, TemporaryDirectory
 
 from docutils import nodes
-from docutils.core import publish_doctree, publish_parts
+from docutils.core import publish_doctree
 from docutils.parsers.rst import directives as docutils
 from blockdiag.utils.rst import directives
 
 
 class TestRstDirectives(unittest.TestCase):
     def setUp(self):
-        docutils.register_directive('blockdiag',
-                                    directives.BlockdiagDirectiveBase)
         self._tmpdir = TemporaryDirectory()
 
     def tearDown(self):
@@ -62,102 +59,274 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual(True, options['noviewbox'])
         self.assertEqual(True, options['inline_svg'])
 
-    @capture_stderr
-    def test_base_noargs(self):
-        text = ".. blockdiag::"
+    def test_setup_fontpath1(self):
+        with self.assertRaises(RuntimeError):
+            directives.setup(format='SVG', fontpath=['dummy.ttf'],
+                             outputdir=self.tmpdir)
+            text = (".. blockdiag::\n"
+                    "\n"
+                    "   A -> B")
+            publish_doctree(text)
+
+    def test_setup_fontpath2(self):
+        with self.assertRaises(RuntimeError):
+            directives.setup(format='SVG', fontpath='dummy.ttf',
+                             outputdir=self.tmpdir)
+            text = (".. blockdiag::\n"
+                    "\n"
+                    "   A -> B")
+            publish_doctree(text)
+
+    def test_setup_nodoctype_is_true(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir, nodoctype=True)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.system_message, type(doctree[0]))
+        self.assertEqual(nodes.image, type(doctree[-1]))
+        svg = open(doctree[0]['uri']).read()
+        self.assertEqual("<svg viewBox=\"0 0 ", svg[:18])
 
-    def test_base_with_block(self):
-        text = ".. blockdiag::\n\n   { A -> B }"
+    def test_setup_nodoctype_is_false(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir, nodoctype=False)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(directives.blockdiag, type(doctree[0]))
-        self.assertEqual('{ A -> B }', doctree[0]['code'])
-        self.assertEqual(None, doctree[0]['alt'])
-        self.assertEqual({}, doctree[0]['options'])
+        self.assertEqual(nodes.image, type(doctree[0]))
+        svg = open(doctree[0]['uri']).read()
+        self.assertEqual("<?xml version='1.0' encoding='UTF-8'?>\n"
+                         "<!DOCTYPE ", svg[:49])
 
-    @capture_stderr
-    def test_base_with_emptyblock(self):
-        text = ".. blockdiag::\n\n   \n"
+    def test_setup_noviewbox_is_true(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir, noviewbox=True)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.system_message, type(doctree[0]))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        svg = open(doctree[0]['uri']).read()
+        self.assertRegexpMatches(svg, '<svg height="\d+" width="\d+" ')
 
-    def test_base_with_filename(self):
-        dirname = os.path.dirname(__file__)
-        filename = os.path.join(dirname, 'diagrams/node_attribute.diag')
-        text = ".. blockdiag:: %s" % filename
+    def test_setup_noviewbox_is_false(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir, noviewbox=False)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        svg = open(doctree[0]['uri']).read()
+        self.assertRegexpMatches(svg, '<svg viewBox="0 0 \d+ \d+" ')
 
+    def test_setup_inline_svg_is_true(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(directives.blockdiag, type(doctree[0]))
-        self.assertEqual(io.open(filename).read(), doctree[0]['code'])
-        self.assertEqual(None, doctree[0]['alt'])
-        self.assertEqual({}, doctree[0]['options'])
+        self.assertEqual(nodes.raw, type(doctree[0]))
+        self.assertEqual('html', doctree[0]['format'])
+        self.assertEqual(nodes.Text, type(doctree[0][0]))
+        self.assertEqual("<?xml version='1.0' encoding='UTF-8'?>\n"
+                         "<!DOCTYPE ", doctree[0][0][:49])
+        self.assertEqual(0, len(os.listdir(self.tmpdir)))
 
-    @capture_stderr
-    def test_base_with_filename_not_exists(self):
-        text = ".. blockdiag:: unknown.diag"
+    def test_setup_inline_svg_is_false(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir, inline_svg=False)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
-        self.assertEqual(nodes.system_message, type(doctree[0]))
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(1, len(os.listdir(self.tmpdir)))
 
-    @capture_stderr
-    def test_base_with_block_and_filename(self):
-        text = ".. blockdiag:: unknown.diag\n\n   { A -> B }"
+    @with_pil
+    def test_setup_inline_svg_is_true_but_format_isnt_svg(self):
+        directives.setup(format='PNG', outputdir=self.tmpdir, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.system_message, type(doctree[0]))
+        self.assertEqual(nodes.image, type(doctree[0]))
 
-    def test_base_with_options(self):
-        text = ".. blockdiag::\n   :alt: hello world\n   :desctable:\n" + \
-               "   :maxwidth: 100\n\n   { A -> B }"
+    def test_setup_inline_svg_is_true_with_multibytes(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = u(".. blockdiag::\n"
+                 "\n"
+                 "   あ -> い")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(directives.blockdiag, type(doctree[0]))
-        self.assertEqual('{ A -> B }', doctree[0]['code'])
-        self.assertEqual('hello world', doctree[0]['alt'])
-        self.assertEqual(None, doctree[0]['options']['desctable'])
-        self.assertEqual(100, doctree[0]['options']['maxwidth'])
+        self.assertEqual(nodes.image, type(doctree[0]))
+
+    def test_setup_inline_svg_is_true_and_width_option1(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir,
+                         nodoctype=True, noviewbox=True, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "   :width: 100\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.raw, type(doctree[0]))
+        self.assertEqual(nodes.Text, type(doctree[0][0]))
+        self.assertRegexpMatches(doctree[0][0],
+                                 '<svg height="\d+" width="100" ')
+
+    def test_setup_inline_svg_is_true_and_width_option2(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir,
+                         nodoctype=True, noviewbox=True, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "   :width: 10000\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.raw, type(doctree[0]))
+        self.assertEqual(nodes.Text, type(doctree[0][0]))
+        self.assertRegexpMatches(doctree[0][0],
+                                 '<svg height="\d+" width="10000" ')
+
+    def test_setup_inline_svg_is_true_and_height_option1(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir,
+                         nodoctype=True, noviewbox=True, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "   :height: 100\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.raw, type(doctree[0]))
+        self.assertEqual(nodes.Text, type(doctree[0][0]))
+        self.assertRegexpMatches(doctree[0][0],
+                                 '<svg height="100" width="\d+" ')
 
-    def test_block(self):
+    def test_setup_inline_svg_is_true_and_height_option2(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir,
+                         nodoctype=True, noviewbox=True, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "   :height: 10000\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.raw, type(doctree[0]))
+        self.assertEqual(nodes.Text, type(doctree[0][0]))
+        self.assertRegexpMatches(doctree[0][0],
+                                 '<svg height="10000" width="\d+" ')
+
+    def test_setup_inline_svg_is_true_and_width_and_height_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir,
+                         nodoctype=True, noviewbox=True, inline_svg=True)
+        text = (".. blockdiag::\n"
+                "   :width: 200\n"
+                "   :height: 100\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.raw, type(doctree[0]))
+        self.assertEqual(nodes.Text, type(doctree[0][0]))
+        self.assertRegexpMatches(doctree[0][0],
+                                 '<svg height="100" width="200" ')
+
+    def test_call_with_brace(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "\n"
+                "   {"
+                "     A -> B"
+                "   }")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
+
+    def test_call_without_braces(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n\n   { A -> B }"
+        text = (".. blockdiag::\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
-        self.assertFalse('alt' in doctree[0])
         self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
-        self.assertFalse('target' in doctree[0])
 
-    def test_block_alt(self):
+    def test_alt_option(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+        text = (".. blockdiag::\n"
+                "   :alt: hello world\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
         self.assertEqual('hello world', doctree[0]['alt'])
         self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
-        self.assertFalse('target' in doctree[0])
 
-    def test_block_fontpath1(self):
-        with self.assertRaises(RuntimeError):
-            directives.setup(format='SVG', fontpath=['dummy.ttf'],
-                             outputdir=self.tmpdir)
-            text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
-            publish_doctree(text)
+    def test_align_option1(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :align: left\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual('left', doctree[0]['align'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    def test_block_fontpath2(self):
-        with self.assertRaises(RuntimeError):
-            directives.setup(format='SVG', fontpath='dummy.ttf',
-                             outputdir=self.tmpdir)
-            text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
-            publish_doctree(text)
+    def test_align_option2(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :align: center\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual('center', doctree[0]['align'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    def test_caption(self):
+    def test_align_option3(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :caption: hello world\n\n   { A -> B }"
+        text = (".. blockdiag::\n"
+                "   :align: right\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual('right', doctree[0]['align'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
+
+    @capture_stderr
+    def test_align_option4(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :align: unknown\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+        # clear stderr outputs (ignore ERROR)
+        from io import StringIO
+        sys.stderr = StringIO()
+
+    def test_caption_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :caption: hello world\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
         self.assertEqual(nodes.figure, type(doctree[0]))
@@ -168,109 +337,143 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual(nodes.Text, type(doctree[0][1][0]))
         self.assertEqual('hello world', doctree[0][1][0])
 
-    def test_block_maxwidth(self):
+    def test_caption_option_and_align_option(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :maxwidth: 100\n\n   { A -> B }"
+        text = (".. blockdiag::\n"
+                "   :align: left\n"
+                "   :caption: hello world\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.image, type(doctree[0]))
-        self.assertFalse('alt' in doctree[0])
-        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
-        self.assertFalse(0, doctree[0]['target'].index(self.tmpdir))
+        self.assertEqual(nodes.figure, type(doctree[0]))
+        self.assertEqual('left', doctree[0]['align'])
+        self.assertEqual(2, len(doctree[0]))
+        self.assertEqual(nodes.image, type(doctree[0][0]))
+        self.assertNotIn('align', doctree[0][0])
+        self.assertEqual(nodes.caption, type(doctree[0][1]))
+        self.assertEqual(1, len(doctree[0][1]))
+        self.assertEqual(nodes.Text, type(doctree[0][1][0]))
+        self.assertEqual('hello world', doctree[0][1][0])
 
-    def test_block_nodoctype_false(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir, nodoctype=False)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    @capture_stderr
+    def test_maxwidth_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :maxwidth: 100\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
-        self.assertEqual(1, len(doctree))
+        self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
-        svg = open(doctree[0]['uri']).read()
-        self.assertEqual("<?xml version='1.0' encoding='UTF-8'?>\n"
-                         "<!DOCTYPE ", svg[:49])
+        self.assertEqual('100', doctree[0]['width'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
+        self.assertEqual(nodes.system_message, type(doctree[1]))
 
-    def test_block_nodoctype_true(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir, nodoctype=True)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    def test_width_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :width: 100\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.image, type(doctree[-1]))
-        svg = open(doctree[0]['uri']).read()
-        self.assertNotEqual("<?xml version='1.0' encoding='UTF-8'?>\n"
-                            "<!DOCTYPE ", svg[:49])
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual('100', doctree[0]['width'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    def test_block_noviewbox_false(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir, noviewbox=False)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    def test_height_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :height: 100\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
-        svg = open(doctree[0]['uri']).read()
-        self.assertRegexpMatches(svg, '<svg viewBox="0 0 \d+ \d+" ')
+        self.assertEqual('100', doctree[0]['height'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    def test_block_noviewbox_true(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir, noviewbox=True)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    def test_scale_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :scale: 50%\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
-        svg = open(doctree[0]['uri']).read()
-        self.assertRegexpMatches(svg, '<svg height="\d+" width="\d+" ')
+        self.assertEqual(50, doctree[0]['scale'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    def test_block_inline_svg_false(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir, inline_svg=False)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    def test_name_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :name: foo%\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
-        self.assertEqual(1, len(os.listdir(self.tmpdir)))
+        self.assertEqual(['foo%'], doctree[0]['names'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    def test_block_inline_svg_true(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir, inline_svg=True)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    def test_class_option(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :class: bar%\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.raw, type(doctree[0]))
-        self.assertEqual('html', doctree[0]['format'])
-        self.assertEqual(nodes.Text, type(doctree[0][0]))
-        self.assertEqual("<?xml version='1.0' encoding='UTF-8'?>\n"
-                         "<!DOCTYPE ", doctree[0][0][:49])
-        self.assertEqual(0, len(os.listdir(self.tmpdir)))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(['bar'], doctree[0]['classes'])
+        self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir))
 
-    @with_pil
-    def test_block_inline_svg_true_but_nonsvg_format(self):
-        directives.setup(format='PNG', outputdir=self.tmpdir, inline_svg=True)
-        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+    def test_figwidth_option1(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :caption: hello world\n"
+                "   :figwidth: 100\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(nodes.figure, type(doctree[0]))
+        self.assertEqual('100px', doctree[0]['width'])
 
-    def test_block_inline_svg_true_with_multibytes(self):
+    def test_figwidth_option2(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = u(".. blockdiag::\n   :alt: hello world\n\n   { あ -> い }")
-        publish_parts(source=text)
-
-    def test_block_max_width_inline_svg(self):
-        directives.setup(format='SVG', outputdir=self.tmpdir,
-                         nodoctype=True, noviewbox=True, inline_svg=True)
-        text = ".. blockdiag::\n   :maxwidth: 100\n\n   { A -> B }"
+        text = (".. blockdiag::\n"
+                "   :caption: hello world\n"
+                "   :figwidth: image\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.raw, type(doctree[0]))
-        self.assertEqual(nodes.Text, type(doctree[0][0]))
-        self.assertRegexpMatches(doctree[0][0],
-                                 '<svg height="\d+" width="100" ')
+        self.assertEqual(nodes.figure, type(doctree[0]))
+        self.assertEqual('448px', doctree[0]['width'])
 
-    def test_desctable_without_description(self):
+    def test_figclass_option(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n   { A -> B }"
+        text = (".. blockdiag::\n"
+                "   :caption: hello world\n"
+                "   :figclass: baz\n"
+                "\n"
+                "   A -> B")
         doctree = publish_doctree(text)
         self.assertEqual(1, len(doctree))
-        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(nodes.figure, type(doctree[0]))
+        self.assertEqual(['baz'], doctree[0]['classes'])
 
-    def test_desctable(self):
+    def test_desctable_option(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A [description = foo]; B [description = bar]; }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A [description = foo]"
+                "   B [description = bar]"
+                "   group { A }")
         doctree = publish_doctree(text)
         self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -301,12 +504,24 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual('B', tbody[1][0][0][0])
         self.assertEqual('bar', tbody[1][1][0][0])
 
-    def test_desctable_using_node_group(self):
+    def test_desctable_option_without_description(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A -> B")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+
+    def test_desctable_option_using_node_group(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n   { A -> B; group { A } }"
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A [description = foo]; B [description = bar]; " + \
-               "     group { A } }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A [description = foo]"
+                "   B [description = bar]"
+                "   group { A }")
         doctree = publish_doctree(text)
         self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -360,11 +575,13 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual(1, len(tbody[1][1]))
         self.assertEqual('bar', tbody[1][1][0][0])
 
-    def test_desctable_with_rest_markups(self):
+    def test_desctable_option_with_rest_markups(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A [description = \"foo *bar* **baz**\"]; " + \
-               "     B [description = \"**foo** *bar* baz\"]; }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A [description = \"foo *bar* **baz**\"]"
+                "   B [description = \"**foo** *bar* baz\"]")
         doctree = publish_doctree(text)
         self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -416,10 +633,13 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual(nodes.Text, type(tbody[1][1][0][3]))
         self.assertEqual(' baz', str(tbody[1][1][0][3]))
 
-    def test_desctable_with_numbered(self):
+    def test_desctable_option_with_numbered(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A [numbered = 2]; B [numbered = 1]; }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A [numbered = 2]"
+                "   B [numbered = 1]")
         doctree = publish_doctree(text)
         self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -450,11 +670,13 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual('2', tbody[1][0][0][0])
         self.assertEqual('A', tbody[1][1][0][0])
 
-    def test_desctable_with_numbered_and_description(self):
+    def test_desctable_option_with_numbered_and_description(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A [description = foo, numbered = 2]; " + \
-               "     B [description = bar, numbered = 1]; }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A [description = foo, numbered = 2]"
+                "   B [description = bar, numbered = 1]")
         doctree = publish_doctree(text)
         self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -491,13 +713,15 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual('A', tbody[1][1][0][0])
         self.assertEqual('foo', tbody[1][2][0][0])
 
-    def test_desctable_for_edges(self):
+    def test_desctable_option_for_edges(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A -> B [description = \"foo\"]; " + \
-               "     C -> D [description = \"bar\"]; " + \
-               "     C [label = \"label_C\"]; " + \
-               "     D [label = \"label_D\"]; }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A -> B [description = \"foo\"]"
+                "   C -> D [description = \"bar\"]"
+                "   C [label = \"label_C\"]"
+                "   D [label = \"label_D\"]")
         doctree = publish_doctree(text)
         self.assertEqual(2, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -532,13 +756,15 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual(nodes.Text, type(tbody[1][1][0][0]))
         self.assertEqual('bar', str(tbody[1][1][0][0]))
 
-    def test_desctable_for_nodes_and_edges(self):
+    def test_desctable_option_for_nodes_and_edges(self):
         directives.setup(format='SVG', outputdir=self.tmpdir)
-        text = ".. blockdiag::\n   :desctable:\n\n" + \
-               "   { A -> B [description = \"foo\"]; " + \
-               "     C -> D [description = \"bar\"]; " + \
-               "     C [label = \"label_C\", description = foo]; " + \
-               "     D [label = \"label_D\"]; }"
+        text = (".. blockdiag::\n"
+                "   :desctable:\n"
+                "\n"
+                "   A -> B [description = \"foo\"]"
+                "   C -> D [description = \"bar\"]"
+                "   C [label = \"label_C\", description = foo]"
+                "   D [label = \"label_D\"]")
         doctree = publish_doctree(text)
         self.assertEqual(3, len(doctree))
         self.assertEqual(nodes.image, type(doctree[0]))
@@ -573,3 +799,13 @@ class TestRstDirectives(unittest.TestCase):
         self.assertEqual(1, len(tbody[1][1][0]))
         self.assertEqual(nodes.Text, type(tbody[1][1][0][0]))
         self.assertEqual('bar', str(tbody[1][1][0][0]))
+
+    @capture_stderr
+    def test_broken_diagram(self):
+        directives.setup(format='SVG', outputdir=self.tmpdir)
+        text = (".. blockdiag::\n"
+                "\n"
+                "     A ->")
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
diff --git a/src/blockdiag/tests/test_boot_params.py b/src/blockdiag/tests/test_boot_params.py
index bf850f3..846ee09 100644
--- a/src/blockdiag/tests/test_boot_params.py
+++ b/src/blockdiag/tests/test_boot_params.py
@@ -115,7 +115,7 @@ class TestBootParams(unittest.TestCase):
         try:
             tmp = tempfile.mkstemp()
             config = u("[blockdiag]\nfontpath = /path/to/font\n")
-            io.open(tmp[0], 'wt').write(config)
+            io.open(tmp[0], 'wt', encoding='utf-8-sig').write(config)
 
             options = self.parser.parse(['-c', tmp[1], 'input.diag'])
             self.assertEqual(options.font, ['/path/to/font'])
@@ -166,11 +166,6 @@ class TestBootParams(unittest.TestCase):
         with self.assertRaises(RuntimeError):
             self.parser.parse(['--size', 'foobar', 'input.diag'])
 
-    def test_auto_font_detection(self):
-        options = self.parser.parse(['input.diag'])
-        fontpath = detectfont(options)
-        self.assertTrue(fontpath)
-
     def test_not_exist_fontmap_config(self):
         with self.assertRaises(RuntimeError):
             args = ['--fontmap', '/fontmap_is_not_exist', 'input.diag']
diff --git a/src/blockdiag/tests/test_builder.py b/src/blockdiag/tests/test_builder.py
index 60f22f8..e1d1f3a 100644
--- a/src/blockdiag/tests/test_builder.py
+++ b/src/blockdiag/tests/test_builder.py
@@ -13,6 +13,7 @@ class TestBuilder(BuilderTestCase):
         self.assertEqual(32, diagram.span_height)
         self.assertEqual((128, 128, 128), diagram.linecolor)       # gray
         self.assertEqual('diamond', diagram.nodes[0].shape)
+        self.assertEqual('dotted', diagram.nodes[0].style)
         self.assertEqual((255, 0, 0), diagram.nodes[0].color)      # red
         self.assertEqual((0, 128, 0), diagram.nodes[0].textcolor)  # green
         self.assertEqual(16, diagram.nodes[0].fontsize)
diff --git a/src/blockdiag/tests/test_builder_errors.py b/src/blockdiag/tests/test_builder_errors.py
index bfe15ec..21d7f5e 100644
--- a/src/blockdiag/tests/test_builder_errors.py
+++ b/src/blockdiag/tests/test_builder_errors.py
@@ -95,6 +95,11 @@ class TestBuilderError(BuilderTestCase):
         with self.assertRaises(ParseException):
             self.build(filename)
 
+    def test_unknown_diagram_type(self):
+        filename = 'errors/unknown_diagram_type.diag'
+        with self.assertRaises(ParseException):
+            self.build(filename)
+
     def test_lexer_error_diagram(self):
         filename = 'errors/lexer_error.diag'
         with self.assertRaises(ParseException):
diff --git a/src/blockdiag/tests/test_builder_node.py b/src/blockdiag/tests/test_builder_node.py
index 4291afd..bf3b939 100644
--- a/src/blockdiag/tests/test_builder_node.py
+++ b/src/blockdiag/tests/test_builder_node.py
@@ -63,11 +63,13 @@ class TestBuilderNode(BuilderTestCase):
     def test_node_attribute(self):
         labels = {'A': 'B', 'B': 'double quoted', 'C': 'single quoted',
                   'D': '\'"double" quoted\'', 'E': '"\'single\' quoted"',
-                  'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'Hello'}
+                  'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I',
+                  'J': 'Hello', 'K': 'K'}
         colors = {'A': (255, 0, 0), 'B': (255, 255, 255), 'C': (255, 0, 0),
                   'D': (255, 0, 0), 'E': (255, 0, 0), 'F': (255, 255, 255),
                   'G': (255, 255, 255), 'H': (255, 255, 255),
-                  'I': (255, 255, 255), 'J': (255, 255, 255)}
+                  'I': (255, 255, 255), 'J': (255, 255, 255),
+                  'K': (255, 255, 255)}
         textcolors = defaultdict(lambda: (0, 0, 0))
         textcolors['F'] = (255, 0, 0)
         linecolors = defaultdict(lambda: (0, 0, 0))
@@ -80,6 +82,9 @@ class TestBuilderNode(BuilderTestCase):
         fontsize['H'] = 16
         orientations = defaultdict(lambda: 'horizontal')
         orientations['J'] = 'vertical'
+        backgrounds = defaultdict(lambda: None)
+        backgrounds['K'] = ('src/blockdiag/tests/diagrams/'
+                            'debian-logo-256color-palettealpha.png')
 
         diagram = self.build('node_attribute.diag')
         self.assertNodeLabel(diagram, labels)
@@ -90,6 +95,7 @@ class TestBuilderNode(BuilderTestCase):
         self.assertNodeStacked(diagram, stacked)
         self.assertNodeFontsize(diagram, fontsize)
         self.assertNodeLabel_Orientation(diagram, orientations)
+        self.assertNodeBackground(diagram, backgrounds)
 
     def test_node_height_diagram(self):
         diagram = self.build('node_height.diag')
diff --git a/src/blockdiag/tests/test_generate_diagram.py b/src/blockdiag/tests/test_generate_diagram.py
index 615c99f..e890685 100644
--- a/src/blockdiag/tests/test_generate_diagram.py
+++ b/src/blockdiag/tests/test_generate_diagram.py
@@ -6,6 +6,12 @@ from nose.tools import nottest
 from blockdiag.tests.utils import capture_stderr, TemporaryDirectory
 from blockdiag.tests.utils import supported_pil, supported_pdf
 
+import sys
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
+
 import blockdiag
 import blockdiag.command
 
@@ -19,7 +25,8 @@ def get_fontpath(testdir=None):
 def get_diagram_files(testdir):
     diagramsdir = os.path.join(testdir, 'diagrams')
 
-    skipped = ['errors', 'white.gif']
+    skipped = ['README', 'debian-logo-256color-palettealpha.png',
+               'errors', 'white.gif']
     for file in os.listdir(diagramsdir):
         if file in skipped:
             pass
@@ -60,9 +67,14 @@ def testcase_generator(basepath, mainfunc, files, options):
         if supported_pil() and os.path.exists(fontpath):
             yield generate, mainfunc, 'png', source, options
             yield generate, mainfunc, 'png', source, options + ['--antialias']
+        else:
+            yield unittest.skip("Pillow is not available")(generate)
+            yield unittest.skip("Pillow is not available")(generate)
 
         if supported_pdf() and os.path.exists(fontpath):
             yield generate, mainfunc, 'pdf', source, options
+        else:
+            yield unittest.skip("reportlab is not available")(generate)
 
 
 @capture_stderr
@@ -72,7 +84,8 @@ def generate(mainfunc, filetype, source, options):
         fd, tmpfile = tmpdir.mkstemp()
         os.close(fd)
 
-        mainfunc(['-T', filetype, '-o', tmpfile, source] + list(options))
+        mainfunc(['--debug', '-T', filetype, '-o', tmpfile, source] +
+                 list(options))
     finally:
         tmpdir.clean()
 
diff --git a/src/blockdiag/tests/test_pep8.py b/src/blockdiag/tests/test_pep8.py
index 287ef95..3d74cbf 100644
--- a/src/blockdiag/tests/test_pep8.py
+++ b/src/blockdiag/tests/test_pep8.py
@@ -43,7 +43,7 @@ def test_pep8():
     if report.total_errors:
         if options.count:
             sys.stderr.write(str(report.total_errors) + '\n')
-        #sys.exit(1)
+        # sys.exit(1)
 
     # reporting errors (additional summary)
     errors = report.get_count('E')
diff --git a/src/blockdiag/tests/test_utils.py b/src/blockdiag/tests/test_utils.py
new file mode 100644
index 0000000..97ed133
--- /dev/null
+++ b/src/blockdiag/tests/test_utils.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+import sys
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
+
+from blockdiag.utils import Size
+
+
+class TestUtils(unittest.TestCase):
+    def test_size_resize(self):
+        size = Size(10, 20)
+
+        resized = size.resize(width=50, height=50)
+        self.assertEqual((50, 50), resized)
+
+        resized = size.resize(width=50)
+        self.assertEqual((50, 100), resized)
+
+        resized = size.resize(height=50)
+        self.assertEqual((25, 50), resized)
+
+        resized = size.resize(scale=50)
+        self.assertEqual((5, 10), resized)
+
+        resized = size.resize(width=50, scale=50)
+        self.assertEqual((25, 50), resized)
+
+        resized = size.resize(height=50, scale=50)
+        self.assertEqual((12.5, 25), resized)
+
+        resized = size.resize(width=50, height=50, scale=50)
+        self.assertEqual((25, 25), resized)
+
+    def test_size_to_integer_point(self):
+        size = Size(1.5, 2.5)
+
+        self.assertEqual((1, 2), size.to_integer_point())
diff --git a/src/blockdiag/tests/test_utils_fontmap.py b/src/blockdiag/tests/test_utils_fontmap.py
index 464c40f..5c79da3 100644
--- a/src/blockdiag/tests/test_utils_fontmap.py
+++ b/src/blockdiag/tests/test_utils_fontmap.py
@@ -11,10 +11,7 @@ import tempfile
 from blockdiag.utils.compat import u
 from blockdiag.tests.utils import capture_stderr
 
-try:
-    from io import StringIO
-except ImportError:
-    from cStringIO import StringIO
+from io import StringIO
 from collections import namedtuple
 from blockdiag.utils.fontmap import FontInfo, FontMap
 
@@ -239,6 +236,19 @@ class TestUtilsFontmap(unittest.TestCase):
             self.assertEqual(self.fontpath[1], font1.path)
             self.assertEqual(11, font1.size)
 
+    def test_fontmap_with_capital_character(self):
+        _config = u("[fontmap]\nCapitalCase-sansserif: %s\n") % \
+                  self.fontpath[0]
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        element = FontElement('CapitalCase-sansserif', 11)
+        font1 = fmap.find(element)
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual('capitalcase-sansserif-normal', font1.familyname)
+        self.assertEqual(self.fontpath[0], font1.path)
+        self.assertEqual(11, font1.size)
+
     @capture_stderr
     def test_fontmap_with_nodefault_fontentry(self):
         _config = u("[fontmap]\nserif: %s\n") % self.fontpath[0]
diff --git a/src/blockdiag/tests/utils.py b/src/blockdiag/tests/utils.py
index d51ddd8..87d4aef 100644
--- a/src/blockdiag/tests/utils.py
+++ b/src/blockdiag/tests/utils.py
@@ -16,9 +16,11 @@ from blockdiag.builder import ScreenNodeBuilder
 from blockdiag.parser import parse_file
 
 try:
-    from io import StringIO
-except ImportError:
+    # sys.stderr in py2.x allows mixture of str and unicode
     from cStringIO import StringIO
+except ImportError:
+    # sys.stderr in py3.x allows only str objects (disallow bytes objs)
+    from io import StringIO
 
 
 def supported_pil():
@@ -62,7 +64,7 @@ def capture_stderr(func):
 
             func(*args, **kwargs)
 
-            if re.search('ERROR', sys.stderr.getvalue()):
+            if re.search('(ERROR|Traceback)', sys.stderr.getvalue()):
                 raise AssertionError('Caught error')
         finally:
             if sys.stderr.getvalue():
diff --git a/src/blockdiag/utils/__init__.py b/src/blockdiag/utils/__init__.py
index 8b06a0a..231aa72 100644
--- a/src/blockdiag/utils/__init__.py
+++ b/src/blockdiag/utils/__init__.py
@@ -16,10 +16,6 @@
 from __future__ import division
 import re
 import math
-from collections import namedtuple
-
-
-Size = namedtuple('Size', 'width height')
 
 
 class XY(tuple):
@@ -41,6 +37,40 @@ class XY(tuple):
         return self.__class__(self.x + x, self.y + y)
 
 
+class Size(tuple):
+    def __new__(cls, width, height):
+        return super(Size, cls).__new__(cls, (width, height))
+
+    @property
+    def width(self):
+        return self[0]
+
+    @property
+    def height(self):
+        return self[1]
+
+    def resize(self, **kwargs):
+        if 'width' in kwargs and 'height' in kwargs:
+            size = Size(float(kwargs['width']), float(kwargs['height']))
+        elif 'width' in kwargs:
+            width = float(kwargs['width'])
+            size = Size(width, self.height * width / self.width)
+        elif 'height' in kwargs:
+            height = float(kwargs['height'])
+            size = Size(self.width * height / self.height, height)
+        else:
+            size = self
+
+        if 'scale' in kwargs:
+            scale = float(kwargs['scale']) / 100
+            size = Size(size.width * scale, size.height * scale)
+
+        return size
+
+    def to_integer_point(self):
+        return Size(int(self.width), int(self.height))
+
+
 class Box(list):
     mapper = dict(x1=0, y1=1, x2=2, y2=3, x=0, y=1)
 
diff --git a/src/blockdiag/utils/bootstrap.py b/src/blockdiag/utils/bootstrap.py
index 535f728..1d18719 100644
--- a/src/blockdiag/utils/bootstrap.py
+++ b/src/blockdiag/utils/bootstrap.py
@@ -17,10 +17,13 @@ import os
 import re
 import sys
 import codecs
+import traceback
 from optparse import OptionParser, SUPPRESS_HELP
 from blockdiag import imagedraw
+from blockdiag import plugins
 from blockdiag.utils.config import ConfigParser
 from blockdiag.utils.fontmap import parse_fontpath, FontMap
+from blockdiag.utils.logging import warning, error
 
 
 class Application(object):
@@ -37,17 +40,16 @@ class Application(object):
         except SystemExit as e:
             return e
         except UnicodeEncodeError as e:
-            msg = "ERROR: UnicodeEncodeError caught " + \
-                  "(check your font settings)\n"
-            sys.stderr.write(msg)
+            error("UnicodeEncodeError caught (check your font settings)")
             return -1
         except Exception as e:
             if self.options and self.options.debug:
-                import traceback
                 traceback.print_exc()
             else:
-                sys.stderr.write("ERROR: %s\n" % e)
+                error("%s" % e)
             return -1
+        finally:
+            plugins.fire_general_event('cleanup')
 
     def parse_options(self, args):
         self.options = Options(self.module).parse(args)
@@ -145,7 +147,7 @@ class Options(object):
 
         self.options.type = self.options.type.upper()
         try:
-            imagedraw.create(self.options.type, None)
+            imagedraw.create(self.options.type, None, debug=self.options.debug)
         except:
             msg = "unknown format: %s" % self.options.type
             raise RuntimeError(msg)
@@ -167,9 +169,8 @@ class Options(object):
                 raise RuntimeError(msg)
 
         if self.options.ignore_pil:
-            msg = "WARNING: --ignore-pil option is deprecated " + \
-                  "(detect automatically).\n"
-            sys.stderr.write(msg)
+            warning("--ignore-pil option is deprecated "
+                    "(detect automatically).")
 
         if self.options.nodoctype and self.options.type != 'SVG':
             msg = "--nodoctype option work in SVG images."
diff --git a/src/blockdiag/utils/compat.py b/src/blockdiag/utils/compat.py
index fb7d85a..973cb16 100644
--- a/src/blockdiag/utils/compat.py
+++ b/src/blockdiag/utils/compat.py
@@ -17,8 +17,10 @@ import sys
 
 if sys.version_info[0] == 2:
     string_types = (str, unicode)  # NOQA: pyflakes complains to unicode in py3
+    from urllib import urlopen  # NOQA: exporting for common interface
 else:
     string_types = (str,)
+    from urllib.request import urlopen  # NOQA: exporting for common interface
 
 
 def u(string):
diff --git a/src/blockdiag/utils/fontmap.py b/src/blockdiag/utils/fontmap.py
index 2264283..05c0236 100644
--- a/src/blockdiag/utils/fontmap.py
+++ b/src/blockdiag/utils/fontmap.py
@@ -15,11 +15,10 @@
 
 import re
 import os
-import sys
 import copy
 from collections import namedtuple
 from blockdiag.utils.config import ConfigParser
-from blockdiag.utils.compat import u
+from blockdiag.utils.logging import warning
 
 
 def parse_fontpath(path):
@@ -134,11 +133,10 @@ class FontMap(object):
             font = FontInfo(fontfamily, path, self.fontsize)
             self.fonts[font.familyname] = font
         else:
-            msg = 'fontfile `%s` is not found: %s' % (fontfamily, path)
-            sys.stderr.write("WARNING: %s\n" % msg)
+            warning('fontfile `%s` is not found: %s', fontfamily, path)
 
     def _regulate_familyname(self, name):
-        return FontInfo(name, None, self.BASE_FONTSIZE).familyname
+        return FontInfo(name, None, self.BASE_FONTSIZE).familyname.lower()
 
     def find(self, element=None):
         fontfamily = getattr(element, 'fontfamily', None) or \
@@ -151,8 +149,7 @@ class FontMap(object):
             font = self.fonts[name].duplicate()
             font.size = fontsize
         elif element is not None:
-            msg = "Unknown fontfamily: %s" % fontfamily
-            sys.stderr.write(u("WARNING: %s\n") % msg)
+            warning("Unknown fontfamily: %s", fontfamily)
             elem = namedtuple('Font', 'fontsize')(fontsize)
             font = self.find(elem)
         else:
diff --git a/src/blockdiag/utils/images.py b/src/blockdiag/utils/images.py
index 6cc4b92..fcc7b44 100644
--- a/src/blockdiag/utils/images.py
+++ b/src/blockdiag/utils/images.py
@@ -14,65 +14,32 @@
 #  limitations under the License.
 
 from __future__ import division
+import io
 import re
+from PIL import Image
+from tempfile import NamedTemporaryFile
 from blockdiag.utils import urlutil
-from blockdiag.utils.compat import string_types
-
-try:
-    from PIL import Image
-except ImportError:
-    class Image:
-        @classmethod
-        def open(cls, filename):
-            return cls(filename)
-
-        def __init__(self, filename):
-            self.filename = filename
-
-        @property
-        def size(self):
-            from blockdiag.utils import jpeg
-            import png
-
-            try:
-                size = jpeg.JpegFile.get_size(self.filename)
-            except:
-                try:
-                    if isinstance(self.filename, string_types):
-                        content = open(self.filename, 'r')
-                    else:
-                        self.filename.seek(0)
-                        content = self.filename
-                    image = png.Reader(file=content).read()
-                    size = (image[0], image[1])
-                except:
-                    size = None
-
-            if hasattr(self.filename, 'seek'):
-                self.filename.seek(0)
-
-            return size
-
-_image_size_cache = {}
+from blockdiag.utils.compat import u
+from blockdiag.utils.logging import warning
 
+urlopen_cache = {}
 
-def get_image_size(filename):
-    if filename not in _image_size_cache:
-        uri = filename
-        if urlutil.isurl(filename):
-            try:
-                from io import StringIO
-            except ImportError:
-                from cStringIO import StringIO
-            import urllib
-            try:
-                uri = StringIO(urllib.urlopen(filename).read())
-            except:
-                return None
 
-        _image_size_cache[filename] = Image.open(uri).size
+def urlopen(url, *args, **kwargs):
+    """ auto caching urlopen() (using tempfile) """
+    from blockdiag.utils.compat import urlopen as orig_urlopen
+
+    if url not in urlopen_cache:
+        tmpfile = NamedTemporaryFile()
+        tmpfile.write(orig_urlopen(url, *args, **kwargs).read())
+        tmpfile.flush()
+        urlopen_cache[url] = tmpfile
+
+    return io.open(urlopen_cache[url].name, 'rb')
 
-    return _image_size_cache[filename]
+
+def get_image_size(filename):
+    return open(filename).size
 
 
 def calc_image_size(size, bounded):
@@ -95,3 +62,61 @@ def color_to_rgb(color):
         rgb = webcolors.name_to_rgb(color)
 
     return rgb
+
+
+def wand_open(url, stream):
+    try:
+        import wand.image
+    except:
+        warning("unknown image type: %s", url)
+        raise IOError
+
+    try:
+        png_image = io.BytesIO()
+        with wand.image.Image(file=stream) as img:
+            img.format = 'PNG'
+            img.save(file=png_image)
+            png_image.seek(0)
+            return png_image
+    except Exception as exc:
+        warning("Fail to convert %s to PNG: %r", url, exc)
+        raise IOError
+
+
+def pillow_open(url, stream):
+    try:
+        if isinstance(url, Image.Image):
+            return url
+        else:
+            return Image.open(stream)
+    except IOError:
+        stream.seek(0)
+        png_stream = wand_open(url, stream)
+
+        return Image.open(png_stream)
+
+
+def open(url, mode='Pillow'):
+    if hasattr(url, 'read') or isinstance(url, Image.Image):
+        stream = url
+    elif not urlutil.isurl(url):
+        stream = io.open(url, 'rb')
+    else:
+        try:
+            # wrap BytesIO for rewind stream
+            stream = io.BytesIO(urlopen(url).read())
+        except:
+            warning(u("Could not retrieve: %s"), url)
+            raise IOError
+
+    image = pillow_open(url, stream)
+    if mode.lower() == 'pillow':
+        # stream will be closed by GC
+        return image
+    else:  # mode == 'png'
+        png_image = io.BytesIO()
+        image.save(png_image, 'PNG')
+        stream.close()
+
+        png_image.seek(0)
+        return png_image
diff --git a/src/blockdiag/utils/jpeg.py b/src/blockdiag/utils/jpeg.py
deleted file mode 100644
index 85345f4..0000000
--- a/src/blockdiag/utils/jpeg.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# -*- coding: utf-8 -*-
-#  Copyright 2011 Takeshi KOMIYA
-#
-#  Licensed under the Apache License, Version 2.0 (the "License");
-#  you may not use this file except in compliance with the License.
-#  You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-#  limitations under the License.
-
-
-from blockdiag.utils.compat import string_types
-
-
-class StreamReader(object):
-    def __init__(self, stream):
-        self.stream = stream
-        self.pos = 0
-
-    def read_byte(self):
-        byte = self.stream[self.pos]
-        self.pos += 1
-        return ord(byte)
-
-    def read_word(self):
-        byte1, byte2 = self.stream[self.pos:self.pos + 2]
-        self.pos += 2
-        return (ord(byte1) << 8) + ord(byte2)
-
-    def read_bytes(self, n):
-        _bytes = self.stream[self.pos:self.pos + n]
-        self.pos += n
-        return _bytes
-
-
-class JpegHeaderReader(StreamReader):
-    M_SOI = 0xd8
-    M_SOS = 0xda
-
-    def read_marker(self):
-        if self.read_byte() != 255:
-            raise ValueError("error reading marker")
-        return self.read_byte()
-
-    def skip_marker(self):
-        """Skip over an unknown or uninteresting variable-length marker"""
-        length = self.read_word()
-        self.read_bytes(length - 2)
-
-    def __iter__(self):
-        while True:
-            if self.read_byte() != 255:
-                raise ValueError("error reading marker")
-
-            marker = self.read_byte()
-            if marker == self.M_SOI:
-                length = 0
-                data = ''
-            else:
-                length = self.read_word()
-                data = self.read_bytes(length - 2)
-
-            yield (marker, data)
-
-            if marker == self.M_SOS:
-                raise StopIteration()
-
-
-class JpegFile(object):
-    M_SOF0 = 0xc0
-    M_SOF1 = 0xc1
-
-    @classmethod
-    def get_size(self, filename):
-        if isinstance(filename, string_types):
-            image = open(filename, 'rb').read()
-        else:
-            image = filename.read()
-
-        headers = JpegHeaderReader(image)
-        for header in headers:
-            if header[0] in (self.M_SOF0, self.M_SOF1):
-                data = header[1]
-
-                height = (ord(data[1]) << 8) + ord(data[2])
-                width = (ord(data[3]) << 8) + ord(data[4])
-                return (width, height)
diff --git a/src/blockdiag/utils/rst/nodes.py b/src/blockdiag/utils/logging.py
similarity index 67%
copy from src/blockdiag/utils/rst/nodes.py
copy to src/blockdiag/utils/logging.py
index 14494b9..4aa6b89 100644
--- a/src/blockdiag/utils/rst/nodes.py
+++ b/src/blockdiag/utils/logging.py
@@ -13,8 +13,17 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from docutils import nodes
+import sys
+from blockdiag.utils.compat import u
 
 
-class blockdiag(nodes.General, nodes.Element):
-    pass
+def warning(msg, *args):
+    sys.stderr.write(u("WARNING: "))
+    sys.stderr.write(msg % args)
+    sys.stderr.write(u("\n"))
+
+
+def error(msg, *args):
+    sys.stderr.write(u("ERROR: "))
+    sys.stderr.write(msg % args)
+    sys.stderr.write(u("\n"))
diff --git a/src/blockdiag/utils/rst/directives.py b/src/blockdiag/utils/rst/directives.py
index 18f116b..44cbd36 100644
--- a/src/blockdiag/utils/rst/directives.py
+++ b/src/blockdiag/utils/rst/directives.py
@@ -15,16 +15,16 @@
 
 import os
 import io
+from hashlib import sha1
 from collections import namedtuple
 from docutils import nodes
 from docutils.parsers import rst
+from docutils.parsers.rst.roles import set_classes
 from docutils.statemachine import ViewList
-from blockdiag import parser
-from blockdiag.builder import ScreenNodeBuilder
-from blockdiag.drawer import DiagramDraw
+
 from blockdiag.utils.bootstrap import create_fontmap
 from blockdiag.utils.compat import string_types
-from blockdiag.utils.rst.nodes import blockdiag
+from blockdiag.utils.rst.nodes import blockdiag as blockdiag_node
 
 directive_options_default = dict(format='PNG',
                                  antialias=False,
@@ -46,10 +46,22 @@ def relfn2path(env, filename):
     return relfn, os.path.join(env.srcdir, relfn)
 
 
+def align(argument):
+    align_values = ('left', 'center', 'right')
+    return rst.directives.choice(argument, align_values)
+
+
+def figwidth_value(argument):
+    if argument.lower() == 'image':
+        return 'image'
+    else:
+        return rst.directives.length_or_percentage_or_unitless(argument, 'px')
+
+
 class BlockdiagDirectiveBase(rst.Directive):
     """ Directive to insert arbitrary dot markup. """
     name = "blockdiag"
-    node_class = blockdiag
+    node_class = blockdiag_node
 
     has_content = True
     required_arguments = 0
@@ -57,9 +69,17 @@ class BlockdiagDirectiveBase(rst.Directive):
     final_argument_whitespace = False
     option_spec = {
         'alt': rst.directives.unchanged,
+        'height': rst.directives.length_or_unitless,
+        'width': rst.directives.length_or_percentage_or_unitless,
+        'scale': rst.directives.percentage,
+        'align': align,
         'caption': rst.directives.unchanged,
         'desctable': rst.directives.flag,
-        'maxwidth': rst.directives.nonnegative_int,
+        'maxwidth': rst.directives.nonnegative_int,  # deprecated
+        'name': rst.directives.unchanged,
+        'class': rst.directives.class_option,
+        'figwidth': figwidth_value,
+        'figclass': rst.directives.class_option,
     }
 
     def run(self):
@@ -88,19 +108,27 @@ class BlockdiagDirectiveBase(rst.Directive):
                 return [self.state_machine.reporter.warning(msg,
                                                             line=self.lineno)]
 
+        set_classes(self.options)
         node = self.node_class()
+        results = [node]
+
         node['code'] = dotcode
+        node['caption'] = self.options.pop('caption', None)
+        node['options'] = self.options
+
+        # for sphinxcontrib.* module (backward compatibility)
         node['alt'] = self.options.get('alt')
-        if 'caption' in self.options:
-            node['caption'] = self.options.get('caption')
 
-        node['options'] = {}
-        if 'maxwidth' in self.options:
-            node['options']['maxwidth'] = self.options['maxwidth']
-        if 'desctable' in self.options:
-            node['options']['desctable'] = self.options['desctable']
+        # replace maxwidth option to width (backward compatibility)
+        if 'maxwidth' in node['options']:
+            node['options']['width'] = str(node['options']['maxwidth'])
+
+            msg = ':maxwidth: option is deprecated. Use :width: option.'
+            warning = self.state_machine.reporter.warning(msg,
+                                                          line=self.lineno)
+            results.append(warning)
 
-        return [node]
+        return results
 
     def source_filename(self, filename):
         if hasattr(self.state.document.settings, 'env'):
@@ -112,7 +140,16 @@ class BlockdiagDirectiveBase(rst.Directive):
 
 
 class BlockdiagDirective(BlockdiagDirectiveBase):
+    processor = None  # backward compatibility for 1.4.0
+
     def run(self):
+        figwidth = self.options.pop('figwidth', None)
+        figclasses = self.options.pop('figclass', None)
+        if self.options.get('caption'):
+            align = self.options.pop('align', None)
+        else:
+            align = None
+
         results = super(BlockdiagDirective, self).run()
 
         node = results[0]
@@ -122,18 +159,29 @@ class BlockdiagDirective(BlockdiagDirectiveBase):
         try:
             diagram = self.node2diagram(node)
         except Exception as e:
-            raise self.warning(e.message)
+            raise self.warning(str(e))
 
         if 'desctable' in node['options']:
-            del node['options']['desctable']
             results += self.description_tables(diagram)
 
         results[0] = self.node2image(node, diagram)
+        self.add_name(results[0])
 
-        if 'caption' in node:
+        if node.get('caption'):
             fig = nodes.figure()
             fig += results[0]
             fig += nodes.caption(text=node['caption'])
+
+            if figwidth == 'image':
+                width = self.get_actual_width(node, diagram)
+                fig['width'] = str(width) + 'px'
+            elif figwidth is not None:
+                fig['width'] = figwidth
+            if figclasses:
+                fig['classes'] += figclasses
+            if align:
+                fig['align'] = align
+
             results[0] = fig
 
         return results
@@ -143,55 +191,66 @@ class BlockdiagDirective(BlockdiagDirectiveBase):
         return directive_options
 
     def node2diagram(self, node):
-        tree = parser.parse_string(node['code'])
-        return ScreenNodeBuilder.build(tree)
+        if hasattr(node, 'to_diagram'):
+            return node.to_diagram()
+        else:
+            try:
+                tree = self.processor.parser.parse_string(node['code'])
+            except:
+                code = '%s { %s }' % (self.name, node['code'])
+                tree = self.processor.parser.parse_string(code)
+                node['code'] = code  # replace if succeeded
 
-    def node2image(self, node, diagram):
-        options = node['options']
-        filename = self.image_filename(node)
+            return self.processor.builder.ScreenNodeBuilder.build(tree)
+
+    def get_actual_width(self, node, diagram):
         fontmap = self.create_fontmap()
-        _format = self.global_options['format'].lower()
+        if hasattr(node, 'to_drawer'):
+            drawer = node.to_drawer('SVG', None, fontmap,
+                                    **self.global_options)
+        else:
+            drawer = self.processor.drawer.DiagramDraw('SVG', diagram,
+                                                       None, fontmap=fontmap)
+
+        return drawer.pagesize()[0]
 
+    def node2image(self, node, diagram):
+        _format = self.global_options['format'].lower()
         if _format == 'svg' and self.global_options['inline_svg'] is True:
-            filename = None
+            return self.node2image_inline_svg(node, diagram)
 
-        kwargs = dict(self.global_options)
-        del kwargs['format']
-        drawer = DiagramDraw(_format, diagram, filename,
-                             fontmap=fontmap, **kwargs)
+        filename = self.image_filename(node)
+        fontmap = self.create_fontmap()
+        if hasattr(node, 'to_drawer'):
+            drawer = node.to_drawer(_format, filename, fontmap,
+                                    **self.global_options)
+        else:
+            drawer = self.processor.drawer.DiagramDraw(_format, diagram,
+                                                       filename,
+                                                       fontmap=fontmap,
+                                                       **self.global_options)
 
-        if filename is None or not os.path.isfile(filename):
+        if not os.path.isfile(filename):
             drawer.draw()
-            content = drawer.save()
-
-            if _format == 'svg' and self.global_options['inline_svg'] is True:
-                size = drawer.pagesize()
-                if 'maxwidth' in options and options['maxwidth'] < size[0]:
-                    ratio = float(options['maxwidth']) / size[0]
-                    new_size = (options['maxwidth'], int(size[1] * ratio))
-                    content = drawer.save(new_size)
-
-                return nodes.raw('', content, format='html')
+            drawer.save()
 
-        size = drawer.pagesize()
-        if 'maxwidth' in options and options['maxwidth'] < size[0]:
-            ratio = float(options['maxwidth']) / size[0]
-            thumb_size = (options['maxwidth'], int(size[1] * ratio))
+        return nodes.image(uri=filename, **node['options'])
 
-            thumb_filename = self.image_filename(node, prefix='_thumb')
-            if not os.path.isfile(thumb_filename):
-                drawer.filename = thumb_filename
-                drawer.draw()
-                drawer.save(thumb_size)
-
-            image = nodes.image(uri=thumb_filename, target=filename)
+    def node2image_inline_svg(self, node, diagram):
+        fontmap = self.create_fontmap()
+        if hasattr(node, 'to_drawer'):
+            drawer = node.to_drawer('SVG', None, fontmap,
+                                    **self.global_options)
         else:
-            image = nodes.image(uri=filename)
+            drawer = self.processor.drawer.DiagramDraw('svg', diagram,
+                                                       None, fontmap=fontmap,
+                                                       **self.global_options)
+        drawer.draw()
 
-        if node['alt']:
-            image['alt'] = node['alt']
+        size = drawer.pagesize().resize(**node['options']).to_integer_point()
+        content = drawer.save(size)
 
-        return image
+        return nodes.raw('', content, format='html')
 
     def create_fontmap(self):
         Options = namedtuple('Options', 'font fontmap')
@@ -206,26 +265,22 @@ class BlockdiagDirective(BlockdiagDirectiveBase):
         return create_fontmap(options)
 
     def image_filename(self, node, prefix='', ext='png'):
-        try:
-            from hashlib import sha1
-            sha = sha1
-        except ImportError:
-            from sha import sha
-            sha = sha
-
-        options = dict(node['options'])
-        options.update(font=self.global_options['fontpath'],
-                       antialias=self.global_options['antialias'])
-        hashseed = (node['code'] + str(options)).encode('utf-8')
-        hashed = sha(hashseed).hexdigest()
-
-        _format = self.global_options['format']
-        outputdir = self.global_options['outputdir']
-        filename = "%s%s-%s.%s" % (self.name, prefix, hashed, _format.lower())
-        if outputdir:
-            filename = os.path.join(outputdir, filename)
-
-        return filename
+        if hasattr(node, 'get_path'):
+            return node.get_path(**self.global_options)
+        else:
+            options = dict(node['options'])
+            options.update(font=self.global_options['fontpath'],
+                           antialias=self.global_options['antialias'])
+            hashseed = (node['code'] + str(options)).encode('utf-8')
+            hashed = sha1(hashseed).hexdigest()
+
+            _format = self.global_options['format'].lower()
+            outputdir = self.global_options['outputdir']
+            filename = "%s%s-%s.%s" % (self.name, prefix, hashed, _format)
+            if outputdir:
+                filename = os.path.join(outputdir, filename)
+
+            return filename
 
     def description_tables(self, diagram):
         tables = []
diff --git a/src/blockdiag/utils/rst/nodes.py b/src/blockdiag/utils/rst/nodes.py
index 14494b9..71c80c0 100644
--- a/src/blockdiag/utils/rst/nodes.py
+++ b/src/blockdiag/utils/rst/nodes.py
@@ -13,8 +13,42 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+import os
+from hashlib import sha1
 from docutils import nodes
+import blockdiag.parser
+import blockdiag.builder
+import blockdiag.drawer
 
 
 class blockdiag(nodes.General, nodes.Element):
-    pass
+    name = 'blockdiag'
+    processor = blockdiag
+
+    def to_diagram(self):
+        try:
+            tree = self.processor.parser.parse_string(self['code'])
+        except:
+            code = '%s { %s }' % (self.name, self['code'])
+            tree = self.processor.parser.parse_string(code)
+            self['code'] = code  # replace if succeeded
+
+        return self.processor.builder.ScreenNodeBuilder.build(tree)
+
+    def to_drawer(self, image_format, filename, fontmap, **kwargs):
+        diagram = self.to_diagram()
+        return self.processor.drawer.DiagramDraw(image_format, diagram,
+                                                 filename, fontmap=fontmap,
+                                                 **kwargs)
+
+    def get_path(self, **options):
+        options.update(self['options'])
+        hashseed = (self['code'] + str(options)).encode('utf-8')
+        hashed = sha1(hashseed).hexdigest()
+
+        filename = "%s-%s.%s" % (self.name, hashed, options['format'].lower())
+        outputdir = options.get('outputdir')
+        if outputdir:
+            filename = os.path.join(outputdir, filename)
+
+        return filename
diff --git a/src/blockdiag/utils/urlutil.py b/src/blockdiag/utils/urlutil.py
index 6d18c45..70b1699 100644
--- a/src/blockdiag/utils/urlutil.py
+++ b/src/blockdiag/utils/urlutil.py
@@ -1,4 +1,17 @@
 # -*- coding: utf-8 -*-
+#  Copyright 2011 Takeshi KOMIYA
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
 
 try:
     import urlparse
diff --git a/tox.ini b/tox.ini
index ad483b6..3b0b529 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,12 +1,14 @@
 [tox]
-envlist=py26,py27,py32,py33
+envlist=py26,py27,py32,py33,py34
 
 [testenv]
 deps=
     nose
-    pep8
+    mock
     flake8
     docutils
+    reportlab
+    wand
 commands=
     nosetests
     flake8 src
@@ -14,7 +16,9 @@ commands=
 [testenv:py26]
 deps=
     nose
-    pep8
+    mock
     flake8
     docutils
     unittest2
+    reportlab
+    wand

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/blockdiag.git



More information about the debian-science-commits mailing list