[blockdiag] 02/29: Import Upstream version 1.1.2

Andreas Tille tille at debian.org
Tue Jan 10 21:35:57 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 3c0d4a66eeb66befba8348fee71e4be6bdff46f2
Author: Andreas Tille <tille at debian.org>
Date:   Tue Jan 10 11:08:01 2017 +0100

    Import Upstream version 1.1.2
---
 LICENSE                                            | 202 +++++
 MANIFEST.in                                        |   8 +
 PKG-INFO                                           | 449 ++++++++++
 blockdiag.1                                        |  72 ++
 bootstrap.py                                       | 200 +++++
 buildout.cfg                                       |  36 +
 examples/group.diag                                |  13 +
 examples/group.png                                 | Bin 0 -> 10146 bytes
 examples/group.svg                                 |  52 ++
 examples/multibyte.diag                            |  11 +
 examples/multibyte.png                             | Bin 0 -> 5989 bytes
 examples/multibyte.svg                             |  53 ++
 examples/numbered.diag                             |   9 +
 examples/numbered.png                              | Bin 0 -> 4503 bytes
 examples/numbered.svg                              |  36 +
 examples/screen.diag                               |  38 +
 examples/screen.png                                | Bin 0 -> 22769 bytes
 examples/screen.svg                                |  96 ++
 examples/simple.diag                               |   5 +
 examples/simple.png                                | Bin 0 -> 6485 bytes
 examples/simple.svg                                |  48 +
 setup.cfg                                          |  11 +
 setup.py                                           | 118 +++
 src/README.txt                                     | 417 +++++++++
 src/TODO.txt                                       |  14 +
 src/blockdiag.egg-info/PKG-INFO                    | 449 ++++++++++
 src/blockdiag.egg-info/SOURCES.txt                 | 234 +++++
 src/blockdiag.egg-info/dependency_links.txt        |   1 +
 src/blockdiag.egg-info/entry_points.txt            |  31 +
 src/blockdiag.egg-info/requires.txt                |  16 +
 src/blockdiag.egg-info/top_level.txt               |   2 +
 src/blockdiag/DiagramDraw.py                       |  16 +
 src/blockdiag/DiagramMetrics.py                    |  16 +
 src/blockdiag/__init__.py                          |  16 +
 src/blockdiag/builder.py                           | 730 +++++++++++++++
 src/blockdiag/command.py                           |  64 ++
 src/blockdiag/drawer.py                            | 191 ++++
 src/blockdiag/elements.py                          | 630 +++++++++++++
 src/blockdiag/imagedraw/__init__.py                |  41 +
 src/blockdiag/imagedraw/filters/__init__.py        |  14 +
 src/blockdiag/imagedraw/filters/linejump.py        | 158 ++++
 src/blockdiag/imagedraw/pdf.py                     | 216 +++++
 src/blockdiag/imagedraw/png.py                     | 375 ++++++++
 src/blockdiag/imagedraw/simplesvg.py               | 231 +++++
 src/blockdiag/imagedraw/svg.py                     | 286 ++++++
 src/blockdiag/metrics.py                           | 974 +++++++++++++++++++++
 src/blockdiag/noderenderer/__init__.py             | 166 ++++
 src/blockdiag/noderenderer/actor.py                | 106 +++
 src/blockdiag/noderenderer/beginpoint.py           |  57 ++
 src/blockdiag/noderenderer/box.py                  |  43 +
 src/blockdiag/noderenderer/circle.py               |  39 +
 src/blockdiag/noderenderer/cloud.py                | 124 +++
 src/blockdiag/noderenderer/diamond.py              |  58 ++
 src/blockdiag/noderenderer/dots.py                 |  53 ++
 src/blockdiag/noderenderer/ellipse.py              |  50 ++
 src/blockdiag/noderenderer/endpoint.py             |  64 ++
 src/blockdiag/noderenderer/flowchart/__init__.py   |  14 +
 src/blockdiag/noderenderer/flowchart/database.py   | 121 +++
 src/blockdiag/noderenderer/flowchart/input.py      |  60 ++
 src/blockdiag/noderenderer/flowchart/loopin.py     |  63 ++
 src/blockdiag/noderenderer/flowchart/loopout.py    |  63 ++
 src/blockdiag/noderenderer/flowchart/terminator.py | 109 +++
 src/blockdiag/noderenderer/mail.py                 |  60 ++
 src/blockdiag/noderenderer/minidiamond.py          |  50 ++
 src/blockdiag/noderenderer/none.py                 |  35 +
 src/blockdiag/noderenderer/note.py                 |  58 ++
 src/blockdiag/noderenderer/roundedbox.py           | 122 +++
 src/blockdiag/noderenderer/square.py               |  43 +
 src/blockdiag/noderenderer/textbox.py              |  47 +
 src/blockdiag/parser.py                            | 190 ++++
 src/blockdiag/plugins/__init__.py                  |  52 ++
 src/blockdiag/plugins/attributes.py                |  36 +
 src/blockdiag/plugins/autoclass.py                 |  34 +
 src/blockdiag/tests/__init__.py                    |   0
 .../tests/diagrams/auto_jumping_edge.diag          |   4 +
 .../tests/diagrams/background_url_image.diag       |   5 +
 src/blockdiag/tests/diagrams/beginpoint_color.diag |   3 +
 src/blockdiag/tests/diagrams/branched.diag         |   5 +
 src/blockdiag/tests/diagrams/circular_ref.diag     |   5 +
 .../diagrams/circular_ref_and_parent_node.diag     |   5 +
 .../tests/diagrams/circular_ref_to_root.diag       |   5 +
 .../tests/diagrams/circular_skipped_edge.diag      |   5 +
 src/blockdiag/tests/diagrams/define_class.diag     |   8 +
 .../tests/diagrams/diagram_attributes.diag         |  17 +
 .../tests/diagrams/diagram_attributes_order.diag   |   6 +
 .../tests/diagrams/diagram_orientation.diag        |   7 +
 src/blockdiag/tests/diagrams/edge_attribute.diag   |   5 +
 src/blockdiag/tests/diagrams/edge_label.diag       |   3 +
 .../tests/diagrams/edge_layout_landscape.diag      |   6 +
 .../tests/diagrams/edge_layout_portrait.diag       |   7 +
 src/blockdiag/tests/diagrams/edge_shape.diag       |   3 +
 src/blockdiag/tests/diagrams/edge_styles.diag      |   9 +
 src/blockdiag/tests/diagrams/empty_group.diag      |   5 +
 .../tests/diagrams/empty_group_declaration.diag    |  10 +
 .../tests/diagrams/empty_nested_group.diag         |   7 +
 src/blockdiag/tests/diagrams/endpoint_color.diag   |   3 +
 .../diagrams/errors/belongs_to_two_groups.diag     |   9 +
 .../tests/diagrams/errors/group_follows_node.diag  |   6 +
 .../tests/diagrams/errors/lexer_error.diag         |   3 +
 .../tests/diagrams/errors/node_follows_group.diag  |   6 +
 .../errors/unknown_diagram_default_shape.diag      |   6 +
 .../errors/unknown_diagram_edge_layout.diag        |   3 +
 .../errors/unknown_diagram_orientation.diag        |   3 +
 .../tests/diagrams/errors/unknown_edge_class.diag  |   3 +
 .../tests/diagrams/errors/unknown_edge_dir.diag    |   3 +
 .../tests/diagrams/errors/unknown_edge_hstyle.diag |   3 +
 .../tests/diagrams/errors/unknown_edge_style.diag  |   3 +
 .../tests/diagrams/errors/unknown_group_class.diag |   6 +
 .../diagrams/errors/unknown_group_orientation.diag |   6 +
 .../tests/diagrams/errors/unknown_group_shape.diag |   8 +
 .../diagrams/errors/unknown_node_attribute.diag    |   3 +
 .../tests/diagrams/errors/unknown_node_class.diag  |   3 +
 .../tests/diagrams/errors/unknown_node_shape.diag  |   5 +
 .../tests/diagrams/errors/unknown_node_style.diag  |   3 +
 .../tests/diagrams/errors/unknown_plugin.diag      |   5 +
 src/blockdiag/tests/diagrams/flowable_node.diag    |   5 +
 src/blockdiag/tests/diagrams/folded_edge.diag      |   6 +
 .../tests/diagrams/group_and_skipped_edge.diag     |   9 +
 src/blockdiag/tests/diagrams/group_attribute.diag  |  10 +
 .../tests/diagrams/group_children_height.diag      |  12 +
 .../tests/diagrams/group_children_order.diag       |  12 +
 .../tests/diagrams/group_children_order2.diag      |  14 +
 .../tests/diagrams/group_children_order3.diag      |  19 +
 .../tests/diagrams/group_children_order4.diag      |   9 +
 .../diagrams/group_declare_as_node_attribute.diag  |  11 +
 src/blockdiag/tests/diagrams/group_height.diag     |   9 +
 .../group_id_and_node_id_are_not_conflicted.diag   |   7 +
 src/blockdiag/tests/diagrams/group_label.diag      |   7 +
 src/blockdiag/tests/diagrams/group_order.diag      |   8 +
 src/blockdiag/tests/diagrams/group_order2.diag     |  13 +
 src/blockdiag/tests/diagrams/group_order3.diag     |  16 +
 .../tests/diagrams/group_orientation.diag          |  10 +
 src/blockdiag/tests/diagrams/group_sibling.diag    |  10 +
 .../tests/diagrams/group_works_node_decorator.diag |   9 +
 .../tests/diagrams/labeled_circular_ref.diag       |   7 +
 .../tests/diagrams/large_group_and_node.diag       |  10 +
 .../tests/diagrams/large_group_and_node2.diag      |   7 +
 .../tests/diagrams/large_group_and_two_nodes.diag  |  11 +
 src/blockdiag/tests/diagrams/merge_groups.diag     |   9 +
 src/blockdiag/tests/diagrams/multiple_groups.diag  |  16 +
 .../tests/diagrams/multiple_nested_groups.diag     |  14 +
 .../tests/diagrams/multiple_node_relation.diag     |   4 +
 .../tests/diagrams/multiple_nodes_definition.diag  |   4 +
 .../tests/diagrams/multiple_parent_node.diag       |   6 +
 .../tests/diagrams/nested_group_orientation.diag   |  13 +
 .../tests/diagrams/nested_group_orientation2.diag  |  14 +
 src/blockdiag/tests/diagrams/nested_groups.diag    |   9 +
 .../tests/diagrams/nested_groups_and_edges.diag    |  11 +
 .../nested_groups_work_node_decorator.diag         |  11 +
 .../tests/diagrams/nested_skipped_circular.diag    |   7 +
 src/blockdiag/tests/diagrams/node_attribute.diag   |  11 +
 .../tests/diagrams/node_attribute_and_group.diag   |  14 +
 .../tests/diagrams/node_has_multilined_label.diag  |   5 +
 src/blockdiag/tests/diagrams/node_height.diag      |   6 +
 src/blockdiag/tests/diagrams/node_icon.diag        |   6 +
 .../tests/diagrams/node_id_includes_dot.diag       |   4 +
 .../diagrams/node_in_group_follows_outer_node.diag |   7 +
 src/blockdiag/tests/diagrams/node_link.diag        |   9 +
 .../tests/diagrams/node_rotated_labels.diag        |   7 +
 src/blockdiag/tests/diagrams/node_shape.diag       |  29 +
 .../tests/diagrams/node_shape_background.diag      |  29 +
 .../tests/diagrams/node_shape_namespace.diag       |   8 +
 .../tests/diagrams/node_style_dasharray.diag       |   7 +
 src/blockdiag/tests/diagrams/node_styles.diag      |   5 +
 .../tests/diagrams/node_width_and_height.diag      |   6 +
 .../diagrams/non_rhombus_relation_height.diag      |   8 +
 .../diagrams/outer_node_follows_node_in_group.diag |   7 +
 .../tests/diagrams/plugin_attributes.diag          |   6 +
 src/blockdiag/tests/diagrams/plugin_autoclass.diag |   7 +
 src/blockdiag/tests/diagrams/portrait_dots.diag    |   5 +
 src/blockdiag/tests/diagrams/quoted_node_id.diag   |   4 +
 .../tests/diagrams/reverse_multiple_groups.diag    |  16 +
 .../tests/diagrams/rhombus_relation_height.diag    |   4 +
 src/blockdiag/tests/diagrams/self_ref.diag         |   4 +
 src/blockdiag/tests/diagrams/separate1.diag        |  16 +
 src/blockdiag/tests/diagrams/separate2.diag        |  22 +
 src/blockdiag/tests/diagrams/simple_group.diag     |   7 +
 src/blockdiag/tests/diagrams/single_edge.diag      |   3 +
 src/blockdiag/tests/diagrams/single_node.diag      |   3 +
 src/blockdiag/tests/diagrams/skipped_circular.diag |   6 +
 src/blockdiag/tests/diagrams/skipped_edge.diag     |   5 +
 .../tests/diagrams/skipped_edge_down.diag          |   4 +
 .../diagrams/skipped_edge_flowchart_rightdown.diag |   8 +
 .../skipped_edge_flowchart_rightdown2.diag         |   8 +
 .../tests/diagrams/skipped_edge_leftdown.diag      |   6 +
 .../tests/diagrams/skipped_edge_portrait_down.diag |   6 +
 .../skipped_edge_portrait_flowchart_rightdown.diag |   8 +
 ...skipped_edge_portrait_flowchart_rightdown2.diag |   9 +
 .../diagrams/skipped_edge_portrait_leftdown.diag   |   6 +
 .../diagrams/skipped_edge_portrait_right.diag      |   6 +
 .../diagrams/skipped_edge_portrait_rightdown.diag  |   8 +
 .../tests/diagrams/skipped_edge_right.diag         |   4 +
 .../tests/diagrams/skipped_edge_rightdown.diag     |   5 +
 .../tests/diagrams/skipped_edge_rightup.diag       |   4 +
 src/blockdiag/tests/diagrams/skipped_edge_up.diag  |   5 +
 .../tests/diagrams/skipped_twin_circular.diag      |   7 +
 src/blockdiag/tests/diagrams/slided_children.diag  |   8 +
 .../tests/diagrams/stacked_node_and_edges.diag     |   6 +
 src/blockdiag/tests/diagrams/triple_branched.diag  |   6 +
 .../tests/diagrams/twin_circular_ref.diag          |   5 +
 .../tests/diagrams/twin_circular_ref_to_root.diag  |   5 +
 src/blockdiag/tests/diagrams/twin_forked.diag      |   7 +
 .../tests/diagrams/twin_multiple_parent_node.diag  |   7 +
 src/blockdiag/tests/diagrams/two_edges.diag        |   3 +
 src/blockdiag/tests/test_boot_params.py            | 156 ++++
 src/blockdiag/tests/test_builder.py                | 158 ++++
 src/blockdiag/tests/test_builder_edge.py           | 155 ++++
 src/blockdiag/tests/test_builder_errors.py         | 100 +++
 src/blockdiag/tests/test_builder_group.py          | 210 +++++
 src/blockdiag/tests/test_builder_node.py           | 134 +++
 src/blockdiag/tests/test_builder_separate.py       |  48 +
 src/blockdiag/tests/test_generate_diagram.py       | 107 +++
 src/blockdiag/tests/test_parser.py                 | 140 +++
 src/blockdiag/tests/test_pep8.py                   |  38 +
 src/blockdiag/tests/test_rst_directives.py         | 365 ++++++++
 src/blockdiag/tests/test_utils_fontmap.py          | 340 +++++++
 src/blockdiag/tests/utils.py                       |  92 ++
 src/blockdiag/utils/PDFTextFolder.py               |  29 +
 src/blockdiag/utils/PILTextFolder.py               |  45 +
 src/blockdiag/utils/TextFolder.py                  | 303 +++++++
 src/blockdiag/utils/__init__.py                    | 107 +++
 src/blockdiag/utils/bootstrap.py                   | 208 +++++
 src/blockdiag/utils/collections.py                 |  39 +
 src/blockdiag/utils/config.py                      |  40 +
 src/blockdiag/utils/ellipse.py                     |  57 ++
 src/blockdiag/utils/fontmap.py                     | 160 ++++
 src/blockdiag/utils/images.py                      |  94 ++
 src/blockdiag/utils/jpeg.py                        |  89 ++
 src/blockdiag/utils/myitertools.py                 |  46 +
 src/blockdiag/utils/namedtuple.py                  |  17 +
 src/blockdiag/utils/rst/__init__.py                |  14 +
 src/blockdiag/utils/rst/directives.py              | 265 ++++++
 src/blockdiag/utils/urlutil.py                     |  12 +
 src/blockdiag/utils/uuid.py                        |  23 +
 src/blockdiag_sphinxhelper.py                      |  21 +
 235 files changed, 13094 insertions(+)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..3e2ed84
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,8 @@
+include buildout.cfg
+include bootstrap.py
+include MANIFEST.in
+include LICENSE
+include blockdiag.1
+recursive-include examples *.diag *.png *.svg
+recursive-include src *.py *.txt *.diag
+
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..89eb60a
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,449 @@
+Metadata-Version: 1.0
+Name: blockdiag
+Version: 1.1.2
+Summary: blockdiag generate block-diagram image file from spec-text file.
+Home-page: http://blockdiag.com/
+Author: Takeshi Komiya
+Author-email: i.tkomiya at gmail.com
+License: Apache License 2.0
+Description: `blockdiag` generate block-diagram image file from spec-text file.
+        
+        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 
+        `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+        
+        Setup
+        =====
+        
+        by easy_install
+        ----------------
+        Make environment::
+        
+           $ easy_install blockdiag
+        
+        If you want to export as PDF format, give pdf arguments::
+        
+           $ easy_install "blockdiag[pdf]"
+        
+        by buildout
+        ------------
+        Make environment::
+        
+           $ hg clone http://bitbucket.org/tk0miya/blockdiag
+           $ cd blockdiag
+           $ python bootstrap.py
+           $ bin/buildout
+        
+        Copy and modify ini file. example::
+        
+           $ cp <blockdiag installed path>/blockdiag/examples/simple.diag .
+           $ vi simple.diag
+        
+        Please refer to `spec-text setting sample`_ section for the format of the
+        `simpla.diag` configuration file.
+        
+        spec-text setting sample
+        ========================
+        Few examples are available.
+        You can get more examples at
+        `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+        
+        simple.diag
+        ------------
+        simple.diag is simply define nodes and transitions by dot-like text format::
+        
+            diagram admin {
+              top_page -> config -> config_edit -> config_confirm -> top_page;
+            }
+        
+        screen.diag
+        ------------
+        screen.diag is more complexly sample. diaglam nodes have a alternative label
+        and some transitions::
+        
+            diagram admin {
+              top_page [label = "Top page"];
+        
+              foo_index [label = "List of FOOs"];
+              foo_detail [label = "Detail FOO"];
+              foo_add [label = "Add FOO"];
+              foo_add_confirm [label = "Add FOO (confirm)"];
+              foo_edit [label = "Edit FOO"];
+              foo_edit_confirm [label = "Edit FOO (confirm)"];
+              foo_delete_confirm [label = "Delete FOO (confirm)"];
+        
+              bar_detail [label = "Detail of BAR"];
+              bar_edit [label = "Edit BAR"];
+              bar_edit_confirm [label = "Edit BAR (confirm)"];
+        
+              logout;
+        
+              top_page -> foo_index;
+              top_page -> bar_detail;
+        
+              foo_index -> foo_detail;
+                           foo_detail -> foo_edit;
+                           foo_detail -> foo_delete_confirm;
+              foo_index -> foo_add -> foo_add_confirm -> foo_index;
+              foo_index -> foo_edit -> foo_edit_confirm -> foo_index;
+              foo_index -> foo_delete_confirm -> foo_index;
+        
+              bar_detail -> bar_edit -> bar_edit_confirm -> bar_detail;
+            }
+        
+        
+        Usage
+        =====
+        Execute blockdiag command::
+        
+           $ blockdiag simple.diag
+           $ ls simple.png
+           simple.png
+        
+        
+        Requirements
+        ============
+        * Python 2.4 or later (not support 3.x)
+        * Python Imaging Library 1.1.5 or later.
+        * funcparserlib 0.3.4 or later.
+        * setuptools or distribute.
+        
+        
+        License
+        =======
+        Apache License 2.0
+        
+        
+        History
+        =======
+        
+        1.1.2 (2011-12-26)
+        ------------------
+        * Support font-index for TrueType Font Collections (.ttc file)
+        * Allow to use reST syntax in descriptions of nodes
+        * Fix bugs
+        
+        1.1.1 (2011-11-27)
+        ------------------
+        * Add node attribute: href (thanks to @r_rudi!)
+        * Fix bugs
+        
+        1.1.0 (2011-11-19)
+        ------------------
+        * Add shape: square and circle
+        * Add fontfamily attribute for switching fontface
+        * Fix bugs
+        
+        1.0.3 (2011-11-13)
+        ------------------
+        * Add plugin: attributes
+        * Change plugin syntax; (cf. plugin attributes [attr = value, attr, value])
+        * Fix bugs
+        
+        1.0.2 (2011-11-07)
+        ------------------
+        * Fix bugs
+        
+        1.0.1 (2011-11-06)
+        ------------------
+        * Add group attribute: shape
+        * Fix bugs
+        
+        1.0.0 (2011-11-04)
+        ------------------
+        * Add node attribute: linecolor
+        * Rename diagram attributes:
+           * fontsize -> default_fontsize
+           * default_line_color -> default_linecolor
+           * default_text_color -> default_textcolor
+        * Add docutils extention
+        * Fix bugs
+        
+        0.9.7 (2011-11-01)
+        ------------------
+        * Add node attribute: fontsize
+        * Add edge attributes: thick, fontsize
+        * Add group attribute: fontsize
+        * Change color of shadow in PDF mode
+        * Add class feature (experimental)
+        * Add handler-plugin framework (experimental)
+        
+        0.9.6 (2011-10-22)
+        ------------------
+        * node.style supports dashed_array format style
+        * Fix bugs
+        
+        0.9.5 (2011-10-19)
+        ------------------
+        * Add node attributes: width and height
+        * Fix bugs
+        
+        0.9.4 (2011-10-07)
+        ------------------
+        * Fix bugs
+        
+        0.9.3 (2011-10-06)
+        ------------------
+        * Replace SVG core by original's (simplesvg.py)
+        * Refactored
+        * Fix bugs
+        
+        0.9.2 (2011-09-30)
+        ------------------
+        * Add node attribute: textcolor
+        * Add group attribute: textcolor
+        * Add edge attribute: textcolor
+        * Add diagram attributes: default_text_attribute
+        * Fix beginpoint shape and endpoint shape were reversed
+        * Fix bugs
+        
+        0.9.1 (2011-09-26)
+        ------------------
+        * Add diagram attributes: default_node_color, default_group_color and default_line_color
+        * Fix bugs
+        
+        0.9.0 (2011-09-25)
+        ------------------
+        * Add icon attribute to node
+        * Make transparency to background of PNG images 
+        * Fix bugs
+        
+        0.8.9 (2011-08-09)
+        ------------------
+        * Fix bugs
+        
+        0.8.8 (2011-08-08)
+        ------------------
+        * Fix bugs
+        
+        0.8.7 (2011-08-06)
+        ------------------
+        * Fix bugs
+        
+        0.8.6 (2011-08-01)
+        ------------------
+        * Support Pillow as replacement of PIL (experimental)
+        * Fix bugs
+        
+        0.8.5 (2011-07-31)
+        ------------------
+        * Allow dot characters in node_id
+        * Fix bugs
+        
+        0.8.4 (2011-07-05)
+        ------------------
+        * Fix bugs
+        
+        0.8.3 (2011-07-03)
+        ------------------
+        * Support input from stdin
+        * Fix bugs
+        
+        0.8.2 (2011-06-29)
+        ------------------
+        * Add node.stacked
+        * Add node shapes: dots, none
+        * Add hiragino-font to font search list
+        * Support background image fetching from web
+        * Add diagram.edge_layout (experimental)
+        * Fix bugs
+        
+        0.8.1 (2011-05-14)
+        ------------------
+        * Change license to Apache License 2.0
+        * Fix bugs
+        
+        0.8.0 (2011-05-04)
+        ------------------
+        * Add --separate option and --version option
+        * Fix bugs
+        
+        0.7.8 (2011-04-19)
+        ------------------
+        * Update layout engine
+        * Update requirements: PIL >= 1.1.5
+        * Update parser for tokenize performance
+        * Add --nodoctype option
+        * Fix bugs
+        * Add many testcases
+        
+        0.7.7 (2011-03-29)
+        ------------------
+        * Fix bugs
+        
+        0.7.6 (2011-03-26)
+        ------------------
+        * Add new layout manager for portrait edges
+        * Fix bugs
+        
+        0.7.5 (2011-03-20)
+        ------------------
+        * Support multiple nodes relations (cf. A -> B, C)
+        * Support node group declaration at attribute of nodes
+        * Fix bugs
+        
+        0.7.4 (2011-03-08)
+        ------------------
+        * Fix bugs
+        
+        0.7.3 (2011-03-02)
+        ------------------
+        * Use UTF-8 characters as Name token (by @swtw7466)
+        * Fix htmlentities included in labels was not escaped on SVG images
+        * Fix bugs
+        
+        0.7.2 (2011-02-28)
+        ------------------
+        * Add default_shape attribute to diagram
+        
+        0.7.1 (2011-02-27)
+        ------------------
+        * Fix edge has broken with antialias option
+        
+        0.7.0 (2011-02-25)
+        ------------------
+        * Support node shape
+        
+        0.6.7 (2011-02-12)
+        ------------------
+        * Change noderenderer interface to new style
+        * Render dashed ellipse more clearly (contributed by @cocoatomo)
+        * Support PDF exporting
+        
+        0.6.6 (2011-01-31)
+        ------------------
+        * Support diagram.shape_namespace
+        * Add new node shapes; mail, cloud, beginpoint, endpoint, minidiamond, actor
+        * Support plug-in structure to install node shapes
+        * Fix bugs
+        
+        0.6.5 (2011-01-18)
+        ------------------
+        * Support node shape (experimental)
+        
+        0.6.4 (2011-01-17)
+        ------------------
+        * Fix bugs
+        
+        0.6.3 (2011-01-15)
+        ------------------
+        * Fix bugs
+        
+        0.6.2 (2011-01-08)
+        ------------------
+        * Fix bugs
+        
+        0.6.1 (2011-01-07)
+        ------------------
+        * Implement 'folded' attribute for edge
+        * Refactor layout engine
+        
+        0.6 (2011-01-02)
+        ------------------
+        * Support nested groups.
+        
+        0.5.5 (2010-12-24)
+        ------------------
+        * Specify direction of edges as syntax (->, --, <-, <->)
+        * Fix bugs.
+        
+        0.5.4 (2010-12-23)
+        ------------------
+        * Remove debug codes.
+        
+        0.5.3 (2010-12-23)
+        ------------------
+        * Support NodeGroup.label.
+        * Implement --separate option (experimental)
+        * Fix right-up edge overrapped on other nodes.
+        * Support configration file: .blockdiagrc
+        
+        0.5.2 (2010-11-06)
+        ------------------
+        * Fix unicode errors for UTF-8'ed SVG exportion.
+        * Refactoring codes for running on GAE.
+        
+        0.5.1 (2010-10-26)
+        ------------------
+        * Fix license text on diagparser.py
+        * Update layout engine.
+        
+        0.5 (2010-10-15)
+        ------------------
+        * Support background-image of node (SVG)
+        * Support labels for edge.
+        * Fix bugs.
+        
+        0.4.2 (2010-10-10)
+        ------------------
+        * Support background-color of node groups.
+        * Draw edge has jumped at edge's cross-points.
+        * Fix bugs.
+        
+        0.4.1 (2010-10-07)
+        ------------------
+        * Fix bugs.
+        
+        0.4 (2010-10-07)
+        ------------------
+        * Support SVG exporting.
+        * Support dashed edge drawing.
+        * Support background image of nodes (PNG only)
+        
+        0.3.1 (2010-09-29)
+        ------------------
+        * Fasten anti-alias process.
+        * Fix text was broken on windows.
+        
+        0.3 (2010-09-26)
+        ------------------
+        * Add --antialias option.
+        * Fix bugs.
+        
+        0.2.2 (2010-09-25)
+        ------------------
+        * Fix edge bugs.
+        
+        0.2.1 (2010-09-25)
+        ------------------
+        * Fix bugs.
+        * Fix package style.
+        
+        0.2 (2010-09-23)
+        ------------------
+        * Update layout engine.
+        * Support group { ... } sentence for create Node-Groups.
+        * Support numbered badge on node (cf. A [numbered = 5])
+        
+        0.1 (2010-09-20)
+        -----------------
+        * first release
+        
+        Todos
+        ======
+        
+        Functionals
+        ------------
+        * Reimplement --separate option
+        * Support diagram legends
+        * Support other block diagram structure
+        
+        Known Issues
+        -------------
+        * Fix some experimental features.
+        * PDF renderer does not support blur shadow
+        * PDF renderer does not support path rendering
+        
+Keywords: diagram,generator
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Documentation
+Classifier: Topic :: Text Processing :: Markup
diff --git a/blockdiag.1 b/blockdiag.1
new file mode 100644
index 0000000..3f0b711
--- /dev/null
+++ b/blockdiag.1
@@ -0,0 +1,72 @@
+.\"                                      Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH BLOCKDIAG 1 "May  9, 2011"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh        disable hyphenation
+.\" .hy        enable hyphenation
+.\" .ad l      left justify
+.\" .ad b      justify to both left and right margins
+.\" .nf        disable filling
+.\" .fi        enable filling
+.\" .br        insert line break
+.\" .sp <n>    insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+blockdiag \- generate block-diagram image file from spec-text file.
+.SH SYNOPSIS
+.B blockdiag
+.RI [ options ] " file"
+.SH DESCRIPTION
+This manual page documents briefly the
+.B blockdiag
+commands.
+.PP
+.\" TeX users may be more comfortable with the \fB<whatever>\fP and
+.\" \fI<whatever>\fP escape sequences to invode bold face and italics,
+.\" respectively.
+\fBblockdiag\fP is a program that generate block-diagram image file from spec-text file.
+.SH OPTIONS
+These programs follow the usual GNU command line syntax, with long
+options starting with two dashes (`-').
+A summary of options is included below.
+For a complete description, see the \fBSEE ALSO\fP.
+.TP
+.B \-\-version
+show program's version number and exit
+.TP
+.B \-h, \-\-help
+Show summary of options
+.TP
+.B \-a, \-\-antialias
+Pass diagram image to anti-alias filter
+.TP
+.B \-c FILE, \-\-config=FILE
+read configurations from FILE
+.TP
+.B \-o FILE
+write diagram to FILE
+.TP
+.B \-f FONT, \-\-font=FONT
+use FONT to draw diagram
+.TP
+.B \-s, \-\-separate
+Separate diagram images for each group
+.TP
+.B \-T TYPE
+Output diagram as TYPE format
+.TP
+.B \-\-nodoctype
+Do not output doctype definition tags (SVG only)
+.SH SEE ALSO
+The programs are documented fully by
+.br
+.BR http://tk0miya.bitbucket.org/blockdiag/build/html/index.html
+.SH AUTHOR
+blockdiag was written by Takeshi Komiya <i.tkomiya at gmail.com>
+.PP
+This manual page was written by Kouhei Maeda <mkouhei at palmtb.net>,
+for the Debian project (and may be used by others).
diff --git a/bootstrap.py b/bootstrap.py
new file mode 100644
index 0000000..49c5f50
--- /dev/null
+++ b/bootstrap.py
@@ -0,0 +1,200 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, textwrap, urllib, urllib2
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    quote = str
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded.  This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient.  However, we'll start with that:
+if 'site' in sys.modules:
+    # We will restart with python -S.
+    args = sys.argv[:]
+    args[0:0] = [sys.executable, '-S']
+    args = map(quote, args)
+    os.execv(sys.executable, args)
+# Now we are running with -S.  We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in sys.modules.items():
+    if (hasattr(v, '__path__') and
+        len(v.__path__)==1 and
+        not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+        # This is a namespace package.  Remove it.
+        sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+    if value:
+        if '://' not in value: # It doesn't smell like a URL.
+            value = 'file://%s' % (
+                urllib.pathname2url(
+                    os.path.abspath(os.path.expanduser(value))),)
+        if opt_str == '--download-base' and not value.endswith('/'):
+            # Download base needs a trailing slash to make the world happy.
+            value += '/'
+    else:
+        value = None
+    name = opt_str[2:].replace('-', '_')
+    setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="use_distribute", default=False,
+                   help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or file location for the setup file. "
+                        "If you use Setuptools, this will default to " +
+                        setuptools_source + "; if you use Distribute, this "
+                        "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or directory for downloading "
+                        "zc.buildout and either Setuptools or Distribute. "
+                        "Defaults to PyPI."))
+parser.add_option("--eggs",
+                  help=("Specify a directory for storing eggs.  Defaults to "
+                        "a temporary directory that is deleted when the "
+                        "bootstrap script completes."))
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.eggs:
+    eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+    eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+    if options.use_distribute:
+        options.setup_source = distribute_source
+    else:
+        options.setup_source = setuptools_source
+
+args = args + ['bootstrap']
+
+
+try:
+    import pkg_resources
+    import setuptools # A flag.  Sometimes pkg_resources is installed alone.
+    if not hasattr(pkg_resources, '_distribute'):
+        raise ImportError
+except ImportError:
+    ez_code = urllib2.urlopen(
+        options.setup_source).read().replace('\r\n', '\n')
+    ez = {}
+    exec ez_code in ez
+    setup_args = dict(to_dir=eggs_dir, download_delay=0)
+    if options.download_base:
+        setup_args['download_base'] = options.download_base
+    if options.use_distribute:
+        setup_args['no_fake'] = True
+    ez['use_setuptools'](**setup_args)
+    reload(sys.modules['pkg_resources'])
+    import pkg_resources
+    # This does not (always?) update the default working set.  We will
+    # do it.
+    for path in sys.path:
+        if path not in pkg_resources.working_set.entries:
+            pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+       '-c',
+       quote('from setuptools.command.easy_install import main; main()'),
+       '-mqNxd',
+       quote(eggs_dir)]
+
+if options.download_base:
+    cmd.extend(['-f', quote(options.download_base)])
+
+requirement = 'zc.buildout'
+if options.version:
+    requirement = '=='.join((requirement, options.version))
+cmd.append(requirement)
+
+if options.use_distribute:
+    setup_requirement = 'distribute'
+else:
+    setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+env = dict(
+    os.environ,
+    PYTHONPATH=ws.find(
+        pkg_resources.Requirement.parse(setup_requirement)).location)
+
+if is_jython:
+    import subprocess
+    exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+    sys.stdout.flush()
+    sys.stderr.flush()
+    print ("An error occurred when trying to install zc.buildout. "
+           "Look above this message for any errors that "
+           "were output by easy_install.")
+    sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+    shutil.rmtree(eggs_dir)
diff --git a/buildout.cfg b/buildout.cfg
new file mode 100644
index 0000000..f7a8df2
--- /dev/null
+++ b/buildout.cfg
@@ -0,0 +1,36 @@
+[buildout]
+parts = blockdiag test coverage
+
+develop = .
+
+[blockdiag]
+recipe = zc.recipe.egg
+eggs = blockdiag[rst]
+interpreter = py
+
+[test]
+recipe = pbp.recipe.noserunner
+eggs =
+    blockdiag[rst]
+    blockdiag[test]
+    coverage
+    unittest-xml-reporting
+
+[coverage]
+recipe = zc.recipe.egg
+eggs = coverage
+
+[test-extra]
+recipe = iw.recipe.cmd:py
+on_install = true
+cmds =
+   >>> url = "http://sourceforge.jp/frs/redir.php?m=jaist&f=%2Fvlgothic%2F46966%2FVLGothic-20100416.zip"
+   >>> buildout_dir = buildout.get('directory', '.')
+   >>> path = os.path.join(buildout_dir, 'src/blockdiag/tests/truetype')
+   >>> if not os.path.exists(path):
+   ...     os.makedirs(path)
+   ...     import cStringIO, urllib2, zipfile
+   ...     archive = urllib2.urlopen(url).read()
+   ...     zip = zipfile.ZipFile(cStringIO.StringIO(archive))
+   ...     ttf = zip.read('VLGothic/VL-PGothic-Regular.ttf')
+   ...     open(os.path.join(path, 'VL-PGothic-Regular.ttf'), 'wb').write(ttf)
diff --git a/examples/group.diag b/examples/group.diag
new file mode 100644
index 0000000..af9cc5b
--- /dev/null
+++ b/examples/group.diag
@@ -0,0 +1,13 @@
+diagram {
+  group {
+    A -> B -> C -> D;
+  }
+              C -> E;
+
+  A -> F -> G -> H;
+
+  group {
+    G;
+    H;
+  }
+}
diff --git a/examples/group.png b/examples/group.png
new file mode 100644
index 0000000..9c631c7
Binary files /dev/null and b/examples/group.png differ
diff --git a/examples/group.svg b/examples/group.svg
new file mode 100644
index 0000000..2959292
--- /dev/null
+++ b/examples/group.svg
@@ -0,0 +1,52 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ">
+<svg viewBox="0 0 1408 216 " xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"  xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" >
+	<defs id="defs_block">
+		<filter inkspace:collect="always" height="1.504" width="1.1575" y="-0.252" x="-0.07875" id="filter_blur">
+			<feGaussianBlur stdDeviation="4.2" id="feGaussianBlur3780" inkspace:collect="always"/>
+		</filter>
+	</defs>
+	<title>blockdiag</title>
+	<rect style="filter:url(#filter_blur)" height="60" width="720" y="38" x="56" stroke-width="1" fill="rgb(243,152,0)"/>
+	<rect style="filter:url(#filter_blur)" height="60" width="336" y="38" x="1016" stroke-width="1" fill="rgb(243,152,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="835" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="1027" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="1219" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="835" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="64" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="124" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">A</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="317" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">B</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="508" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">C</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="700" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">D</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="832" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="893" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">F</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="1024" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="1084" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">G</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="1216" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="1276" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">H</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="832" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="893" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">E</text>
+	<path stroke="rgb(0,0,0)" d="M 960 68 L 1024 68" fill="none"/>
+	<polygon style="None" points="1024, 68  1016, 64  1016, 72  1024, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 224 68 L 224 108 L 604 108" fill="none"/>
+	<path stroke="rgb(0,0,0)" d="M 612 108 L 816 108 L 816 68 L 832 68" fill="none"/>
+	<polygon style="None" points="832, 68  824, 64  824, 72  832, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 604.0 108.0 A4,4 0 0 1 612.0 108.0" fill="none"/>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 256 68" fill="none"/>
+	<polygon style="None" points="256, 68  248, 64  248, 72  256, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 448 68" fill="none"/>
+	<polygon style="None" points="448, 68  440, 64  440, 72  448, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 608 68 L 608 148 L 832 148" fill="none"/>
+	<polygon style="None" points="832, 148  824, 144  824, 152  832, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 640 68" fill="none"/>
+	<polygon style="None" points="640, 68  632, 64  632, 72  640, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 1152 68 L 1216 68" fill="none"/>
+	<polygon style="None" points="1216, 68  1208, 64  1208, 72  1216, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+</svg>
diff --git a/examples/multibyte.diag b/examples/multibyte.diag
new file mode 100644
index 0000000..8f37462
--- /dev/null
+++ b/examples/multibyte.diag
@@ -0,0 +1,11 @@
+diagram {
+  A [label = "朝食"];
+  B [label = "昼食"];
+  C [label = "おやつ"];
+  D [label = "夕飯"];
+  E [label = "夜食"];
+
+  "起" -> "承" -> "転" -> "結";
+  A -> B      -> D -> E;
+       B -> C -> D;
+}
diff --git a/examples/multibyte.png b/examples/multibyte.png
new file mode 100644
index 0000000..78b145f
Binary files /dev/null and b/examples/multibyte.png differ
diff --git a/examples/multibyte.svg b/examples/multibyte.svg
new file mode 100644
index 0000000..62c3d59
--- /dev/null
+++ b/examples/multibyte.svg
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ">
+<svg viewBox="0 0 1024 216 " xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"  xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" >
+	<defs id="defs_block">
+		<filter inkspace:collect="always" height="1.504" width="1.1575" y="-0.252" x="-0.07875" id="filter_blur">
+			<feGaussianBlur stdDeviation="4.2" id="feGaussianBlur3780" inkspace:collect="always"/>
+		</filter>
+	</defs>
+	<title>blockdiag</title>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="835" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="64" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="117" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">朝食</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="309" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">昼食</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="497" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">おやつ</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="693" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">夕飯</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="832" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="885" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">夜食</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="64" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="123" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">起</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="315" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">承</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="507" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">転</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="699" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">結</text>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 256 68" fill="none"/>
+	<polygon style="None" points="256, 68  248, 64  248, 72  256, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 448 68" fill="none"/>
+	<polygon style="None" points="448, 68  440, 64  440, 72  448, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 416 68 L 416 108 L 624 108 L 624 68 L 640 68" fill="none"/>
+	<polygon style="None" points="640, 68  632, 64  632, 72  640, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 640 68" fill="none"/>
+	<polygon style="None" points="640, 68  632, 64  632, 72  640, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 768 68 L 832 68" fill="none"/>
+	<polygon style="None" points="832, 68  824, 64  824, 72  832, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 192 148 L 256 148" fill="none"/>
+	<polygon style="None" points="256, 148  248, 144  248, 152  256, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 148 L 448 148" fill="none"/>
+	<polygon style="None" points="448, 148  440, 144  440, 152  448, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 148 L 640 148" fill="none"/>
+	<polygon style="None" points="640, 148  632, 144  632, 152  640, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+</svg>
diff --git a/examples/numbered.diag b/examples/numbered.diag
new file mode 100644
index 0000000..60652eb
--- /dev/null
+++ b/examples/numbered.diag
@@ -0,0 +1,9 @@
+diagram {
+  A [numbered = 1];
+  B [numbered = 2];
+  C [numbered = 3];
+  D [numbered = 4];
+
+  A -> B -> C;
+  A -> D;
+}
diff --git a/examples/numbered.png b/examples/numbered.png
new file mode 100644
index 0000000..53baf88
Binary files /dev/null and b/examples/numbered.png differ
diff --git a/examples/numbered.svg b/examples/numbered.svg
new file mode 100644
index 0000000..f37b032
--- /dev/null
+++ b/examples/numbered.svg
@@ -0,0 +1,36 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ">
+<svg viewBox="0 0 640 216 " xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"  xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" >
+	<defs id="defs_block">
+		<filter inkspace:collect="always" height="1.504" width="1.1575" y="-0.252" x="-0.07875" id="filter_blur">
+			<feGaussianBlur stdDeviation="4.2" id="feGaussianBlur3780" inkspace:collect="always"/>
+		</filter>
+	</defs>
+	<title>blockdiag</title>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="64" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="124" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">A</text>
+	<ellipse style="None" rx="12" ry="12" stroke="rgb(0,0,0)" cy="48" cx="64" fill="pink"/>
+	<text y="54" x="61" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">1</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="317" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">B</text>
+	<ellipse style="None" rx="12" ry="12" stroke="rgb(0,0,0)" cy="48" cx="256" fill="pink"/>
+	<text y="54" x="253" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">2</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="316" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">D</text>
+	<ellipse style="None" rx="12" ry="12" stroke="rgb(0,0,0)" cy="128" cx="256" fill="pink"/>
+	<text y="134" x="253" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">4</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="508" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">C</text>
+	<ellipse style="None" rx="12" ry="12" stroke="rgb(0,0,0)" cy="48" cx="448" fill="pink"/>
+	<text y="54" x="445" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">3</text>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 224 68 L 224 148 L 256 148" fill="none"/>
+	<polygon style="None" points="256, 148  248, 144  248, 152  256, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 256 68" fill="none"/>
+	<polygon style="None" points="256, 68  248, 64  248, 72  256, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 448 68" fill="none"/>
+	<polygon style="None" points="448, 68  440, 64  440, 72  448, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+</svg>
diff --git a/examples/screen.diag b/examples/screen.diag
new file mode 100644
index 0000000..b9b28a6
--- /dev/null
+++ b/examples/screen.diag
@@ -0,0 +1,38 @@
+diagram admin {
+  top_page [label = "Top page", color = "pink"];
+
+  group foo {
+    foo_index [label = "List of FOOs"];
+    foo_detail [label = "Detail FOO", style = "dashed"];
+    foo_add [label = "Add FOO"];
+    foo_add_confirm [label = "Add FOO (confirm)"];
+    foo_edit [label = "Edit FOO"];
+    foo_edit_confirm [label = "Edit FOO (confirm)"];
+    foo_delete_confirm [label = "Delete FOO (confirm)"];
+  }
+
+  group bar {
+    bar_detail [label = "Detail of BAR", style = "dotted"];
+    bar_edit [label = "Edit BAR"];
+    bar_edit_confirm [label = "Edit BAR (confirm)"];
+  }
+
+  logout;
+
+  top_page -> foo_index [color = "red", dir = none, label = "button"];
+  top_page -> bar_detail [style = dashed, label = "link"];
+
+  foo_index -> foo_detail;
+               foo_detail -> foo_edit;
+               foo_detail -> foo_delete_confirm;
+  foo_index -> foo_add -> foo_add_confirm;
+                          foo_add_confirm -> foo_index [label = "added"];
+  foo_index -> foo_edit -> foo_edit_confirm;
+                           foo_edit_confirm -> foo_index [label = "changed"];
+  foo_index -> foo_delete_confirm;
+               foo_delete_confirm -> foo_index [label = "deleted"];
+
+  bar_detail -> bar_edit -> bar_edit_confirm -> bar_detail [dir = both, style = dotted];
+
+  foo_index -> foo_edit [style = dotted];
+}
diff --git a/examples/screen.png b/examples/screen.png
new file mode 100644
index 0000000..1b4d7a0
Binary files /dev/null and b/examples/screen.png differ
diff --git a/examples/screen.svg b/examples/screen.svg
new file mode 100644
index 0000000..4e12b47
--- /dev/null
+++ b/examples/screen.svg
@@ -0,0 +1,96 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ">
+<svg viewBox="0 0 1024 456 " xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"  xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" >
+	<defs id="defs_block">
+		<filter inkspace:collect="always" height="1.504" width="1.1575" y="-0.252" x="-0.07875" id="filter_blur">
+			<feGaussianBlur stdDeviation="4.2" id="feGaussianBlur3780" inkspace:collect="always"/>
+		</filter>
+	</defs>
+	<title>blockdiag</title>
+	<rect style="filter:url(#filter_blur)" height="220" width="720" y="38" x="248" stroke-width="1" fill="rgb(243,152,0)"/>
+	<rect style="filter:url(#filter_blur)" height="60" width="528" y="278" x="248" stroke-width="1" fill="rgb(243,152,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="214" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="214" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="835" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="294" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="294" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="294" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="374" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="64" stroke-width="1" fill="pink"/>
+	<text y="74" x="103" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Top page</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="289" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">List of FOOs</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" stroke-dasharray="4" y="48" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="484" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Detail FOO</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="208" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="234" x="488" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Add FOO</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="680" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Edit FOO</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="649" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Delete FOO (confirm)</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="208" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="234" x="654" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Add FOO (confirm)</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="832" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="847" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Edit FOO (confirm)</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" stroke-dasharray="2" y="288" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="314" x="287" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Detail of BAR</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="288" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="314" x="490" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Edit BAR</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="288" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="314" x="656" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">Edit BAR (confirm)</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="368" x="64" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="394" x="111" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">logout</text>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="4" d="M 192 68 L 224 68 L 224 308 L 256 308" fill="none"/>
+	<polygon style="None" points="256, 308  248, 304  248, 312  256, 308  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="red" d="M 192 68 L 256 68" fill="none"/>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="2" d="M 384 68 L 416 68 L 416 108 L 604 108" fill="none"/>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="2" d="M 612 108 L 624 108 L 624 68 L 640 68" fill="none"/>
+	<polygon style="None" points="640, 68  632, 64  632, 72  640, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="2" d="M 604.0 108.0 A4,4 0 0 1 612.0 108.0" fill="none"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 416 68 L 416 228 L 448 228" fill="none"/>
+	<polygon style="None" points="448, 228  440, 224  440, 232  448, 228  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 448 68" fill="none"/>
+	<polygon style="None" points="448, 68  440, 64  440, 72  448, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 416 68 L 416 148 L 640 148" fill="none"/>
+	<polygon style="None" points="640, 148  632, 144  632, 152  640, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 608 68 L 608 148 L 640 148" fill="none"/>
+	<polygon style="None" points="640, 148  632, 144  632, 152  640, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 640 68" fill="none"/>
+	<polygon style="None" points="640, 68  632, 64  632, 72  640, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 228 L 640 228" fill="none"/>
+	<polygon style="None" points="640, 228  632, 224  632, 232  640, 228  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 768 228 L 776 228 L 776 33 L 320 33 L 320 48" fill="none"/>
+	<polygon style="None" points="320, 48  316, 40  324, 40  320, 48  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 768 68 L 772 68" fill="none"/>
+	<path stroke="rgb(0,0,0)" d="M 780 68 L 832 68" fill="none"/>
+	<polygon style="None" points="832, 68  824, 64  824, 72  832, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 772.0 68.0 A4,4 0 0 1 780.0 68.0" fill="none"/>
+	<path stroke="rgb(0,0,0)" d="M 960 68 L 968 68 L 968 33 L 320 33 L 320 48" fill="none"/>
+	<polygon style="None" points="320, 48  316, 40  324, 40  320, 48  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 768 148 L 776 148 L 776 33 L 320 33 L 320 48" fill="none"/>
+	<polygon style="None" points="320, 48  316, 40  324, 40  320, 48  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="2" d="M 384 308 L 448 308" fill="none"/>
+	<polygon style="None" points="384, 308  392, 304  392, 312  384, 308  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<polygon style="None" points="448, 308  440, 304  440, 312  448, 308  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="2" d="M 576 308 L 640 308" fill="none"/>
+	<polygon style="None" points="576, 308  584, 304  584, 312  576, 308  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<polygon style="None" points="640, 308  632, 304  632, 312  640, 308  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" stroke-dasharray="2" d="M 768 308 L 776 308 L 776 273 L 320 273 L 320 288" fill="none"/>
+	<polygon style="None" points="768, 308  776, 304  776, 312  768, 308  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<polygon style="None" points="320, 288  316, 280  324, 280  320, 288  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<rect style="None" height="15" width="23" stroke="black" y="286" x="213" stroke-width="1" fill="white"/>
+	<text y="299" x="215" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">link</text>
+	<rect style="None" height="15" width="40" stroke="black" y="46" x="204" stroke-width="1" fill="white"/>
+	<text y="59" x="206" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">button</text>
+	<rect style="None" height="15" width="37" stroke="black" y="191" x="758" stroke-width="1" fill="white"/>
+	<text y="204" x="760" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">added</text>
+	<rect style="None" height="15" width="49" stroke="black" y="11" x="920" stroke-width="1" fill="white"/>
+	<text y="24" x="922" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">changed</text>
+	<rect style="None" height="15" width="43" stroke="black" y="111" x="755" stroke-width="1" fill="white"/>
+	<text y="124" x="757" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">deleted</text>
+</svg>
diff --git a/examples/simple.diag b/examples/simple.diag
new file mode 100644
index 0000000..64eb58b
--- /dev/null
+++ b/examples/simple.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C -> D;
+            C -> E;
+  A -> F -> G -> H;
+}
diff --git a/examples/simple.png b/examples/simple.png
new file mode 100644
index 0000000..bfad6e5
Binary files /dev/null and b/examples/simple.png differ
diff --git a/examples/simple.svg b/examples/simple.svg
new file mode 100644
index 0000000..e88238a
--- /dev/null
+++ b/examples/simple.svg
@@ -0,0 +1,48 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ">
+<svg viewBox="0 0 832 296 " xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink"  xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" >
+	<defs id="defs_block">
+		<filter inkspace:collect="always" height="1.504" width="1.1575" y="-0.252" x="-0.07875" id="filter_blur">
+			<feGaussianBlur stdDeviation="4.2" id="feGaussianBlur3780" inkspace:collect="always"/>
+		</filter>
+	</defs>
+	<title>blockdiag</title>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="67" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="214" x="259" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="54" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="134" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="214" x="451" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" height="40" width="128" stroke="rgb(0,0,0)" y="214" x="643" stroke-width="1" fill="rgb(0,0,0)"/>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="64" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="124" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">A</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="317" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">B</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="208" x="256" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="234" x="317" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">F</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="508" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">C</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="48" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="74" x="700" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">D</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="128" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="154" x="701" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">E</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="208" x="448" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="234" x="508" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">G</text>
+	<rect style="None" height="40" width="128" stroke="rgb(0,0,0)" y="208" x="640" stroke-width="1" fill="rgb(255,255,255)"/>
+	<text y="234" x="700" font-size="11" font-family="/usr/share/fonts/truetype/ipafont/ipagp.ttf" fill="rgb(0,0,0)">H</text>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 224 68 L 224 228 L 256 228" fill="none"/>
+	<polygon style="None" points="256, 228  248, 224  248, 232  256, 228  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 192 68 L 256 68" fill="none"/>
+	<polygon style="None" points="256, 68  248, 64  248, 72  256, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 68 L 448 68" fill="none"/>
+	<polygon style="None" points="448, 68  440, 64  440, 72  448, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 608 68 L 608 148 L 640 148" fill="none"/>
+	<polygon style="None" points="640, 148  632, 144  632, 152  640, 148  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 68 L 640 68" fill="none"/>
+	<polygon style="None" points="640, 68  632, 64  632, 72  640, 68  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 384 228 L 448 228" fill="none"/>
+	<polygon style="None" points="448, 228  440, 224  440, 232  448, 228  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+	<path stroke="rgb(0,0,0)" d="M 576 228 L 640 228" fill="none"/>
+	<polygon style="None" points="640, 228  632, 224  632, 232  640, 228  " stroke="rgb(0,0,0)" fill="rgb(0,0,0)"/>
+</svg>
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..be3189c
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,11 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
+[build]
+build-base = _build
+
+[sdist]
+formats = gztar
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..655ffd5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+from setuptools import setup, find_packages
+import os, sys
+import pkg_resources
+
+sys.path.insert(0, 'src')
+import blockdiag
+
+
+def is_installed(name):
+    try:
+        pkg_resources.get_distribution(name)
+        return True
+    except:
+        return False
+
+
+long_description = \
+        open(os.path.join("src","README.txt")).read() + \
+        open(os.path.join("src","TODO.txt")).read()
+
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: System Administrators",
+    "License :: OSI Approved :: Apache Software License",
+    "Programming Language :: Python",
+    "Topic :: Software Development",
+    "Topic :: Software Development :: Documentation",
+    "Topic :: Text Processing :: Markup",
+]
+
+requires = ['setuptools',
+            'funcparserlib',
+            'webcolors']
+deplinks = []
+
+# Find imaging libraries
+if is_installed('PIL'):
+    requires.append('PIL')
+elif is_installed('Pillow'):
+    requires.append('Pillow')
+elif sys.platform == 'win32':
+    requires.append('Pillow')
+else:
+    requires.append('PIL')
+
+
+# only for Python2.6
+if sys.version_info > (2, 6) and sys.version_info < (2, 7):
+    requires.append('OrderedDict')
+
+
+setup(
+     name='blockdiag',
+     version=blockdiag.__version__,
+     description='blockdiag generate block-diagram image file from spec-text file.',
+     long_description=long_description,
+     classifiers=classifiers,
+     keywords=['diagram','generator'],
+     author='Takeshi Komiya',
+     author_email='i.tkomiya at gmail.com',
+     url='http://blockdiag.com/',
+     license='Apache License 2.0',
+     py_modules=['blockdiag_sphinxhelper'],
+     packages=find_packages('src'),
+     package_dir={'': 'src'},
+     package_data = {'': ['buildout.cfg']},
+     include_package_data=True,
+     install_requires=requires,
+     extras_require=dict(
+         test=[
+             'Nose',
+             'pep8',
+             'unittest2',
+         ],
+         pdf=[
+             'reportlab',
+         ],
+         rst=[
+             'docutils',
+         ],
+     ),
+     dependency_links=deplinks,
+     test_suite='nose.collector',
+     tests_require=['Nose','pep8'],
+     entry_points="""
+        [console_scripts]
+        blockdiag = blockdiag.command:main
+
+        [blockdiag_noderenderer]
+        box = blockdiag.noderenderer.box
+        square = blockdiag.noderenderer.square
+        roundedbox = blockdiag.noderenderer.roundedbox
+        diamond = blockdiag.noderenderer.diamond
+        minidiamond = blockdiag.noderenderer.minidiamond
+        mail = blockdiag.noderenderer.mail
+        note = blockdiag.noderenderer.note
+        cloud = blockdiag.noderenderer.cloud
+        circle = blockdiag.noderenderer.circle
+        ellipse = blockdiag.noderenderer.ellipse
+        beginpoint = blockdiag.noderenderer.beginpoint
+        endpoint = blockdiag.noderenderer.endpoint
+        actor = blockdiag.noderenderer.actor
+        flowchart.database = blockdiag.noderenderer.flowchart.database
+        flowchart.input = blockdiag.noderenderer.flowchart.input
+        flowchart.loopin = blockdiag.noderenderer.flowchart.loopin
+        flowchart.loopout = blockdiag.noderenderer.flowchart.loopout
+        flowchart.terminator = blockdiag.noderenderer.flowchart.terminator
+        textbox = blockdiag.noderenderer.textbox
+        dots = blockdiag.noderenderer.dots
+        none = blockdiag.noderenderer.none
+
+        [blockdiag_plugins]
+        attributes = blockdiag.plugins.attributes
+        autoclass = blockdiag.plugins.autoclass
+     """,
+)
+
diff --git a/src/README.txt b/src/README.txt
new file mode 100644
index 0000000..fbb9010
--- /dev/null
+++ b/src/README.txt
@@ -0,0 +1,417 @@
+`blockdiag` generate block-diagram image file from spec-text file.
+
+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 
+`blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+
+Setup
+=====
+
+by easy_install
+----------------
+Make environment::
+
+   $ easy_install blockdiag
+
+If you want to export as PDF format, give pdf arguments::
+
+   $ easy_install "blockdiag[pdf]"
+
+by buildout
+------------
+Make environment::
+
+   $ hg clone http://bitbucket.org/tk0miya/blockdiag
+   $ cd blockdiag
+   $ python bootstrap.py
+   $ bin/buildout
+
+Copy and modify ini file. example::
+
+   $ cp <blockdiag installed path>/blockdiag/examples/simple.diag .
+   $ vi simple.diag
+
+Please refer to `spec-text setting sample`_ section for the format of the
+`simpla.diag` configuration file.
+
+spec-text setting sample
+========================
+Few examples are available.
+You can get more examples at
+`blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+
+simple.diag
+------------
+simple.diag is simply define nodes and transitions by dot-like text format::
+
+    diagram admin {
+      top_page -> config -> config_edit -> config_confirm -> top_page;
+    }
+
+screen.diag
+------------
+screen.diag is more complexly sample. diaglam nodes have a alternative label
+and some transitions::
+
+    diagram admin {
+      top_page [label = "Top page"];
+
+      foo_index [label = "List of FOOs"];
+      foo_detail [label = "Detail FOO"];
+      foo_add [label = "Add FOO"];
+      foo_add_confirm [label = "Add FOO (confirm)"];
+      foo_edit [label = "Edit FOO"];
+      foo_edit_confirm [label = "Edit FOO (confirm)"];
+      foo_delete_confirm [label = "Delete FOO (confirm)"];
+
+      bar_detail [label = "Detail of BAR"];
+      bar_edit [label = "Edit BAR"];
+      bar_edit_confirm [label = "Edit BAR (confirm)"];
+
+      logout;
+
+      top_page -> foo_index;
+      top_page -> bar_detail;
+
+      foo_index -> foo_detail;
+                   foo_detail -> foo_edit;
+                   foo_detail -> foo_delete_confirm;
+      foo_index -> foo_add -> foo_add_confirm -> foo_index;
+      foo_index -> foo_edit -> foo_edit_confirm -> foo_index;
+      foo_index -> foo_delete_confirm -> foo_index;
+
+      bar_detail -> bar_edit -> bar_edit_confirm -> bar_detail;
+    }
+
+
+Usage
+=====
+Execute blockdiag command::
+
+   $ blockdiag simple.diag
+   $ ls simple.png
+   simple.png
+
+
+Requirements
+============
+* Python 2.4 or later (not support 3.x)
+* Python Imaging Library 1.1.5 or later.
+* funcparserlib 0.3.4 or later.
+* setuptools or distribute.
+
+
+License
+=======
+Apache License 2.0
+
+
+History
+=======
+
+1.1.2 (2011-12-26)
+------------------
+* Support font-index for TrueType Font Collections (.ttc file)
+* Allow to use reST syntax in descriptions of nodes
+* Fix bugs
+
+1.1.1 (2011-11-27)
+------------------
+* Add node attribute: href (thanks to @r_rudi!)
+* Fix bugs
+
+1.1.0 (2011-11-19)
+------------------
+* Add shape: square and circle
+* Add fontfamily attribute for switching fontface
+* Fix bugs
+
+1.0.3 (2011-11-13)
+------------------
+* Add plugin: attributes
+* Change plugin syntax; (cf. plugin attributes [attr = value, attr, value])
+* Fix bugs
+
+1.0.2 (2011-11-07)
+------------------
+* Fix bugs
+
+1.0.1 (2011-11-06)
+------------------
+* Add group attribute: shape
+* Fix bugs
+
+1.0.0 (2011-11-04)
+------------------
+* Add node attribute: linecolor
+* Rename diagram attributes:
+   * fontsize -> default_fontsize
+   * default_line_color -> default_linecolor
+   * default_text_color -> default_textcolor
+* Add docutils extention
+* Fix bugs
+
+0.9.7 (2011-11-01)
+------------------
+* Add node attribute: fontsize
+* Add edge attributes: thick, fontsize
+* Add group attribute: fontsize
+* Change color of shadow in PDF mode
+* Add class feature (experimental)
+* Add handler-plugin framework (experimental)
+
+0.9.6 (2011-10-22)
+------------------
+* node.style supports dashed_array format style
+* Fix bugs
+
+0.9.5 (2011-10-19)
+------------------
+* Add node attributes: width and height
+* Fix bugs
+
+0.9.4 (2011-10-07)
+------------------
+* Fix bugs
+
+0.9.3 (2011-10-06)
+------------------
+* Replace SVG core by original's (simplesvg.py)
+* Refactored
+* Fix bugs
+
+0.9.2 (2011-09-30)
+------------------
+* Add node attribute: textcolor
+* Add group attribute: textcolor
+* Add edge attribute: textcolor
+* Add diagram attributes: default_text_attribute
+* Fix beginpoint shape and endpoint shape were reversed
+* Fix bugs
+
+0.9.1 (2011-09-26)
+------------------
+* Add diagram attributes: default_node_color, default_group_color and default_line_color
+* Fix bugs
+
+0.9.0 (2011-09-25)
+------------------
+* Add icon attribute to node
+* Make transparency to background of PNG images 
+* Fix bugs
+
+0.8.9 (2011-08-09)
+------------------
+* Fix bugs
+
+0.8.8 (2011-08-08)
+------------------
+* Fix bugs
+
+0.8.7 (2011-08-06)
+------------------
+* Fix bugs
+
+0.8.6 (2011-08-01)
+------------------
+* Support Pillow as replacement of PIL (experimental)
+* Fix bugs
+
+0.8.5 (2011-07-31)
+------------------
+* Allow dot characters in node_id
+* Fix bugs
+
+0.8.4 (2011-07-05)
+------------------
+* Fix bugs
+
+0.8.3 (2011-07-03)
+------------------
+* Support input from stdin
+* Fix bugs
+
+0.8.2 (2011-06-29)
+------------------
+* Add node.stacked
+* Add node shapes: dots, none
+* Add hiragino-font to font search list
+* Support background image fetching from web
+* Add diagram.edge_layout (experimental)
+* Fix bugs
+
+0.8.1 (2011-05-14)
+------------------
+* Change license to Apache License 2.0
+* Fix bugs
+
+0.8.0 (2011-05-04)
+------------------
+* Add --separate option and --version option
+* Fix bugs
+
+0.7.8 (2011-04-19)
+------------------
+* Update layout engine
+* Update requirements: PIL >= 1.1.5
+* Update parser for tokenize performance
+* Add --nodoctype option
+* Fix bugs
+* Add many testcases
+
+0.7.7 (2011-03-29)
+------------------
+* Fix bugs
+
+0.7.6 (2011-03-26)
+------------------
+* Add new layout manager for portrait edges
+* Fix bugs
+
+0.7.5 (2011-03-20)
+------------------
+* Support multiple nodes relations (cf. A -> B, C)
+* Support node group declaration at attribute of nodes
+* Fix bugs
+
+0.7.4 (2011-03-08)
+------------------
+* Fix bugs
+
+0.7.3 (2011-03-02)
+------------------
+* Use UTF-8 characters as Name token (by @swtw7466)
+* Fix htmlentities included in labels was not escaped on SVG images
+* Fix bugs
+
+0.7.2 (2011-02-28)
+------------------
+* Add default_shape attribute to diagram
+
+0.7.1 (2011-02-27)
+------------------
+* Fix edge has broken with antialias option
+
+0.7.0 (2011-02-25)
+------------------
+* Support node shape
+
+0.6.7 (2011-02-12)
+------------------
+* Change noderenderer interface to new style
+* Render dashed ellipse more clearly (contributed by @cocoatomo)
+* Support PDF exporting
+
+0.6.6 (2011-01-31)
+------------------
+* Support diagram.shape_namespace
+* Add new node shapes; mail, cloud, beginpoint, endpoint, minidiamond, actor
+* Support plug-in structure to install node shapes
+* Fix bugs
+
+0.6.5 (2011-01-18)
+------------------
+* Support node shape (experimental)
+
+0.6.4 (2011-01-17)
+------------------
+* Fix bugs
+
+0.6.3 (2011-01-15)
+------------------
+* Fix bugs
+
+0.6.2 (2011-01-08)
+------------------
+* Fix bugs
+
+0.6.1 (2011-01-07)
+------------------
+* Implement 'folded' attribute for edge
+* Refactor layout engine
+
+0.6 (2011-01-02)
+------------------
+* Support nested groups.
+
+0.5.5 (2010-12-24)
+------------------
+* Specify direction of edges as syntax (->, --, <-, <->)
+* Fix bugs.
+
+0.5.4 (2010-12-23)
+------------------
+* Remove debug codes.
+
+0.5.3 (2010-12-23)
+------------------
+* Support NodeGroup.label.
+* Implement --separate option (experimental)
+* Fix right-up edge overrapped on other nodes.
+* Support configration file: .blockdiagrc
+
+0.5.2 (2010-11-06)
+------------------
+* Fix unicode errors for UTF-8'ed SVG exportion.
+* Refactoring codes for running on GAE.
+
+0.5.1 (2010-10-26)
+------------------
+* Fix license text on diagparser.py
+* Update layout engine.
+
+0.5 (2010-10-15)
+------------------
+* Support background-image of node (SVG)
+* Support labels for edge.
+* Fix bugs.
+
+0.4.2 (2010-10-10)
+------------------
+* Support background-color of node groups.
+* Draw edge has jumped at edge's cross-points.
+* Fix bugs.
+
+0.4.1 (2010-10-07)
+------------------
+* Fix bugs.
+
+0.4 (2010-10-07)
+------------------
+* Support SVG exporting.
+* Support dashed edge drawing.
+* Support background image of nodes (PNG only)
+
+0.3.1 (2010-09-29)
+------------------
+* Fasten anti-alias process.
+* Fix text was broken on windows.
+
+0.3 (2010-09-26)
+------------------
+* Add --antialias option.
+* Fix bugs.
+
+0.2.2 (2010-09-25)
+------------------
+* Fix edge bugs.
+
+0.2.1 (2010-09-25)
+------------------
+* Fix bugs.
+* Fix package style.
+
+0.2 (2010-09-23)
+------------------
+* Update layout engine.
+* Support group { ... } sentence for create Node-Groups.
+* Support numbered badge on node (cf. A [numbered = 5])
+
+0.1 (2010-09-20)
+-----------------
+* first release
+
diff --git a/src/TODO.txt b/src/TODO.txt
new file mode 100644
index 0000000..b28943a
--- /dev/null
+++ b/src/TODO.txt
@@ -0,0 +1,14 @@
+Todos
+======
+
+Functionals
+------------
+* Reimplement --separate option
+* Support diagram legends
+* Support other block diagram structure
+
+Known Issues
+-------------
+* Fix some experimental features.
+* PDF renderer does not support blur shadow
+* PDF renderer does not support path rendering
diff --git a/src/blockdiag.egg-info/PKG-INFO b/src/blockdiag.egg-info/PKG-INFO
new file mode 100644
index 0000000..89eb60a
--- /dev/null
+++ b/src/blockdiag.egg-info/PKG-INFO
@@ -0,0 +1,449 @@
+Metadata-Version: 1.0
+Name: blockdiag
+Version: 1.1.2
+Summary: blockdiag generate block-diagram image file from spec-text file.
+Home-page: http://blockdiag.com/
+Author: Takeshi Komiya
+Author-email: i.tkomiya at gmail.com
+License: Apache License 2.0
+Description: `blockdiag` generate block-diagram image file from spec-text file.
+        
+        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 
+        `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+        
+        Setup
+        =====
+        
+        by easy_install
+        ----------------
+        Make environment::
+        
+           $ easy_install blockdiag
+        
+        If you want to export as PDF format, give pdf arguments::
+        
+           $ easy_install "blockdiag[pdf]"
+        
+        by buildout
+        ------------
+        Make environment::
+        
+           $ hg clone http://bitbucket.org/tk0miya/blockdiag
+           $ cd blockdiag
+           $ python bootstrap.py
+           $ bin/buildout
+        
+        Copy and modify ini file. example::
+        
+           $ cp <blockdiag installed path>/blockdiag/examples/simple.diag .
+           $ vi simple.diag
+        
+        Please refer to `spec-text setting sample`_ section for the format of the
+        `simpla.diag` configuration file.
+        
+        spec-text setting sample
+        ========================
+        Few examples are available.
+        You can get more examples at
+        `blockdiag.com <http://blockdiag.com/blockdiag/build/html/index.html>`_ .
+        
+        simple.diag
+        ------------
+        simple.diag is simply define nodes and transitions by dot-like text format::
+        
+            diagram admin {
+              top_page -> config -> config_edit -> config_confirm -> top_page;
+            }
+        
+        screen.diag
+        ------------
+        screen.diag is more complexly sample. diaglam nodes have a alternative label
+        and some transitions::
+        
+            diagram admin {
+              top_page [label = "Top page"];
+        
+              foo_index [label = "List of FOOs"];
+              foo_detail [label = "Detail FOO"];
+              foo_add [label = "Add FOO"];
+              foo_add_confirm [label = "Add FOO (confirm)"];
+              foo_edit [label = "Edit FOO"];
+              foo_edit_confirm [label = "Edit FOO (confirm)"];
+              foo_delete_confirm [label = "Delete FOO (confirm)"];
+        
+              bar_detail [label = "Detail of BAR"];
+              bar_edit [label = "Edit BAR"];
+              bar_edit_confirm [label = "Edit BAR (confirm)"];
+        
+              logout;
+        
+              top_page -> foo_index;
+              top_page -> bar_detail;
+        
+              foo_index -> foo_detail;
+                           foo_detail -> foo_edit;
+                           foo_detail -> foo_delete_confirm;
+              foo_index -> foo_add -> foo_add_confirm -> foo_index;
+              foo_index -> foo_edit -> foo_edit_confirm -> foo_index;
+              foo_index -> foo_delete_confirm -> foo_index;
+        
+              bar_detail -> bar_edit -> bar_edit_confirm -> bar_detail;
+            }
+        
+        
+        Usage
+        =====
+        Execute blockdiag command::
+        
+           $ blockdiag simple.diag
+           $ ls simple.png
+           simple.png
+        
+        
+        Requirements
+        ============
+        * Python 2.4 or later (not support 3.x)
+        * Python Imaging Library 1.1.5 or later.
+        * funcparserlib 0.3.4 or later.
+        * setuptools or distribute.
+        
+        
+        License
+        =======
+        Apache License 2.0
+        
+        
+        History
+        =======
+        
+        1.1.2 (2011-12-26)
+        ------------------
+        * Support font-index for TrueType Font Collections (.ttc file)
+        * Allow to use reST syntax in descriptions of nodes
+        * Fix bugs
+        
+        1.1.1 (2011-11-27)
+        ------------------
+        * Add node attribute: href (thanks to @r_rudi!)
+        * Fix bugs
+        
+        1.1.0 (2011-11-19)
+        ------------------
+        * Add shape: square and circle
+        * Add fontfamily attribute for switching fontface
+        * Fix bugs
+        
+        1.0.3 (2011-11-13)
+        ------------------
+        * Add plugin: attributes
+        * Change plugin syntax; (cf. plugin attributes [attr = value, attr, value])
+        * Fix bugs
+        
+        1.0.2 (2011-11-07)
+        ------------------
+        * Fix bugs
+        
+        1.0.1 (2011-11-06)
+        ------------------
+        * Add group attribute: shape
+        * Fix bugs
+        
+        1.0.0 (2011-11-04)
+        ------------------
+        * Add node attribute: linecolor
+        * Rename diagram attributes:
+           * fontsize -> default_fontsize
+           * default_line_color -> default_linecolor
+           * default_text_color -> default_textcolor
+        * Add docutils extention
+        * Fix bugs
+        
+        0.9.7 (2011-11-01)
+        ------------------
+        * Add node attribute: fontsize
+        * Add edge attributes: thick, fontsize
+        * Add group attribute: fontsize
+        * Change color of shadow in PDF mode
+        * Add class feature (experimental)
+        * Add handler-plugin framework (experimental)
+        
+        0.9.6 (2011-10-22)
+        ------------------
+        * node.style supports dashed_array format style
+        * Fix bugs
+        
+        0.9.5 (2011-10-19)
+        ------------------
+        * Add node attributes: width and height
+        * Fix bugs
+        
+        0.9.4 (2011-10-07)
+        ------------------
+        * Fix bugs
+        
+        0.9.3 (2011-10-06)
+        ------------------
+        * Replace SVG core by original's (simplesvg.py)
+        * Refactored
+        * Fix bugs
+        
+        0.9.2 (2011-09-30)
+        ------------------
+        * Add node attribute: textcolor
+        * Add group attribute: textcolor
+        * Add edge attribute: textcolor
+        * Add diagram attributes: default_text_attribute
+        * Fix beginpoint shape and endpoint shape were reversed
+        * Fix bugs
+        
+        0.9.1 (2011-09-26)
+        ------------------
+        * Add diagram attributes: default_node_color, default_group_color and default_line_color
+        * Fix bugs
+        
+        0.9.0 (2011-09-25)
+        ------------------
+        * Add icon attribute to node
+        * Make transparency to background of PNG images 
+        * Fix bugs
+        
+        0.8.9 (2011-08-09)
+        ------------------
+        * Fix bugs
+        
+        0.8.8 (2011-08-08)
+        ------------------
+        * Fix bugs
+        
+        0.8.7 (2011-08-06)
+        ------------------
+        * Fix bugs
+        
+        0.8.6 (2011-08-01)
+        ------------------
+        * Support Pillow as replacement of PIL (experimental)
+        * Fix bugs
+        
+        0.8.5 (2011-07-31)
+        ------------------
+        * Allow dot characters in node_id
+        * Fix bugs
+        
+        0.8.4 (2011-07-05)
+        ------------------
+        * Fix bugs
+        
+        0.8.3 (2011-07-03)
+        ------------------
+        * Support input from stdin
+        * Fix bugs
+        
+        0.8.2 (2011-06-29)
+        ------------------
+        * Add node.stacked
+        * Add node shapes: dots, none
+        * Add hiragino-font to font search list
+        * Support background image fetching from web
+        * Add diagram.edge_layout (experimental)
+        * Fix bugs
+        
+        0.8.1 (2011-05-14)
+        ------------------
+        * Change license to Apache License 2.0
+        * Fix bugs
+        
+        0.8.0 (2011-05-04)
+        ------------------
+        * Add --separate option and --version option
+        * Fix bugs
+        
+        0.7.8 (2011-04-19)
+        ------------------
+        * Update layout engine
+        * Update requirements: PIL >= 1.1.5
+        * Update parser for tokenize performance
+        * Add --nodoctype option
+        * Fix bugs
+        * Add many testcases
+        
+        0.7.7 (2011-03-29)
+        ------------------
+        * Fix bugs
+        
+        0.7.6 (2011-03-26)
+        ------------------
+        * Add new layout manager for portrait edges
+        * Fix bugs
+        
+        0.7.5 (2011-03-20)
+        ------------------
+        * Support multiple nodes relations (cf. A -> B, C)
+        * Support node group declaration at attribute of nodes
+        * Fix bugs
+        
+        0.7.4 (2011-03-08)
+        ------------------
+        * Fix bugs
+        
+        0.7.3 (2011-03-02)
+        ------------------
+        * Use UTF-8 characters as Name token (by @swtw7466)
+        * Fix htmlentities included in labels was not escaped on SVG images
+        * Fix bugs
+        
+        0.7.2 (2011-02-28)
+        ------------------
+        * Add default_shape attribute to diagram
+        
+        0.7.1 (2011-02-27)
+        ------------------
+        * Fix edge has broken with antialias option
+        
+        0.7.0 (2011-02-25)
+        ------------------
+        * Support node shape
+        
+        0.6.7 (2011-02-12)
+        ------------------
+        * Change noderenderer interface to new style
+        * Render dashed ellipse more clearly (contributed by @cocoatomo)
+        * Support PDF exporting
+        
+        0.6.6 (2011-01-31)
+        ------------------
+        * Support diagram.shape_namespace
+        * Add new node shapes; mail, cloud, beginpoint, endpoint, minidiamond, actor
+        * Support plug-in structure to install node shapes
+        * Fix bugs
+        
+        0.6.5 (2011-01-18)
+        ------------------
+        * Support node shape (experimental)
+        
+        0.6.4 (2011-01-17)
+        ------------------
+        * Fix bugs
+        
+        0.6.3 (2011-01-15)
+        ------------------
+        * Fix bugs
+        
+        0.6.2 (2011-01-08)
+        ------------------
+        * Fix bugs
+        
+        0.6.1 (2011-01-07)
+        ------------------
+        * Implement 'folded' attribute for edge
+        * Refactor layout engine
+        
+        0.6 (2011-01-02)
+        ------------------
+        * Support nested groups.
+        
+        0.5.5 (2010-12-24)
+        ------------------
+        * Specify direction of edges as syntax (->, --, <-, <->)
+        * Fix bugs.
+        
+        0.5.4 (2010-12-23)
+        ------------------
+        * Remove debug codes.
+        
+        0.5.3 (2010-12-23)
+        ------------------
+        * Support NodeGroup.label.
+        * Implement --separate option (experimental)
+        * Fix right-up edge overrapped on other nodes.
+        * Support configration file: .blockdiagrc
+        
+        0.5.2 (2010-11-06)
+        ------------------
+        * Fix unicode errors for UTF-8'ed SVG exportion.
+        * Refactoring codes for running on GAE.
+        
+        0.5.1 (2010-10-26)
+        ------------------
+        * Fix license text on diagparser.py
+        * Update layout engine.
+        
+        0.5 (2010-10-15)
+        ------------------
+        * Support background-image of node (SVG)
+        * Support labels for edge.
+        * Fix bugs.
+        
+        0.4.2 (2010-10-10)
+        ------------------
+        * Support background-color of node groups.
+        * Draw edge has jumped at edge's cross-points.
+        * Fix bugs.
+        
+        0.4.1 (2010-10-07)
+        ------------------
+        * Fix bugs.
+        
+        0.4 (2010-10-07)
+        ------------------
+        * Support SVG exporting.
+        * Support dashed edge drawing.
+        * Support background image of nodes (PNG only)
+        
+        0.3.1 (2010-09-29)
+        ------------------
+        * Fasten anti-alias process.
+        * Fix text was broken on windows.
+        
+        0.3 (2010-09-26)
+        ------------------
+        * Add --antialias option.
+        * Fix bugs.
+        
+        0.2.2 (2010-09-25)
+        ------------------
+        * Fix edge bugs.
+        
+        0.2.1 (2010-09-25)
+        ------------------
+        * Fix bugs.
+        * Fix package style.
+        
+        0.2 (2010-09-23)
+        ------------------
+        * Update layout engine.
+        * Support group { ... } sentence for create Node-Groups.
+        * Support numbered badge on node (cf. A [numbered = 5])
+        
+        0.1 (2010-09-20)
+        -----------------
+        * first release
+        
+        Todos
+        ======
+        
+        Functionals
+        ------------
+        * Reimplement --separate option
+        * Support diagram legends
+        * Support other block diagram structure
+        
+        Known Issues
+        -------------
+        * Fix some experimental features.
+        * PDF renderer does not support blur shadow
+        * PDF renderer does not support path rendering
+        
+Keywords: diagram,generator
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Documentation
+Classifier: Topic :: Text Processing :: Markup
diff --git a/src/blockdiag.egg-info/SOURCES.txt b/src/blockdiag.egg-info/SOURCES.txt
new file mode 100644
index 0000000..fadb6a7
--- /dev/null
+++ b/src/blockdiag.egg-info/SOURCES.txt
@@ -0,0 +1,234 @@
+LICENSE
+MANIFEST.in
+blockdiag.1
+bootstrap.py
+buildout.cfg
+setup.cfg
+setup.py
+examples/group.diag
+examples/group.png
+examples/group.svg
+examples/multibyte.diag
+examples/multibyte.png
+examples/multibyte.svg
+examples/numbered.diag
+examples/numbered.png
+examples/numbered.svg
+examples/screen.diag
+examples/screen.png
+examples/screen.svg
+examples/simple.diag
+examples/simple.png
+examples/simple.svg
+src/README.txt
+src/TODO.txt
+src/blockdiag_sphinxhelper.py
+src/blockdiag/DiagramDraw.py
+src/blockdiag/DiagramMetrics.py
+src/blockdiag/__init__.py
+src/blockdiag/builder.py
+src/blockdiag/command.py
+src/blockdiag/drawer.py
+src/blockdiag/elements.py
+src/blockdiag/metrics.py
+src/blockdiag/parser.py
+src/blockdiag.egg-info/PKG-INFO
+src/blockdiag.egg-info/SOURCES.txt
+src/blockdiag.egg-info/dependency_links.txt
+src/blockdiag.egg-info/entry_points.txt
+src/blockdiag.egg-info/requires.txt
+src/blockdiag.egg-info/top_level.txt
+src/blockdiag/imagedraw/__init__.py
+src/blockdiag/imagedraw/pdf.py
+src/blockdiag/imagedraw/png.py
+src/blockdiag/imagedraw/simplesvg.py
+src/blockdiag/imagedraw/svg.py
+src/blockdiag/imagedraw/filters/__init__.py
+src/blockdiag/imagedraw/filters/linejump.py
+src/blockdiag/noderenderer/__init__.py
+src/blockdiag/noderenderer/actor.py
+src/blockdiag/noderenderer/beginpoint.py
+src/blockdiag/noderenderer/box.py
+src/blockdiag/noderenderer/circle.py
+src/blockdiag/noderenderer/cloud.py
+src/blockdiag/noderenderer/diamond.py
+src/blockdiag/noderenderer/dots.py
+src/blockdiag/noderenderer/ellipse.py
+src/blockdiag/noderenderer/endpoint.py
+src/blockdiag/noderenderer/mail.py
+src/blockdiag/noderenderer/minidiamond.py
+src/blockdiag/noderenderer/none.py
+src/blockdiag/noderenderer/note.py
+src/blockdiag/noderenderer/roundedbox.py
+src/blockdiag/noderenderer/square.py
+src/blockdiag/noderenderer/textbox.py
+src/blockdiag/noderenderer/flowchart/__init__.py
+src/blockdiag/noderenderer/flowchart/database.py
+src/blockdiag/noderenderer/flowchart/input.py
+src/blockdiag/noderenderer/flowchart/loopin.py
+src/blockdiag/noderenderer/flowchart/loopout.py
+src/blockdiag/noderenderer/flowchart/terminator.py
+src/blockdiag/plugins/__init__.py
+src/blockdiag/plugins/attributes.py
+src/blockdiag/plugins/autoclass.py
+src/blockdiag/tests/__init__.py
+src/blockdiag/tests/test_boot_params.py
+src/blockdiag/tests/test_builder.py
+src/blockdiag/tests/test_builder_edge.py
+src/blockdiag/tests/test_builder_errors.py
+src/blockdiag/tests/test_builder_group.py
+src/blockdiag/tests/test_builder_node.py
+src/blockdiag/tests/test_builder_separate.py
+src/blockdiag/tests/test_generate_diagram.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_fontmap.py
+src/blockdiag/tests/utils.py
+src/blockdiag/tests/diagrams/auto_jumping_edge.diag
+src/blockdiag/tests/diagrams/background_url_image.diag
+src/blockdiag/tests/diagrams/beginpoint_color.diag
+src/blockdiag/tests/diagrams/branched.diag
+src/blockdiag/tests/diagrams/circular_ref.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/define_class.diag
+src/blockdiag/tests/diagrams/diagram_attributes.diag
+src/blockdiag/tests/diagrams/diagram_attributes_order.diag
+src/blockdiag/tests/diagrams/diagram_orientation.diag
+src/blockdiag/tests/diagrams/edge_attribute.diag
+src/blockdiag/tests/diagrams/edge_label.diag
+src/blockdiag/tests/diagrams/edge_layout_landscape.diag
+src/blockdiag/tests/diagrams/edge_layout_portrait.diag
+src/blockdiag/tests/diagrams/edge_shape.diag
+src/blockdiag/tests/diagrams/edge_styles.diag
+src/blockdiag/tests/diagrams/empty_group.diag
+src/blockdiag/tests/diagrams/empty_group_declaration.diag
+src/blockdiag/tests/diagrams/empty_nested_group.diag
+src/blockdiag/tests/diagrams/endpoint_color.diag
+src/blockdiag/tests/diagrams/flowable_node.diag
+src/blockdiag/tests/diagrams/folded_edge.diag
+src/blockdiag/tests/diagrams/group_and_skipped_edge.diag
+src/blockdiag/tests/diagrams/group_attribute.diag
+src/blockdiag/tests/diagrams/group_children_height.diag
+src/blockdiag/tests/diagrams/group_children_order.diag
+src/blockdiag/tests/diagrams/group_children_order2.diag
+src/blockdiag/tests/diagrams/group_children_order3.diag
+src/blockdiag/tests/diagrams/group_children_order4.diag
+src/blockdiag/tests/diagrams/group_declare_as_node_attribute.diag
+src/blockdiag/tests/diagrams/group_height.diag
+src/blockdiag/tests/diagrams/group_id_and_node_id_are_not_conflicted.diag
+src/blockdiag/tests/diagrams/group_label.diag
+src/blockdiag/tests/diagrams/group_order.diag
+src/blockdiag/tests/diagrams/group_order2.diag
+src/blockdiag/tests/diagrams/group_order3.diag
+src/blockdiag/tests/diagrams/group_orientation.diag
+src/blockdiag/tests/diagrams/group_sibling.diag
+src/blockdiag/tests/diagrams/group_works_node_decorator.diag
+src/blockdiag/tests/diagrams/labeled_circular_ref.diag
+src/blockdiag/tests/diagrams/large_group_and_node.diag
+src/blockdiag/tests/diagrams/large_group_and_node2.diag
+src/blockdiag/tests/diagrams/large_group_and_two_nodes.diag
+src/blockdiag/tests/diagrams/merge_groups.diag
+src/blockdiag/tests/diagrams/multiple_groups.diag
+src/blockdiag/tests/diagrams/multiple_nested_groups.diag
+src/blockdiag/tests/diagrams/multiple_node_relation.diag
+src/blockdiag/tests/diagrams/multiple_nodes_definition.diag
+src/blockdiag/tests/diagrams/multiple_parent_node.diag
+src/blockdiag/tests/diagrams/nested_group_orientation.diag
+src/blockdiag/tests/diagrams/nested_group_orientation2.diag
+src/blockdiag/tests/diagrams/nested_groups.diag
+src/blockdiag/tests/diagrams/nested_groups_and_edges.diag
+src/blockdiag/tests/diagrams/nested_groups_work_node_decorator.diag
+src/blockdiag/tests/diagrams/nested_skipped_circular.diag
+src/blockdiag/tests/diagrams/node_attribute.diag
+src/blockdiag/tests/diagrams/node_attribute_and_group.diag
+src/blockdiag/tests/diagrams/node_has_multilined_label.diag
+src/blockdiag/tests/diagrams/node_height.diag
+src/blockdiag/tests/diagrams/node_icon.diag
+src/blockdiag/tests/diagrams/node_id_includes_dot.diag
+src/blockdiag/tests/diagrams/node_in_group_follows_outer_node.diag
+src/blockdiag/tests/diagrams/node_link.diag
+src/blockdiag/tests/diagrams/node_rotated_labels.diag
+src/blockdiag/tests/diagrams/node_shape.diag
+src/blockdiag/tests/diagrams/node_shape_background.diag
+src/blockdiag/tests/diagrams/node_shape_namespace.diag
+src/blockdiag/tests/diagrams/node_style_dasharray.diag
+src/blockdiag/tests/diagrams/node_styles.diag
+src/blockdiag/tests/diagrams/node_width_and_height.diag
+src/blockdiag/tests/diagrams/non_rhombus_relation_height.diag
+src/blockdiag/tests/diagrams/outer_node_follows_node_in_group.diag
+src/blockdiag/tests/diagrams/plugin_attributes.diag
+src/blockdiag/tests/diagrams/plugin_autoclass.diag
+src/blockdiag/tests/diagrams/portrait_dots.diag
+src/blockdiag/tests/diagrams/quoted_node_id.diag
+src/blockdiag/tests/diagrams/reverse_multiple_groups.diag
+src/blockdiag/tests/diagrams/rhombus_relation_height.diag
+src/blockdiag/tests/diagrams/self_ref.diag
+src/blockdiag/tests/diagrams/separate1.diag
+src/blockdiag/tests/diagrams/separate2.diag
+src/blockdiag/tests/diagrams/simple_group.diag
+src/blockdiag/tests/diagrams/single_edge.diag
+src/blockdiag/tests/diagrams/single_node.diag
+src/blockdiag/tests/diagrams/skipped_circular.diag
+src/blockdiag/tests/diagrams/skipped_edge.diag
+src/blockdiag/tests/diagrams/skipped_edge_down.diag
+src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown.diag
+src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown2.diag
+src/blockdiag/tests/diagrams/skipped_edge_leftdown.diag
+src/blockdiag/tests/diagrams/skipped_edge_portrait_down.diag
+src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown.diag
+src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown2.diag
+src/blockdiag/tests/diagrams/skipped_edge_portrait_leftdown.diag
+src/blockdiag/tests/diagrams/skipped_edge_portrait_right.diag
+src/blockdiag/tests/diagrams/skipped_edge_portrait_rightdown.diag
+src/blockdiag/tests/diagrams/skipped_edge_right.diag
+src/blockdiag/tests/diagrams/skipped_edge_rightdown.diag
+src/blockdiag/tests/diagrams/skipped_edge_rightup.diag
+src/blockdiag/tests/diagrams/skipped_edge_up.diag
+src/blockdiag/tests/diagrams/skipped_twin_circular.diag
+src/blockdiag/tests/diagrams/slided_children.diag
+src/blockdiag/tests/diagrams/stacked_node_and_edges.diag
+src/blockdiag/tests/diagrams/triple_branched.diag
+src/blockdiag/tests/diagrams/twin_circular_ref.diag
+src/blockdiag/tests/diagrams/twin_circular_ref_to_root.diag
+src/blockdiag/tests/diagrams/twin_forked.diag
+src/blockdiag/tests/diagrams/twin_multiple_parent_node.diag
+src/blockdiag/tests/diagrams/two_edges.diag
+src/blockdiag/tests/diagrams/errors/belongs_to_two_groups.diag
+src/blockdiag/tests/diagrams/errors/group_follows_node.diag
+src/blockdiag/tests/diagrams/errors/lexer_error.diag
+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_edge_class.diag
+src/blockdiag/tests/diagrams/errors/unknown_edge_dir.diag
+src/blockdiag/tests/diagrams/errors/unknown_edge_hstyle.diag
+src/blockdiag/tests/diagrams/errors/unknown_edge_style.diag
+src/blockdiag/tests/diagrams/errors/unknown_group_class.diag
+src/blockdiag/tests/diagrams/errors/unknown_group_orientation.diag
+src/blockdiag/tests/diagrams/errors/unknown_group_shape.diag
+src/blockdiag/tests/diagrams/errors/unknown_node_attribute.diag
+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/utils/PDFTextFolder.py
+src/blockdiag/utils/PILTextFolder.py
+src/blockdiag/utils/TextFolder.py
+src/blockdiag/utils/__init__.py
+src/blockdiag/utils/bootstrap.py
+src/blockdiag/utils/collections.py
+src/blockdiag/utils/config.py
+src/blockdiag/utils/ellipse.py
+src/blockdiag/utils/fontmap.py
+src/blockdiag/utils/images.py
+src/blockdiag/utils/jpeg.py
+src/blockdiag/utils/myitertools.py
+src/blockdiag/utils/namedtuple.py
+src/blockdiag/utils/urlutil.py
+src/blockdiag/utils/uuid.py
+src/blockdiag/utils/rst/__init__.py
+src/blockdiag/utils/rst/directives.py
\ No newline at end of file
diff --git a/src/blockdiag.egg-info/dependency_links.txt b/src/blockdiag.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/blockdiag.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/src/blockdiag.egg-info/entry_points.txt b/src/blockdiag.egg-info/entry_points.txt
new file mode 100644
index 0000000..b146167
--- /dev/null
+++ b/src/blockdiag.egg-info/entry_points.txt
@@ -0,0 +1,31 @@
+
+        [console_scripts]
+        blockdiag = blockdiag.command:main
+
+        [blockdiag_noderenderer]
+        box = blockdiag.noderenderer.box
+        square = blockdiag.noderenderer.square
+        roundedbox = blockdiag.noderenderer.roundedbox
+        diamond = blockdiag.noderenderer.diamond
+        minidiamond = blockdiag.noderenderer.minidiamond
+        mail = blockdiag.noderenderer.mail
+        note = blockdiag.noderenderer.note
+        cloud = blockdiag.noderenderer.cloud
+        circle = blockdiag.noderenderer.circle
+        ellipse = blockdiag.noderenderer.ellipse
+        beginpoint = blockdiag.noderenderer.beginpoint
+        endpoint = blockdiag.noderenderer.endpoint
+        actor = blockdiag.noderenderer.actor
+        flowchart.database = blockdiag.noderenderer.flowchart.database
+        flowchart.input = blockdiag.noderenderer.flowchart.input
+        flowchart.loopin = blockdiag.noderenderer.flowchart.loopin
+        flowchart.loopout = blockdiag.noderenderer.flowchart.loopout
+        flowchart.terminator = blockdiag.noderenderer.flowchart.terminator
+        textbox = blockdiag.noderenderer.textbox
+        dots = blockdiag.noderenderer.dots
+        none = blockdiag.noderenderer.none
+
+        [blockdiag_plugins]
+        attributes = blockdiag.plugins.attributes
+        autoclass = blockdiag.plugins.autoclass
+     
\ No newline at end of file
diff --git a/src/blockdiag.egg-info/requires.txt b/src/blockdiag.egg-info/requires.txt
new file mode 100644
index 0000000..b311d36
--- /dev/null
+++ b/src/blockdiag.egg-info/requires.txt
@@ -0,0 +1,16 @@
+setuptools
+funcparserlib
+webcolors
+PIL
+OrderedDict
+
+[rst]
+docutils
+
+[pdf]
+reportlab
+
+[test]
+Nose
+pep8
+unittest2
\ No newline at end of file
diff --git a/src/blockdiag.egg-info/top_level.txt b/src/blockdiag.egg-info/top_level.txt
new file mode 100644
index 0000000..40c073e
--- /dev/null
+++ b/src/blockdiag.egg-info/top_level.txt
@@ -0,0 +1,2 @@
+blockdiag_sphinxhelper
+blockdiag
diff --git a/src/blockdiag/DiagramDraw.py b/src/blockdiag/DiagramDraw.py
new file mode 100644
index 0000000..e5ccf94
--- /dev/null
+++ b/src/blockdiag/DiagramDraw.py
@@ -0,0 +1,16 @@
+# -*- 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.drawer import DiagramDraw
diff --git a/src/blockdiag/DiagramMetrics.py b/src/blockdiag/DiagramMetrics.py
new file mode 100644
index 0000000..e05463c
--- /dev/null
+++ b/src/blockdiag/DiagramMetrics.py
@@ -0,0 +1,16 @@
+# -*- 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.metrics import *
diff --git a/src/blockdiag/__init__.py b/src/blockdiag/__init__.py
new file mode 100644
index 0000000..40e1275
--- /dev/null
+++ b/src/blockdiag/__init__.py
@@ -0,0 +1,16 @@
+# -*- 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.
+
+__version__ = '1.1.2'
diff --git a/src/blockdiag/builder.py b/src/blockdiag/builder.py
new file mode 100644
index 0000000..4481585
--- /dev/null
+++ b/src/blockdiag/builder.py
@@ -0,0 +1,730 @@
+# -*- 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 elements import *
+import parser
+from utils import XY
+
+
+class DiagramTreeBuilder:
+    def build(self, tree):
+        self.diagram = Diagram()
+        self.instantiate(self.diagram, tree)
+        for subgroup in self.diagram.traverse_groups():
+            if len(subgroup.nodes) == 0:
+                subgroup.group.nodes.remove(subgroup)
+
+        self.bind_edges(self.diagram)
+        return self.diagram
+
+    def is_related_group(self, group1, group2):
+        if group1.is_parent(group2) or group2.is_parent(group1):
+            return True
+        else:
+            return False
+
+    def belong_to(self, node, group):
+        if node.group and node.group.level > group.level:
+            override = False
+        else:
+            override = True
+
+        if node.group and node.group != group and override:
+            if not self.is_related_group(node.group, group):
+                msg = "could not belong to two groups: %s" % node.id
+                raise RuntimeError(msg)
+
+            old_group = node.group
+
+            parent = group.parent(old_group.level + 1)
+            if parent:
+                if parent in old_group.nodes:
+                    old_group.nodes.remove(parent)
+
+                index = old_group.nodes.index(node)
+                old_group.nodes.insert(index + 1, parent)
+
+            old_group.nodes.remove(node)
+            node.group = None
+
+        if node.group is None:
+            node.group = group
+
+            if node not in group.nodes:
+                group.nodes.append(node)
+
+    def instantiate(self, group, tree):
+        for stmt in tree.stmts:
+            # Translate Node having group attribute to SubGraph
+            if isinstance(stmt, parser.Node):
+                group_attr = [a for a in stmt.attrs if a.name == 'group']
+                if group_attr:
+                    group_id = group_attr[-1]
+                    stmt.attrs.remove(group_id)
+
+                    if group_id.value != group.id:
+                        stmt = parser.SubGraph(group_id.value, [stmt])
+
+            # Instantiate statements
+            if isinstance(stmt, parser.Node):
+                node = DiagramNode.get(stmt.id)
+                node.set_attributes(stmt.attrs)
+                self.belong_to(node, group)
+
+            elif isinstance(stmt, parser.Edge):
+                nodes = stmt.nodes.pop(0)
+                edge_from = [DiagramNode.get(n) for n in nodes]
+                for node in edge_from:
+                    self.belong_to(node, group)
+
+                while len(stmt.nodes):
+                    edge_type, edge_to = stmt.nodes.pop(0)
+                    edge_to = [DiagramNode.get(n) for n in edge_to]
+                    for node in edge_to:
+                        self.belong_to(node, group)
+
+                    for node1 in edge_from:
+                        for node2 in edge_to:
+                            edge = DiagramEdge.get(node1, node2)
+                            if edge_type:
+                                attrs = [parser.Attr('dir', edge_type)]
+                                edge.set_attributes(attrs)
+                            edge.set_attributes(stmt.attrs)
+
+                    edge_from = edge_to
+
+            elif isinstance(stmt, parser.SubGraph):
+                subgroup = NodeGroup.get(stmt.id)
+                subgroup.level = group.level + 1
+                self.belong_to(subgroup, group)
+                self.instantiate(subgroup, stmt)
+
+            elif isinstance(stmt, parser.DefAttrs):
+                group.set_attributes(stmt.attrs)
+
+            elif isinstance(stmt, parser.AttrClass):
+                name = unquote(stmt.name)
+                Diagram.classes[name] = stmt
+
+            elif isinstance(stmt, parser.AttrPlugin):
+                self.diagram.set_plugin(stmt.name, stmt.attrs)
+
+            elif isinstance(stmt, parser.Statements):
+                self.instantiate(group, stmt)
+
+        group.update_order()
+        return group
+
+    def bind_edges(self, group):
+        for node in group.nodes:
+            if isinstance(node, DiagramNode):
+                group.edges += DiagramEdge.find(node)
+            else:
+                self.bind_edges(node)
+
+
+class DiagramLayoutManager:
+    def __init__(self, diagram):
+        self.diagram = diagram
+
+        self.circulars = []
+        self.heightRefs = []
+        self.coordinates = []
+
+    def run(self):
+        if isinstance(self.diagram, Diagram):
+            for group in self.diagram.traverse_groups():
+                self.__class__(group).run()
+
+        self.edges = DiagramEdge.find_by_level(self.diagram.level)
+        self.do_layout()
+        self.diagram.fixiate()
+
+        if self.diagram.orientation == 'portrait':
+            self.rotate_diagram()
+
+    def rotate_diagram(self):
+        for node in self.diagram.traverse_nodes():
+            node.xy = XY(node.xy.y, node.xy.x)
+            node.colwidth, node.colheight = (node.colheight, node.colwidth)
+
+            if isinstance(node, NodeGroup):
+                if node.orientation == 'portrait':
+                    node.orientation = 'landscape'
+                else:
+                    node.orientation = 'portrait'
+
+        xy = (self.diagram.colheight, self.diagram.colwidth)
+        self.diagram.colwidth, self.diagram.colheight = xy
+
+    def do_layout(self):
+        self.detect_circulars()
+
+        self.set_node_width()
+        self.adjust_node_order()
+
+        height = 0
+        for node in self.diagram.nodes:
+            if node.xy.x == 0:
+                self.set_node_height(node, height)
+                height = max(xy.y for xy in self.coordinates) + 1
+
+    def get_related_nodes(self, node, parent=False, child=False):
+        uniq = {}
+        for edge in self.edges:
+            if edge.folded:
+                continue
+
+            if parent and edge.node2 == node:
+                uniq[edge.node1] = 1
+            elif child and edge.node1 == node:
+                uniq[edge.node2] = 1
+
+        related = []
+        for uniq_node in uniq.keys():
+            if uniq_node == node:
+                pass
+            elif uniq_node.group != node.group:
+                pass
+            else:
+                related.append(uniq_node)
+
+        related.sort(lambda x, y: cmp(x.order, y.order))
+        return related
+
+    def get_parent_nodes(self, node):
+        return self.get_related_nodes(node, parent=True)
+
+    def get_child_nodes(self, node):
+        return self.get_related_nodes(node, child=True)
+
+    def detect_circulars(self):
+        for node in self.diagram.nodes:
+            if not [x for x in self.circulars if node in x]:
+                self.detect_circulars_sub(node, [node])
+
+        # remove part of other circular
+        for c1 in self.circulars[:]:
+            for c2 in self.circulars:
+                intersect = set(c1) & set(c2)
+
+                if c1 != c2 and set(c1) == intersect:
+                    if c1 in self.circulars:
+                        self.circulars.remove(c1)
+                    break
+
+                if c1 != c2 and intersect:
+                    if c1 in self.circulars:
+                        self.circulars.remove(c1)
+                    self.circulars.remove(c2)
+                    self.circulars.append(c1 + c2)
+                    break
+
+    def detect_circulars_sub(self, node, parents):
+        for child in self.get_child_nodes(node):
+            if child in parents:
+                i = parents.index(child)
+                self.circulars.append(parents[i:])
+            else:
+                self.detect_circulars_sub(child, parents + [child])
+
+    def is_circular_ref(self, node1, node2):
+        for circular in self.circulars:
+            if node1 in circular and node2 in circular:
+                parents = []
+                for node in circular:
+                    for parent in self.get_parent_nodes(node):
+                        if not parent in circular:
+                            parents.append(parent)
+
+                parents.sort(lambda x, y: cmp(x.order, y.order))
+
+                for parent in parents:
+                    children = self.get_child_nodes(parent)
+                    if node1 in children and node2 in children:
+                        if circular.index(node1) > circular.index(node2):
+                            return True
+                    elif node2 in children:
+                        return True
+                    elif node1 in children:
+                        return False
+                else:
+                    if circular.index(node1) > circular.index(node2):
+                        return True
+
+        return False
+
+    def set_node_width(self, depth=0):
+        for node in self.diagram.nodes:
+            if node.xy.x != depth:
+                continue
+
+            for child in self.get_child_nodes(node):
+                if self.is_circular_ref(node, child):
+                    pass
+                elif node == child:
+                    pass
+                elif child.xy.x > node.xy.x + node.colwidth:
+                    pass
+                else:
+                    child.xy = XY(node.xy.x + node.colwidth, 0)
+
+        depther_node = [x for x in self.diagram.nodes if x.xy.x > depth]
+        if len(depther_node) > 0:
+            self.set_node_width(depth + 1)
+
+    def adjust_node_order(self):
+        for node in list(self.diagram.nodes):
+            parents = self.get_parent_nodes(node)
+            if len(set(parents)) > 1:
+                for i in range(1, len(parents)):
+                    node1 = parents[i - 1]
+                    node2 = parents[i]
+
+                    if node1.xy.x == node2.xy.x:
+                        idx1 = self.diagram.nodes.index(node1)
+                        idx2 = self.diagram.nodes.index(node2)
+
+                        if idx1 < idx2:
+                            self.diagram.nodes.remove(node2)
+                            self.diagram.nodes.insert(idx1 + 1, node2)
+                        else:
+                            self.diagram.nodes.remove(node1)
+                            self.diagram.nodes.insert(idx2 + 1, node1)
+
+            children = self.get_child_nodes(node)
+            if len(set(children)) > 1:
+                for i in range(1, len(children)):
+                    node1 = children[i - 1]
+                    node2 = children[i]
+
+                    idx1 = self.diagram.nodes.index(node1)
+                    idx2 = self.diagram.nodes.index(node2)
+
+                    if node1.xy.x == node2.xy.x:
+                        if idx1 < idx2:
+                            self.diagram.nodes.remove(node2)
+                            self.diagram.nodes.insert(idx1 + 1, node2)
+                        else:
+                            self.diagram.nodes.remove(node1)
+                            self.diagram.nodes.insert(idx2 + 1, node1)
+                    elif self.is_circular_ref(node1, node2):
+                        pass
+                    else:
+                        if node1.xy.x < node2.xy.x:
+                            self.diagram.nodes.remove(node2)
+                            self.diagram.nodes.insert(idx1 + 1, node2)
+                        else:
+                            self.diagram.nodes.remove(node1)
+                            self.diagram.nodes.insert(idx2 + 1, node1)
+
+            if isinstance(node, NodeGroup):
+                children = self.get_child_nodes(node)
+                if len(set(children)) > 1:
+                    while True:
+                        exchange = 0
+
+                        for i in range(1, len(children)):
+                            node1 = children[i - 1]
+                            node2 = children[i]
+
+                            idx1 = self.diagram.nodes.index(node1)
+                            idx2 = self.diagram.nodes.index(node2)
+                            ret = self.compare_child_node_order(node,
+                                                                node1, node2)
+
+                            if ret > 0 and idx1 < idx2:
+                                self.diagram.nodes.remove(node1)
+                                self.diagram.nodes.insert(idx2 + 1, node1)
+                                exchange += 1
+
+                        if exchange == 0:
+                            break
+
+        self.diagram.update_order()
+
+    def compare_child_node_order(self, parent, node1, node2):
+        def compare(x, y):
+            x = x.duplicate()
+            y = y.duplicate()
+            while x.node1 == y.node1 and x.node1.group is not None:
+                x.node1 = x.node1.group
+                y.node1 = y.node1.group
+
+            return cmp(x.node1.order, y.node1.order)
+
+        edges = DiagramEdge.find(parent, node1) + \
+                DiagramEdge.find(parent, node2)
+        edges.sort(compare)
+        if len(edges) == 0:
+            return 0
+        elif edges[0].node2 == node2:
+            return 1
+        else:
+            return -1
+
+    def mark_xy(self, xy, width, height):
+        for w in range(width):
+            for h in range(height):
+                self.coordinates.append(XY(xy.x + w, xy.y + h))
+
+    def set_node_height(self, node, height=0):
+        for x in range(node.colwidth):
+            for y in range(node.colheight):
+                xy = XY(node.xy.x + x, height + y)
+                if xy in self.coordinates:
+                    return False
+        node.xy = XY(node.xy.x, height)
+        self.mark_xy(node.xy, node.colwidth, node.colheight)
+
+        count = 0
+        children = self.get_child_nodes(node)
+        children.sort(lambda x, y: cmp(x.xy.x, y.xy.y))
+
+        grandchild = 0
+        for child in children:
+            if self.get_child_nodes(child):
+                grandchild += 1
+
+        prev_child = None
+        for child in children:
+            if child.id in self.heightRefs:
+                pass
+            elif node.xy.x >= child.xy.x:
+                pass
+            else:
+                if isinstance(node, NodeGroup):
+                    parent_height = self.get_parent_node_height(node, child)
+                    if parent_height and parent_height > height:
+                        height = parent_height
+
+                if prev_child and grandchild > 1 and \
+                   not self.is_rhombus(prev_child, child):
+                    coord = [p.y for p in self.coordinates if p.x > child.xy.x]
+                    if coord and max(coord) >= node.xy.y:
+                        height = max(coord) + 1
+
+                while True:
+                    if self.set_node_height(child, height):
+                        child.xy = XY(child.xy.x, height)
+                        self.mark_xy(child.xy, child.colwidth, child.colheight)
+                        self.heightRefs.append(child.id)
+
+                        count += 1
+                        break
+                    else:
+                        if count == 0:
+                            return False
+
+                        height += 1
+
+                height += 1
+                prev_child = child
+
+        return True
+
+    def is_rhombus(self, node1, node2):
+        ret = False
+        while True:
+            if node1 == node2:
+                ret = True
+                break
+
+            child1 = self.get_child_nodes(node1)
+            child2 = self.get_child_nodes(node2)
+
+            if len(child1) != 1 or len(child2) != 1:
+                break
+            elif node1.xy.x > child1[0].xy.x or node2.xy.x > child2[0].xy.x:
+                break
+            else:
+                node1 = child1[0]
+                node2 = child2[0]
+
+        return ret
+
+    def get_parent_node_height(self, parent, child):
+        heights = []
+        for e in DiagramEdge.find(parent, child):
+            y = parent.xy.y
+
+            node = e.node1
+            while node != parent:
+                y += node.xy.y
+                node = node.group
+
+            heights.append(y)
+
+        if heights:
+            return min(heights)
+        else:
+            return None
+
+
+class EdgeLayoutManager(object):
+    def __init__(self, diagram):
+        self.diagram = diagram
+
+    @property
+    def groups(self):
+        if self.diagram.separated:
+            seq = self.diagram.nodes
+        else:
+            seq = self.diagram.traverse_groups(preorder=True)
+
+        for group in seq:
+            if not group.drawable:
+                yield group
+
+    @property
+    def nodes(self):
+        if self.diagram.separated:
+            seq = self.diagram.nodes
+        else:
+            seq = self.diagram.traverse_nodes()
+
+        for node in seq:
+            if node.drawable:
+                yield node
+
+    @property
+    def edges(self):
+        for edge in (e for e in self.diagram.edges  if e.style != 'none'):
+            yield edge
+
+        for group in self.groups:
+            for edge in (e for e in group.edges  if e.style != 'none'):
+                yield edge
+
+    def run(self):
+        for edge in self.edges:
+            dir = edge.direction
+
+            if edge.node1.group.orientation == 'landscape':
+                if dir == 'right':
+                    r = range(edge.node1.xy.x + 1, edge.node2.xy.x)
+                    for x in r:
+                        xy = (x, edge.node1.xy.y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+                elif dir == 'right-up':
+                    r = range(edge.node1.xy.x + 1, edge.node2.xy.x)
+                    for x in r:
+                        xy = (x, edge.node1.xy.y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+                elif dir == 'right-down':
+                    if self.diagram.edge_layout == 'flowchart':
+                        r = range(edge.node1.xy.y, edge.node2.xy.y)
+                        for y in r:
+                            xy = (edge.node1.xy.x, y + 1)
+                            nodes = [x for x in self.nodes if x.xy == xy]
+                            if len(nodes) > 0:
+                                edge.skipped = 1
+
+                    r = range(edge.node1.xy.x + 1, edge.node2.xy.x)
+                    for x in r:
+                        xy = (x, edge.node2.xy.y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+                elif dir in ('left-down', 'down'):
+                    r = range(edge.node1.xy.y + 1, edge.node2.xy.y)
+                    for y in r:
+                        xy = (edge.node1.xy.x, y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+                elif dir == 'up':
+                    r = range(edge.node2.xy.y + 1, edge.node1.xy.y)
+                    for y in r:
+                        xy = (edge.node1.xy.x, y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+            else:
+                if dir == 'right':
+                    r = range(edge.node1.xy.x + 1, edge.node2.xy.x)
+                    for x in r:
+                        xy = (x, edge.node1.xy.y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+                elif dir in ('left-down', 'down'):
+                    r = range(edge.node1.xy.y + 1, edge.node2.xy.y)
+                    for y in r:
+                        xy = (edge.node1.xy.x, y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+                elif dir == 'right-down':
+                    if self.diagram.edge_layout == 'flowchart':
+                        r = range(edge.node1.xy.x, edge.node2.xy.x)
+                        for x in r:
+                            xy = (x + 1, edge.node1.xy.y)
+                            nodes = [x for x in self.nodes if x.xy == xy]
+                            if len(nodes) > 0:
+                                edge.skipped = 1
+
+                    r = range(edge.node1.xy.y + 1, edge.node2.xy.y)
+                    for y in r:
+                        xy = (edge.node2.xy.x, y)
+                        nodes = [x for x in self.nodes if x.xy == xy]
+                        if len(nodes) > 0:
+                            edge.skipped = 1
+
+
+class ScreenNodeBuilder:
+    @classmethod
+    def build(cls, tree, layout=True):
+        DiagramNode.clear()
+        DiagramEdge.clear()
+        NodeGroup.clear()
+        Diagram.clear()
+
+        return cls(tree, layout).run()
+
+    def __init__(self, tree, layout):
+        self.diagram = DiagramTreeBuilder().build(tree)
+        self.layout = layout
+
+    def run(self):
+        if self.layout:
+            DiagramLayoutManager(self.diagram).run()
+            self.diagram.fixiate(True)
+
+        EdgeLayoutManager(self.diagram).run()
+
+        return self.diagram
+
+
+class SeparateDiagramBuilder(ScreenNodeBuilder):
+    @property
+    def _groups(self):
+        # Store nodes and edges of subgroups
+        nodes = {self.diagram: self.diagram.nodes}
+        edges = {self.diagram: self.diagram.edges}
+        levels = {self.diagram: self.diagram.level}
+        for group in self.diagram.traverse_groups():
+            nodes[group] = group.nodes
+            edges[group] = group.edges
+            levels[group] = group.level
+
+        groups = {}
+        orders = {}
+        for node in self.diagram.traverse_nodes():
+            groups[node] = node.group
+            orders[node] = node.order
+
+        for group in self.diagram.traverse_groups():
+            yield group
+
+            # Restore nodes, groups and edges
+            for g in nodes:
+                g.nodes = nodes[g]
+                g.edges = edges[g]
+                g.level = levels[g]
+
+            for n in groups:
+                n.group = groups[n]
+                n.order = orders[n]
+                n.xy = XY(0, 0)
+                n.colwidth = 1
+                n.colheight = 1
+                n.separated = False
+
+            for edge in DiagramEdge.find_all():
+                edge.skipped = False
+                edge.crosspoints = []
+
+        yield self.diagram
+
+    def _filter_edges(self, edges, parent, level):
+        filtered = {}
+        for e in edges:
+            if e.node1.group.is_parent(parent):
+                if e.node1.group.level > level:
+                    e = e.duplicate()
+                    if isinstance(e.node1, NodeGroup):
+                        e.node1 = e.node1.parent(level + 1)
+                    else:
+                        e.node1 = e.node1.group.parent(level + 1)
+            else:
+                continue
+
+            if e.node2.group.is_parent(parent):
+                if e.node2.group.level > level:
+                    e = e.duplicate()
+                    if isinstance(e.node2, NodeGroup):
+                        e.node2 = e.node2.parent(level + 1)
+                    else:
+                        e.node2 = e.node2.group.parent(level + 1)
+            else:
+                continue
+
+            filtered[(e.node1, e.node2)] = e
+
+        return filtered.values()
+
+    def run(self):
+        for i, group in enumerate(self._groups):
+            base = self.diagram.duplicate()
+            base.level = group.level - 1
+
+            # bind edges on base diagram (outer the group)
+            edges = DiagramEdge.find(None, group) + \
+                    DiagramEdge.find(group, None)
+            base.edges = self._filter_edges(edges, self.diagram, group.level)
+
+            # bind edges on target group (inner the group)
+            subgroups = group.traverse_groups()
+            edges = sum([g.edges for g in subgroups], group.edges)
+            group.edges = []
+            for e in self._filter_edges(edges, group, group.level):
+                if isinstance(e.node1, NodeGroup) and e.node1 == e.node2:
+                    pass
+                else:
+                    group.edges.append(e)
+
+            # clear subgroups in the group
+            for g in group.nodes:
+                if isinstance(g, NodeGroup):
+                    g.nodes = []
+                    g.edges = []
+                    g.separated = True
+
+            # pick up nodes to base diagram
+            nodes1 = [e.node1 for e in DiagramEdge.find(None, group)]
+            nodes1.sort(lambda x, y: cmp(x.order, y.order))
+            nodes2 = [e.node2 for e in DiagramEdge.find(group, None)]
+            nodes2.sort(lambda x, y: cmp(x.order, y.order))
+
+            nodes = nodes1 + [group] + nodes2
+            for i, n in enumerate(nodes):
+                n.order = i
+                if n not in base.nodes:
+                    base.nodes.append(n)
+                    n.group = base
+
+            if isinstance(group, Diagram):
+                base = group
+
+            DiagramLayoutManager(base).run()
+            base.fixiate(True)
+            EdgeLayoutManager(base).run()
+
+            yield base
diff --git a/src/blockdiag/command.py b/src/blockdiag/command.py
new file mode 100644
index 0000000..a8c5e00
--- /dev/null
+++ b/src/blockdiag/command.py
@@ -0,0 +1,64 @@
+# -*- 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.
+
+import re
+import sys
+import blockdiag
+import blockdiag.builder
+import blockdiag.drawer
+import blockdiag.parser
+from blockdiag.utils.bootstrap import Application, Options
+
+# for compatibility
+from blockdiag.utils.bootstrap import create_fontmap, detectfont
+
+
+class BlockdiagOptions(Options):
+    def build_parser(self):
+        super(BlockdiagOptions, self).build_parser()
+        self.parser.add_option(
+            '-s', '--separate', action='store_true',
+            help='Separate diagram images for each group (SVG only)'
+        )
+
+
+class BlockdiagApp(Application):
+    module = blockdiag
+
+    def parse_options(self):
+        self.options = BlockdiagOptions(self.module).parse()
+
+    def build_diagram(self, tree):
+        if not self.options.separate:
+            return super(BlockdiagApp, self).build_diagram(tree)
+        else:
+            DiagramBuilder = self.module.builder.SeparateDiagramBuilder
+            DiagramDraw = self.module.drawer.DiagramDraw
+
+            basename = re.sub('.svg$', '', self.options.output)
+            for i, group in enumerate(DiagramBuilder.build(tree)):
+                outfile = '%s_%d.svg' % (basename, i + 1)
+                draw = DiagramDraw(self.options.type, group, outfile,
+                                   fontmap=self.fontmap,
+                                   antialias=self.options.antialias,
+                                   nodoctype=self.options.nodoctype)
+                draw.draw()
+                draw.save()
+
+            return 0
+
+
+def main():
+    return BlockdiagApp().run()
diff --git a/src/blockdiag/drawer.py b/src/blockdiag/drawer.py
new file mode 100644
index 0000000..2ad6deb
--- /dev/null
+++ b/src/blockdiag/drawer.py
@@ -0,0 +1,191 @@
+# -*- 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.
+
+import noderenderer
+import imagedraw
+from metrics import AutoScaler
+from metrics import DiagramMetrics
+from imagedraw.filters.linejump import LineJumpDrawFilter
+
+
+class DiagramDraw(object):
+    MetricsClass = DiagramMetrics
+
+    @classmethod
+    def set_metrics_class(cls, MetricsClass):
+        cls.MetricsClass = MetricsClass
+
+    def __init__(self, format, diagram, filename=None, **kwargs):
+        self.format = format.upper()
+        self.diagram = diagram
+        self.fill = kwargs.get('fill', (0, 0, 0))
+        self.badgeFill = kwargs.get('badgeFill', 'pink')
+        self.filename = filename
+        self.scale_ratio = 1
+
+        basediagram = kwargs.get('basediagram', diagram)
+        self.metrics = self.MetricsClass(basediagram, **kwargs)
+
+        if self.format == 'PNG':
+            if kwargs.get('antialias'):
+                self.scale_ratio = ratio = 2
+                self.metrics = AutoScaler(self.metrics, scale_ratio=ratio)
+            self.shadow = kwargs.get('shadow', (64, 64, 64))
+        elif self.format == 'PDF':
+            self.shadow = kwargs.get('shadow', (144, 144, 144))
+        else:
+            self.shadow = kwargs.get('shadow', (0, 0, 0))
+
+        kwargs = dict(nodoctype=kwargs.get('nodoctype'),
+                      scale_ratio=self.scale_ratio)
+        drawer = imagedraw.create(self.format, self.filename,
+                                  self.pagesize(), **kwargs)
+        if drawer is None:
+            msg = 'failed to load %s image driver' % self.format
+            raise RuntimeError(msg)
+
+        self.drawer = LineJumpDrawFilter(drawer, self.metrics.cellsize / 2)
+
+    @property
+    def nodes(self):
+        for node in self.diagram.traverse_nodes():
+            if node.drawable:
+                yield node
+
+    @property
+    def groups(self):
+        for group in self.diagram.traverse_groups(preorder=True):
+            if not group.drawable:
+                yield group
+
+    @property
+    def edges(self):
+        for edge in (e for e in self.diagram.edges  if e.style != 'none'):
+            yield edge
+
+        for group in self.groups:
+            for edge in (e for e in group.edges  if e.style != 'none'):
+                yield edge
+
+    def pagesize(self, scaled=False):
+        if scaled:
+            metrics = self.metrics
+        else:
+            metrics = self.metrics.original_metrics
+
+        width = self.diagram.colwidth
+        height = self.diagram.colheight
+        return metrics.pagesize(width, height)
+
+    def draw(self, **kwargs):
+        # switch metrics object during draw backgrounds
+        temp, self.metrics = self.metrics, self.metrics.original_metrics
+        self._draw_background()
+        self.metrics = temp
+
+        # Smoothing background images.
+        if self.format == 'PNG':
+            self.drawer.smoothCanvas()
+
+        if self.scale_ratio > 1:
+            pagesize = self.pagesize(scaled=True)
+            self.drawer.resizeCanvas(pagesize)
+
+        self._draw_elements(**kwargs)
+
+    def _draw_background(self):
+        # Draw node groups.
+        for group in self.groups:
+            if group.shape == 'box':
+                box = self.metrics.group(group).marginbox
+                if group.href and self.format == 'SVG':
+                    drawer = self.drawer.anchor(group.href)
+                else:
+                    drawer = self.drawer
+
+                drawer.rectangle(box, fill=group.color, filter='blur')
+
+        # Drop node shadows.
+        for node in self.nodes:
+            if node.color != 'none':
+                r = noderenderer.get(node.shape)
+
+                shape = r(node, self.metrics)
+                if node.href and self.format == 'SVG':
+                    drawer = self.drawer.anchor(node.href)
+                else:
+                    drawer = self.drawer
+
+                shape.render(drawer, self.format,
+                             fill=self.shadow, shadow=True)
+
+    def _draw_elements(self, **kwargs):
+        for node in self.nodes:
+            self.node(node, **kwargs)
+
+        for edge in self.edges:
+            self.edge(edge)
+
+        for group in self.groups:
+            if group.shape == 'line':
+                box = self.metrics.group(group).marginbox
+                self.drawer.rectangle(box, fill='none', outline=group.color,
+                                      style=group.style, thick=group.thick)
+
+        for node in self.groups:
+            self.group_label(node, **kwargs)
+
+    def node(self, node, **kwargs):
+        r = noderenderer.get(node.shape)
+        shape = r(node, self.metrics)
+        if node.href and self.format == 'SVG':
+            drawer = self.drawer.anchor(node.href)
+        else:
+            drawer = self.drawer
+
+        shape.render(drawer, self.format, fill=self.fill,
+                     badgeFill=self.badgeFill)
+
+    def group_label(self, group):
+        m = self.metrics.group(group)
+        font = self.metrics.font_for(group)
+
+        if group.label and not group.separated:
+            self.drawer.textarea(m.grouplabelbox, group.label, font=font,
+                                 fill=group.textcolor)
+        elif group.label:
+            self.drawer.textarea(m.corebox, group.label, font=font,
+                                 fill=group.textcolor)
+
+    def edge(self, edge):
+        metrics = self.metrics.edge(edge)
+
+        for line in metrics.shaft.polylines:
+            self.drawer.line(line, fill=edge.color, thick=edge.thick,
+                             style=edge.style, jump=True)
+
+        for head in metrics.heads:
+            if edge.hstyle in ('generalization', 'aggregation'):
+                self.drawer.polygon(head, outline=edge.color, fill='white')
+            else:
+                self.drawer.polygon(head, outline=edge.color, fill=edge.color)
+
+        if edge.label:
+            font = self.metrics.font_for(edge)
+            self.drawer.textarea(metrics.labelbox, edge.label, font=font,
+                                 fill=edge.textcolor, outline=self.fill)
+
+    def save(self, size=None):
+        return self.drawer.save(self.filename, size, self.format)
diff --git a/src/blockdiag/elements.py b/src/blockdiag/elements.py
new file mode 100644
index 0000000..557c8f0
--- /dev/null
+++ b/src/blockdiag/elements.py
@@ -0,0 +1,630 @@
+# -*- 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.
+
+import os
+import re
+import sys
+import copy
+from utils import images, urlutil, uuid, XY
+import plugins
+import noderenderer
+
+
+def unquote(string):
+    """
+    Remove quotas from string
+
+    >>> unquote('"test"')
+    'test'
+    >>> unquote("'test'")
+    'test'
+    >>> unquote("'half quoted")
+    "'half quoted"
+    >>> unquote('"half quoted')
+    '"half quoted'
+    """
+    if string:
+        m = re.match('\A(?P<quote>"|\')((.|\s)*)(?P=quote)\Z', string, re.M)
+        if m:
+            return re.sub("\\\\" + m.group(1), m.group(1), m.group(2))
+        else:
+            return string
+    else:
+        return string
+
+
+class Base(object):
+    basecolor = (255, 255, 255)
+    textcolor = (0, 0, 0)
+    fontfamily = None
+    fontsize = None
+    style = None
+    int_attrs = ['colwidth', 'colheight', 'fontsize']
+
+    @classmethod
+    def set_default_color(cls, color):
+        cls.basecolor = images.color_to_rgb(color)
+
+    @classmethod
+    def set_default_text_color(cls, color):
+        cls.textcolor = images.color_to_rgb(color)
+
+    @classmethod
+    def set_default_fontfamily(cls, fontfamily):
+        cls.fontfamily = fontfamily
+
+    @classmethod
+    def set_default_fontsize(cls, fontsize):
+        cls.fontsize = int(fontsize)
+
+    @classmethod
+    def clear(cls):
+        cls.basecolor = (255, 255, 255)
+        cls.textcolor = (0, 0, 0)
+        cls.fontfamily = None
+        cls.fontsize = None
+
+    def duplicate(self):
+        return copy.copy(self)
+
+    def set_attribute(self, attr):
+        name = attr.name
+        value = unquote(attr.value)
+
+        if name == 'class':
+            if value in Diagram.classes:
+                klass = Diagram.classes[value]
+                self.set_attributes(klass.attrs)
+            else:
+                msg = "Unknown class: %s" % value
+                raise AttributeError(msg)
+        elif hasattr(self, "set_%s" % name):
+            getattr(self, "set_%s" % name)(value)
+        elif name in self.int_attrs:
+            setattr(self, name, int(value))
+        elif hasattr(self, name) and not callable(getattr(self, name)):
+            setattr(self, name, value)
+        else:
+            class_name = self.__class__.__name__
+            msg = "Unknown attribute: %s.%s" % (class_name, attr.name)
+            raise AttributeError(msg)
+
+    def set_attributes(self, attrs):
+        for attr in attrs:
+            self.set_attribute(attr)
+
+    def set_style(self, value):
+        if re.search('^(?:none|solid|dotted|dashed|\d+(,\d+)*)$', value, re.I):
+            self.style = value.lower()
+        else:
+            class_name = self.__class__.__name__
+            msg = "WARNING: unknown %s style: %s\n" % (class_name, value)
+            raise AttributeError(msg)
+
+
+class Element(Base):
+    namespace = {}
+    int_attrs = Base.int_attrs + ['width', 'height']
+
+    @classmethod
+    def get(cls, id):
+        if not id:
+            id = uuid.generate()
+
+        unquote_id = unquote(id)
+        if unquote_id not in cls.namespace:
+            obj = cls(id)
+            cls.namespace[unquote_id] = obj
+
+        return cls.namespace[unquote_id]
+
+    @classmethod
+    def clear(cls):
+        super(Element, cls).clear()
+        cls.namespace = {}
+        cls.basecolor = (255, 255, 255)
+        cls.textcolor = (0, 0, 0)
+
+    def __init__(self, id):
+        self.id = unquote(id)
+        self.label = ''
+        self.xy = XY(0, 0)
+        self.group = None
+        self.drawable = False
+        self.order = 0
+        self.color = self.basecolor
+        self.width = None
+        self.height = None
+        self.colwidth = 1
+        self.colheight = 1
+        self.stacked = False
+
+    def __repr__(self):
+        class_name = self.__class__.__name__
+        nodeid = self.id
+        xy = str(self.xy)
+        width = self.colwidth
+        height = self.colheight
+        addr = id(self)
+
+        format = "<%(class_name)s '%(nodeid)s' %(xy)s " + \
+                 "%(width)dx%(height)d at 0x%(addr)08x>"
+        return format % locals()
+
+    def set_color(self, color):
+        self.color = images.color_to_rgb(color)
+
+    def set_textcolor(self, color):
+        self.textcolor = images.color_to_rgb(color)
+
+
+class DiagramNode(Element):
+    shape = 'box'
+    int_attrs = Element.int_attrs + ['rotate']
+    linecolor = (0, 0, 0)
+    desctable = []
+    attrname = {}
+
+    @classmethod
+    def set_default_shape(cls, shape):
+        cls.shape = shape
+
+    @classmethod
+    def set_default_linecolor(cls, color):
+        cls.linecolor = images.color_to_rgb(color)
+
+    @classmethod
+    def clear(cls):
+        super(DiagramNode, cls).clear()
+        cls.shape = 'box'
+        cls.linecolor = (0, 0, 0)
+        cls.desctable = ['numbered', 'label', 'description']
+        cls.attrname = dict(numbered='No', label='Name',
+                            description='Description')
+
+    def __init__(self, id):
+        super(DiagramNode, self).__init__(id)
+
+        self.label = unquote(id) or ''
+        self.numbered = None
+        self.icon = None
+        self.background = None
+        self.description = None
+        self.rotate = 0
+        self.drawable = True
+        self.href = None
+
+        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)
+
+    def set_linecolor(self, color):
+        self.linecolor = images.color_to_rgb(color)
+
+    def set_shape(self, value):
+        try:
+            noderenderer.get(value)
+            self.shape = value
+        except:
+            msg = "WARNING: unknown node shape: %s\n" % 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)
+
+    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)
+
+    def set_stacked(self, value):
+        self.stacked = True
+
+    def to_desctable(self):
+        attrs = []
+        for name in self.desctable:
+            value = getattr(self, name)
+            if value is None:
+                attrs.append(u"")
+            else:
+                attrs.append(getattr(self, name))
+
+        return attrs
+
+
+class NodeGroup(Element):
+    basecolor = (243, 152, 0)
+
+    @classmethod
+    def clear(cls):
+        super(NodeGroup, cls).clear()
+        cls.basecolor = (243, 152, 0)
+
+    def __init__(self, id):
+        super(NodeGroup, self).__init__(id)
+
+        self.level = 0
+        self.separated = False
+        self.shape = 'box'
+        self.thick = 3
+        self.nodes = []
+        self.edges = []
+        self.icon = None
+        self.orientation = 'landscape'
+        self.href = None
+
+    def duplicate(self):
+        copied = super(NodeGroup, self).duplicate()
+        copied.nodes = []
+        copied.edges = []
+
+        return copied
+
+    def parent(self, level):
+        if self.level < level:
+            return None
+
+        group = self
+        while group.level != level:
+            group = group.group
+
+        return group
+
+    def is_parent(self, other):
+        parent = self.parent(other.level)
+        return parent == other
+
+    def traverse_nodes(self, preorder=False):
+        for node in self.nodes:
+            if isinstance(node, NodeGroup):
+                if preorder:
+                    yield node
+
+                for subnode in node.traverse_nodes(preorder=preorder):
+                    yield subnode
+
+                if not preorder:
+                    yield node
+            else:
+                yield node
+
+    def traverse_groups(self, preorder=False):
+        for node in self.traverse_nodes(preorder=preorder):
+            if isinstance(node, NodeGroup):
+                yield node
+
+    def fixiate(self, fixiate_nodes=False):
+        if self.separated:
+            self.colwidth = 1
+            self.colheight = 1
+
+            return
+        elif len(self.nodes) > 0:
+            self.colwidth = max(x.xy.x + x.colwidth for x in self.nodes)
+            self.colheight = max(x.xy.y + x.colheight for x in self.nodes)
+
+        for node in self.nodes:
+            if fixiate_nodes:
+                node.xy = XY(self.xy.x + node.xy.x,
+                             self.xy.y + node.xy.y)
+
+            if isinstance(node, NodeGroup):
+                node.fixiate(fixiate_nodes)
+
+    def update_order(self):
+        for i, node in enumerate(self.nodes):
+            node.order = i
+
+    def set_orientation(self, value):
+        value = value.lower()
+        if value in ('landscape', 'portrait'):
+            self.orientation = value
+        else:
+            msg = "WARNING: unknown diagram orientation: %s\n" % value
+            raise AttributeError(msg)
+
+    def set_shape(self, value):
+        value = value.lower()
+        if value in ('box', 'line'):
+            self.shape = value
+        else:
+            msg = "WARNING: unknown group shape: %s\n" % value
+            raise AttributeError(msg)
+
+
+class DiagramEdge(Base):
+    basecolor = (0, 0, 0)
+    namespace = {}
+
+    @classmethod
+    def get(cls, node1, node2):
+        if node1 not in cls.namespace:
+            cls.namespace[node1] = {}
+
+        if node2 not in cls.namespace[node1]:
+            obj = cls(node1, node2)
+            cls.namespace[node1][node2] = obj
+
+        return cls.namespace[node1][node2]
+
+    @classmethod
+    def find(cls, node1, node2=None):
+        if node1 is None and node2 is None:
+            return cls.find_all()
+        elif isinstance(node1, NodeGroup):
+            edges = cls.find(None, node2)
+            edges = (e for e in edges if e.node1.group.is_parent(node1))
+            return [e for e in edges if not e.node2.group.is_parent(node1)]
+        elif isinstance(node2, NodeGroup):
+            edges = cls.find(node1, None)
+            edges = (e for e in edges if e.node2.group.is_parent(node2))
+            return [e for e in edges if not e.node1.group.is_parent(node2)]
+        elif node1 is None:
+            return [e for e in cls.find_all() if e.node2 == node2]
+        else:
+            if node1 not in cls.namespace:
+                return []
+
+            if node2 is None:
+                return cls.namespace[node1].values()
+
+            if node2 not in cls.namespace[node1]:
+                return []
+
+        return cls.namespace[node1][node2]
+
+    @classmethod
+    def find_all(cls):
+        for v1 in cls.namespace.values():
+            for v2 in v1.values():
+                yield v2
+
+    @classmethod
+    def find_by_level(cls, level):
+        edges = []
+        for e in cls.find_all():
+            edge = e.duplicate()
+            skips = 0
+
+            if edge.node1.group.level < level:
+                skips += 1
+            else:
+                while edge.node1.group.level != level:
+                    edge.node1 = edge.node1.group
+
+            if edge.node2.group.level < level:
+                skips += 1
+            else:
+                while edge.node2.group.level != level:
+                    edge.node2 = edge.node2.group
+
+            if skips == 2:
+                continue
+
+            edges.append(edge)
+
+        return edges
+
+    @classmethod
+    def clear(cls):
+        super(DiagramEdge, cls).clear()
+        cls.namespace = {}
+        cls.basecolor = (0, 0, 0)
+
+    def __init__(self, node1, node2):
+        self.node1 = node1
+        self.node2 = node2
+        self.crosspoints = []
+        self.skipped = 0
+
+        self.label = None
+        self.dir = 'forward'
+        self.color = self.basecolor
+        self.hstyle = None
+        self.folded = None
+        self.thick = None
+
+    def __repr__(self):
+        class_name = self.__class__.__name__
+        node1_id = self.node1.id
+        node1_xy = self.node1.xy
+        node2_id = self.node2.id
+        node2_xy = self.node2.xy
+        addr = id(self)
+
+        format = "<%(class_name)s '%(node1_id)s' %(node1_xy)s - " + \
+                 "'%(node2_id)s' %(node2_xy)s, at 0x%(addr)08x>"
+        return format % locals()
+
+    def set_dir(self, value):
+        value = value.lower()
+        if value in ('back', 'both', 'none', 'forward'):
+            self.dir = value
+        elif value == '->':
+            self.dir = 'forward'
+        elif value == '<-':
+            self.dir = 'back'
+        elif value == '<->':
+            self.dir = 'both'
+        elif value == '--':
+            self.dir = 'none'
+        else:
+            msg = "WARNING: unknown edge dir: %s\n" % value
+            raise AttributeError(msg)
+
+    def set_color(self, color):
+        self.color = images.color_to_rgb(color)
+
+    def set_hstyle(self, value):
+        value = value.lower()
+        if value in ('generalization', 'composition', 'aggregation'):
+            self.hstyle = value
+        else:
+            msg = "WARNING: unknown edge hstyle: %s\n" % value
+            raise AttributeError(msg)
+
+    def set_folded(self, value):
+        self.folded = True
+
+    def set_nofolded(self, value):
+        self.folded = False
+
+    def set_thick(self, value):
+        self.thick = 3
+
+    @property
+    def direction(self):
+        node1 = self.node1.xy
+        node2 = self.node2.xy
+
+        if node1.x > node2.x:
+            if node1.y > node2.y:
+                dir = 'left-up'
+            elif node1.y == node2.y:
+                dir = 'left'
+            else:
+                dir = 'left-down'
+        elif node1.x == node2.x:
+            if node1.y > node2.y:
+                dir = 'up'
+            elif node1.y == node2.y:
+                dir = 'same'
+            else:
+                dir = 'down'
+        else:
+            if node1.y > node2.y:
+                dir = 'right-up'
+            elif node1.y == node2.y:
+                dir = 'right'
+            else:
+                dir = 'right-down'
+
+        return dir
+
+
+class Diagram(NodeGroup):
+    _DiagramNode = DiagramNode
+    _DiagramEdge = DiagramEdge
+    _NodeGroup = NodeGroup
+
+    classes = {}
+    linecolor = (0, 0, 0)
+    int_attrs = NodeGroup.int_attrs + \
+                ['node_width', 'node_height', 'span_width', 'span_height']
+
+    @classmethod
+    def clear(cls):
+        super(NodeGroup, cls).clear()
+        cls.linecolor = (0, 0, 0)
+        cls.classes = {}
+
+    def __init__(self):
+        super(Diagram, self).__init__(None)
+
+        self.node_width = None
+        self.node_height = None
+        self.span_width = None
+        self.span_height = None
+        self.page_padding = None
+        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)
+
+    def set_plugins(self, value):
+        modules = [name.strip() for name in value.split(',')]
+        plugins.load(modules, diagram=self)
+
+    def set_default_shape(self, value):
+        try:
+            noderenderer.get(value)
+            DiagramNode.set_default_shape(value)
+        except:
+            msg = "WARNING: unknown node shape: %s\n" % 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)
+        self.set_default_textcolor(color)
+
+    def set_default_textcolor(self, color):
+        self.textcolor = images.color_to_rgb(color)
+        self._DiagramNode.set_default_text_color(self.textcolor)
+        self._NodeGroup.set_default_text_color(self.textcolor)
+        self._DiagramEdge.set_default_text_color(self.textcolor)
+
+    def set_default_node_color(self, color):
+        color = images.color_to_rgb(color)
+        self._DiagramNode.set_default_color(color)
+
+    def set_default_line_color(self, color):
+        msg = "WARNING: default_line_color is obsoleted; " + \
+              "use default_linecolor\n"
+        sys.stderr.write(msg)
+        self.set_default_linecolor(color)
+
+    def set_default_linecolor(self, color):
+        self.linecolor = images.color_to_rgb(color)
+        self._DiagramNode.set_default_linecolor(self.linecolor)
+        self._DiagramEdge.set_default_color(self.linecolor)
+
+    def set_default_group_color(self, color):
+        color = images.color_to_rgb(color)
+        self._NodeGroup.set_default_color(color)
+
+    def set_shape_namespace(self, value):
+        noderenderer.set_default_namespace(value)
+
+    def set_default_fontfamily(self, fontfamily):
+        self._DiagramNode.set_default_fontfamily(fontfamily)
+        self._NodeGroup.set_default_fontfamily(fontfamily)
+        self._DiagramEdge.set_default_fontfamily(fontfamily)
+
+    def set_default_fontsize(self, fontsize):
+        self._DiagramNode.set_default_fontsize(fontsize)
+        self._NodeGroup.set_default_fontsize(fontsize)
+        self._DiagramEdge.set_default_fontsize(fontsize)
+
+    def set_edge_layout(self, value):
+        value = value.lower()
+        if value in ('normal', 'flowchart'):
+            msg = "WARNING: edge_layout is very experimental feature!\n"
+            sys.stderr.write(msg)
+
+            self.edge_layout = value
+        else:
+            msg = "WARNING: unknown edge layout: %s\n" % value
+            raise AttributeError(msg)
+
+    def set_fontsize(self, value):
+        msg = "WARNING: fontsize is obsoleted; use default_fontsize\n"
+        sys.stderr.write(msg)
+        self.set_default_fontsize(int(value))
diff --git a/src/blockdiag/imagedraw/__init__.py b/src/blockdiag/imagedraw/__init__.py
new file mode 100644
index 0000000..e921163
--- /dev/null
+++ b/src/blockdiag/imagedraw/__init__.py
@@ -0,0 +1,41 @@
+# -*- 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.
+
+drawers = {}
+
+try:
+    from png import ImageDrawEx
+    drawers['png'] = ImageDrawEx
+except ImportError:
+    pass
+
+try:
+    from svg import SVGImageDraw
+    drawers['svg'] = SVGImageDraw
+except RuntimeError:
+    pass
+
+try:
+    from pdf import PDFImageDraw
+    drawers['pdf'] = PDFImageDraw
+except ImportError:
+    pass
+
+
+def create(format, filename, size, **kwargs):
+    if format.lower() in drawers:
+        return drawers[format.lower()](filename, size, **kwargs)
+    else:
+        return None
diff --git a/src/blockdiag/imagedraw/filters/__init__.py b/src/blockdiag/imagedraw/filters/__init__.py
new file mode 100644
index 0000000..5c383c2
--- /dev/null
+++ b/src/blockdiag/imagedraw/filters/__init__.py
@@ -0,0 +1,14 @@
+# -*- 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.
diff --git a/src/blockdiag/imagedraw/filters/linejump.py b/src/blockdiag/imagedraw/filters/linejump.py
new file mode 100644
index 0000000..a9f6703
--- /dev/null
+++ b/src/blockdiag/imagedraw/filters/linejump.py
@@ -0,0 +1,158 @@
+# -*- 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 import XY
+
+
+class LazyReciever(object):
+    def __init__(self, target):
+        self.target = target
+        self.calls = []
+        self.nested = []
+
+    def __getattr__(self, name):
+        return self.get_lazy_method(name)
+
+    def get_lazy_method(self, name):
+        method = self._find_method(name)
+
+        def _(*args, **kwargs):
+            if name in self.target.self_generative_methods:
+                ret = method(self.target, *args, **kwargs)
+                reciever = LazyReciever(ret)
+                self.nested.append(reciever)
+                return reciever
+            else:
+                self.calls.append((method, args, kwargs))
+                return self
+
+        return _
+
+    def _find_method(self, name):
+        for p in self.target.__class__.__mro__:
+            if name in p.__dict__:
+                return p.__dict__[name]
+
+        raise AttributeError("%s instance has no attribute '%s'"
+            % (self.target.__class__.__name__, name))
+
+    def _run(self):
+        for recv in self.nested:
+            recv._run()
+
+        for method, args, kwargs in self.calls:
+            method(self.target, *args, **kwargs)
+
+
+class LineJumpDrawFilter(LazyReciever):
+    def __init__(self, target, jump_radius):
+        super(LineJumpDrawFilter, self).__init__(target)
+        self.ytree = []
+        self.x_cross = {}
+        self.y_cross = {}
+        self.forward = 'holizonal'
+        self.jump_radius = jump_radius
+        self.jump_shift = 0
+
+    def _run(self):
+        for recv in self.nested:
+            recv._run()
+
+        line_method = self._find_method("line")
+        for method, args, kwargs in self.calls:
+            if method == line_method and kwargs.get('jump'):
+                ((x1, y1), (x2, y2)) = args[0]
+                if self.forward == 'holizonal' and y1 == y2:
+                    self._holizonal_jumpline(x1, y1, x2, y2, **kwargs)
+                    continue
+                elif self.forward == 'vertical' and x1 == x2:
+                    self._vertical_jumpline(x1, y1, x2, y2, **kwargs)
+                    continue
+
+            method(self.target, *args, **kwargs)
+
+    def _holizonal_jumpline(self, x1, y1, x2, y2, **kwargs):
+        y = y1
+        if x2 < x1:
+            x1, x2 = x2, x1
+
+        for x in sorted(self.x_cross.get(y, [])):
+            if x1 < x and x < x2:
+                arckwargs = dict(kwargs)
+                del arckwargs['jump']
+
+                x += self.jump_shift
+                r = self.jump_radius
+                line = (XY(x1, y), XY(x - r, y))
+                self.target.line(line, **kwargs)
+                box = (x - r, y - r, x + r, y + r)
+                self.target.arc(box, 180, 0, **arckwargs)
+                x1 = x + r
+
+        self.target.line((XY(x1, y), XY(x2, y)), **kwargs)
+
+    def _vertical_jumpline(self, x1, y1, x2, y2, **kwargs):
+        x = x1
+        if y2 < y1:
+            y1, y2 = y2, y1
+
+        for y in sorted(self.y_cross.get(x, [])):
+            if y1 < y and y < y2:
+                arckwargs = dict(kwargs)
+                del arckwargs['jump']
+
+                y += self.jump_shift
+                r = self.jump_radius
+                line = (XY(x, y1), XY(x, y - r))
+                self.target.line(line, **kwargs)
+                box = (x - r, y - r, x + r, y + r)
+                self.target.arc(box, 270, 90, **arckwargs)
+                y1 = y + r
+
+        self.target.line((XY(x, y1), XY(x, y2)), **kwargs)
+
+    def line(self, xy, **kwargs):
+        from bisect import insort
+        for st, ed in zip(xy[:-1], xy[1:]):
+            self.get_lazy_method("line")((st, ed), **kwargs)
+
+            if 'jump' in kwargs and kwargs['jump'] == True:
+                if st.y == ed.y:    # horizonal
+                    insort(self.ytree, (st.y, 0, (st, ed)))
+                elif st.x == ed.x:  # vertical
+                    insort(self.ytree, (max(st.y, ed.y), -1, (st, ed)))
+                    insort(self.ytree, (min(st.y, ed.y), +1, (st, ed)))
+
+    def save(self, *args, **kwargs):
+        # Search crosspoints
+        from bisect import insort, bisect_left, bisect_right
+        xtree = []
+        for y, _, ((x1, y1), (x2, y2)) in self.ytree:
+            if x2 < x1:
+                x1, x2 = x2, x1
+            if y2 < y1:
+                y1, y2 = y2, y1
+
+            if y == y1:
+                insort(xtree, x1)
+
+            if y == y2:
+                del xtree[bisect_left(xtree, x1)]
+                for x in xtree[bisect_right(xtree, x1):bisect_left(xtree, x2)]:
+                    self.x_cross.setdefault(y, set()).add(x)
+                    self.y_cross.setdefault(x, set()).add(y)
+
+        self._run()
+        return self.target.save(*args, **kwargs)
diff --git a/src/blockdiag/imagedraw/pdf.py b/src/blockdiag/imagedraw/pdf.py
new file mode 100644
index 0000000..b6d7856
--- /dev/null
+++ b/src/blockdiag/imagedraw/pdf.py
@@ -0,0 +1,216 @@
+# -*- 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.
+
+import re
+import sys
+from reportlab.pdfgen import canvas
+from reportlab.pdfbase import pdfmetrics
+from reportlab.pdfbase.ttfonts import TTFont
+from blockdiag.utils import urlutil, Box
+from blockdiag.utils.fontmap import parse_fontpath
+from blockdiag.utils.PDFTextFolder import PDFTextFolder as TextFolder
+
+
+class PDFImageDraw(object):
+    self_generative_methods = []
+
+    def __init__(self, filename, size, **kwargs):
+        self.canvas = canvas.Canvas(filename, pagesize=size)
+        self.size = size
+        self.fonts = {}
+
+    def set_font(self, font):
+        if font.path is None:
+            msg = "Could not detect fonts, use --font opiton\n"
+            raise RuntimeError(msg)
+
+        if font.path not in self.fonts:
+            path, index = parse_fontpath(font.path)
+            if index:
+                ttfont = TTFont(font.path, path, subfontIndex=index)
+            else:
+                ttfont = TTFont(font.path, path)
+            pdfmetrics.registerFont(ttfont)
+
+            self.fonts[font.path] = ttfont
+
+        self.canvas.setFont(font.path, font.size)
+
+    def set_render_params(self, **kwargs):
+        self.set_stroke_color(kwargs.get('outline'))
+        self.set_fill_color(kwargs.get('fill', 'none'))
+        self.set_style(kwargs.get('style'), kwargs.get('thick'))
+
+        params = {}
+        if kwargs.get('fill', 'none') == 'none':
+            params['fill'] = 0
+        else:
+            params['fill'] = 1
+
+        if kwargs.get('outline') is None:
+            params['stroke'] = 0
+        else:
+            params['stroke'] = 1
+
+        return params
+
+    def set_style(self, style, thick):
+        if thick is None:
+            thick = 1
+
+        if style == 'dotted':
+            self.canvas.setDash([2 * thick, 2 * thick])
+        elif style == 'dashed':
+            self.canvas.setDash([4 * thick, 4 * thick])
+        elif style == 'none':
+            self.canvas.setDash([0, 65535 * thick])
+        elif re.search('^\d+(,\d+)*$', style or ""):
+            self.canvas.setDash([int(n) * thick for n in style.split(',')])
+        else:
+            self.canvas.setDash()
+
+    def set_stroke_color(self, color="black"):
+        if isinstance(color, basestring):
+            self.canvas.setStrokeColor(color)
+        elif color:
+            rgb = (color[0] / 256.0, color[1] / 256.0, color[2] / 256.0)
+            self.canvas.setStrokeColorRGB(*rgb)
+        else:
+            self.set_stroke_color()
+
+    def set_fill_color(self, color="white"):
+        if isinstance(color, basestring):
+            if color != 'none':
+                self.canvas.setFillColor(color)
+        elif color:
+            rgb = (color[0] / 256.0, color[1] / 256.0, color[2] / 256.0)
+            self.canvas.setFillColorRGB(*rgb)
+        else:
+            self.set_fill_color()
+
+    def path(self, pd, **kwargs):
+        params = self.set_render_params(**kwargs)
+        self.canvas.drawPath(pd, **params)
+
+    def rectangle(self, box, **kwargs):
+        x = box[0]
+        y = self.size[1] - box[3]
+        width = box[2] - box[0]
+        height = box[3] - box[1]
+
+        if 'thick' in kwargs and kwargs['thick'] is not None:
+            self.canvas.setLineWidth(kwargs['thick'])
+
+        params = self.set_render_params(**kwargs)
+        self.canvas.rect(x, y, width, height, **params)
+
+        if 'thick' in kwargs:
+            self.canvas.setLineWidth(1)
+
+    def text(self, xy, string, font, **kwargs):
+        self.set_font(font)
+        self.set_fill_color(kwargs.get('fill'))
+        self.canvas.drawString(xy[0], self.size[1] - xy[1], string)
+
+    def textarea(self, box, string, font, **kwargs):
+        self.canvas.saveState()
+
+        if 'rotate' in kwargs and kwargs['rotate'] != 0:
+            angle = 360 - int(kwargs['rotate']) % 360
+            self.canvas.rotate(angle)
+
+            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)
+            elif angle == 180:
+                box = Box(-box.x2, -box.y2, -box.x1, -box.y2 + box.height)
+                box = box.shift(y=self.size.y * 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)
+
+        self.set_font(font)
+        lines = TextFolder(box, string, font, adjustBaseline=True,
+                           canvas=self.canvas, **kwargs)
+
+        if kwargs.get('outline'):
+            outline = kwargs.get('outline')
+            self.rectangle(lines.outlinebox, fill='white', outline=outline)
+
+        for string, xy in lines.lines:
+            self.text(xy, string, font, **kwargs)
+        self.canvas.restoreState()
+
+    def line(self, xy, **kwargs):
+        self.set_stroke_color(kwargs.get('fill', 'none'))
+        self.set_style(kwargs.get('style'), kwargs.get('thick'))
+
+        if 'thick' in kwargs and kwargs['thick'] is not None:
+            self.canvas.setLineWidth(kwargs['thick'])
+
+        p1 = xy[0]
+        y = self.size[1]
+        for p2 in xy[1:]:
+            self.canvas.line(p1.x, y - p1.y, p2.x, y - p2.y)
+            p1 = p2
+
+        if 'thick' in kwargs:
+            self.canvas.setLineWidth(1)
+
+    def arc(self, xy, start, end, **kwargs):
+        start, end = 360 - end, 360 - start
+        r = (360 + end - start) % 360
+
+        params = self.set_render_params(**kwargs)
+        y = self.size[1]
+        self.canvas.arc(xy[0], y - xy[3], xy[2], y - xy[1], start, r)
+
+    def ellipse(self, xy, **kwargs):
+        params = self.set_render_params(**kwargs)
+        y = self.size[1]
+        self.canvas.ellipse(xy[0], y - xy[3], xy[2], y - xy[1], **params)
+
+    def polygon(self, xy, **kwargs):
+        pd = self.canvas.beginPath()
+        y = self.size[1]
+        pd.moveTo(xy[0][0], y - xy[0][1])
+        for p in xy[1:]:
+            pd.lineTo(p[0], y - p[1])
+
+        params = self.set_render_params(**kwargs)
+        self.canvas.drawPath(pd, **params)
+
+    def loadImage(self, filename, box):
+        x = box[0]
+        y = self.size[1] - box[3]
+        w = box[2] - box[0]
+        h = box[3] - box[1]
+
+        if urlutil.isurl(filename):
+            from reportlab.lib.utils import ImageReader
+            try:
+                filename = ImageReader(filename)
+            except:
+                msg = "WARNING: Could not retrieve: %s\n" % filename
+                sys.stderr.write(msg)
+                return
+        self.canvas.drawImage(filename, x, y, w, h, mask='auto',
+                                  preserveAspectRatio=True)
+
+    def save(self, filename, size, format):
+        # Ignore size and format parameter; compatibility for ImageDrawEx.
+
+        self.canvas.showPage()
+        self.canvas.save()
diff --git a/src/blockdiag/imagedraw/png.py b/src/blockdiag/imagedraw/png.py
new file mode 100644
index 0000000..c87dd35
--- /dev/null
+++ b/src/blockdiag/imagedraw/png.py
@@ -0,0 +1,375 @@
+# -*- 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.
+
+import re
+import math
+from itertools import izip, tee
+from blockdiag.utils import ellipse, urlutil, Box
+from blockdiag.utils.fontmap import parse_fontpath
+from blockdiag.utils.myitertools import istep, stepslice
+from blockdiag.utils.PILTextFolder import PILTextFolder as TextFolder
+try:
+    from PIL import Image
+    from PIL import ImageDraw
+    from PIL import ImageFont
+    from PIL import ImageFilter
+except ImportError:
+    import Image
+    import ImageDraw
+    import ImageFont
+    import ImageFilter
+
+
+def point_pairs(xylist):
+    iterable = iter(xylist)
+    for pt in iterable:
+        if isinstance(pt, int):
+            yield (pt, iterable.next())
+        else:
+            yield pt
+
+
+def line_segments(xylist):
+    p1, p2 = tee(point_pairs(xylist))
+    p2.next()
+    return izip(p1, p2)
+
+
+def dashize_line(line, length):
+    pt1, pt2 = line
+    if pt1[0] == pt2[0]:  # holizonal
+        if pt1[1] > pt2[1]:
+            pt2, pt1 = line
+
+        r = stepslice(xrange(pt1[1], pt2[1]), length)
+        for y1, y2 in istep(n for n in r):
+            yield [(pt1[0], y1), (pt1[0], y2)]
+
+    elif pt1[1] == pt2[1]:  # vertical
+        if pt1[0] > pt2[0]:
+            pt2, pt1 = line
+
+        r = stepslice(xrange(pt1[0], pt2[0]), length)
+        for x1, x2 in istep(n for n in r):
+            yield [(x1, pt1[1]), (x2, pt1[1])]
+    else:  # diagonal
+        if pt1[0] > pt2[0]:
+            pt2, pt1 = line
+
+        # DDA (Digital Differential Analyzer) Algorithm
+        locus = []
+        m = float(pt2[1] - pt1[1]) / float(pt2[0] - pt1[0])
+        x = pt1[0]
+        y = pt1[1]
+
+        while x <= pt2[0]:
+            locus.append((int(x), int(round(y))))
+            x += 1
+            y += m
+
+        for p1, p2 in istep(stepslice(locus, length)):
+            yield (p1, p2)
+
+
+def style2cycle(style, thick):
+    if thick is None:
+        thick = 1
+
+    if style == 'dotted':
+        length = [2 * thick, 2 * thick]
+    elif style == 'dashed':
+        length = [4 * thick, 4 * thick]
+    elif style == 'none':
+        length = [0, 65535 * thick]
+    elif re.search('^\d+(,\d+)*$', style or ""):
+        length = [int(n) * thick for n in style.split(',')]
+    else:
+        length = None
+
+    return length
+
+
+class ImageDrawEx(object):
+    self_generative_methods = []
+
+    def __init__(self, filename, size, **kwargs):
+        if kwargs.get('im'):
+            self.image = kwargs.get('im')
+        else:
+            self.image = Image.new('RGB', size, (256, 256, 256))
+
+            # set transparency to background
+            alpha = Image.new('L', size, 1)
+            self.image.putalpha(alpha)
+
+        self.filename = filename
+        self.scale_ratio = kwargs.get('scale_ratio', 1)
+        self.mode = kwargs.get('mode')
+        self.draw = ImageDraw.ImageDraw(self.image, self.mode)
+
+        if 'parent' in kwargs:
+            parent = kwargs['parent']
+            self.scale_ratio = parent.scale_ratio
+
+    def resizeCanvas(self, size):
+        self.image = self.image.resize(size, Image.ANTIALIAS)
+        self.draw = ImageDraw.ImageDraw(self.image, self.mode)
+
+    def smoothCanvas(self):
+        for i in range(15):
+            self.image = self.image.filter(ImageFilter.SMOOTH_MORE)
+
+        self.draw = ImageDraw.ImageDraw(self.image, self.mode)
+
+    def arc(self, box, start, end, **kwargs):
+        style = kwargs.get('style')
+        if 'style' in kwargs:
+            del kwargs['style']
+        if 'thick' in kwargs:
+            del kwargs['thick']
+
+        if style:
+            while start > end:
+                end += 360
+
+            cycle = style2cycle(style, kwargs.get('width'))
+            for pt in ellipse.dots(box, cycle, start, end):
+                self.draw.line([pt, pt], fill=kwargs['fill'])
+        else:
+            self.draw.arc(box, start, end, **kwargs)
+
+    def ellipse(self, box, **kwargs):
+        if 'filter' in kwargs:
+            del kwargs['filter']
+
+        style = kwargs.get('style')
+        if 'style' in kwargs:
+            del kwargs['style']
+
+        if style:
+            if kwargs.get('fill') != 'none':
+                kwargs2 = dict(kwargs)
+                if 'outline' in kwargs2:
+                    del kwargs2['outline']
+                self.draw.ellipse(box, **kwargs2)
+
+            if 'outline' in kwargs:
+                kwargs['fill'] = kwargs['outline']
+                del kwargs['outline']
+
+            cycle = style2cycle(style, kwargs.get('width'))
+            for pt in ellipse.dots(box, cycle):
+                self.draw.line([pt, pt], fill=kwargs['fill'])
+        else:
+            if kwargs.get('fill') == 'none':
+                del kwargs['fill']
+
+            self.draw.ellipse(box, **kwargs)
+
+    def line(self, xy, **kwargs):
+        if 'jump' in kwargs:
+            del kwargs['jump']
+        if 'thick' in kwargs:
+            if kwargs['thick'] is not None:
+                kwargs['width'] = kwargs['thick']
+            del kwargs['thick']
+
+        style = kwargs.get('style')
+        if kwargs.get('fill') == 'none':
+            pass
+        elif style in ('dotted', 'dashed', 'none') or \
+             re.search('^\d+(,\d+)*$', style or ""):
+            self.dashed_line(xy, **kwargs)
+        else:
+            if 'style' in kwargs:
+                del kwargs['style']
+
+            self.draw.line(xy, **kwargs)
+
+    def dashed_line(self, xy, **kwargs):
+        style = kwargs.get('style')
+        del kwargs['style']
+
+        cycle = style2cycle(style, kwargs.get('width'))
+        for line in line_segments(xy):
+            for subline in dashize_line(line, cycle):
+                self.line(subline, **kwargs)
+
+    def rectangle(self, box, **kwargs):
+        thick = kwargs.get('thick', self.scale_ratio)
+        fill = kwargs.get('fill')
+        outline = kwargs.get('outline')
+        style = kwargs.get('style')
+
+        if thick == 1:
+            d = 0
+        else:
+            d = int(math.ceil(thick / 2.0))
+
+        if fill and fill != 'none':
+            self.draw.rectangle(box, fill=fill)
+
+        x1, y1, x2, y2 = box
+        lines = (((x1, y1), (x2, y1)), ((x1, y2), (x2, y2)),  # horizonal
+                 ((x1, y1 - d), (x1, y2 + d)),  # vettical (left)
+                 ((x2, y1 - d), (x2, y2 + d)))  # vertical (right)
+
+        for line in lines:
+            self.line(line, fill=outline, width=thick, style=style)
+
+    def polygon(self, xy, **kwargs):
+        if 'filter' in kwargs:
+            del kwargs['filter']
+
+        if kwargs.get('fill') != 'none':
+            kwargs2 = dict(kwargs)
+
+            if 'style' in kwargs2:
+                del kwargs2['style']
+            if 'outline' in kwargs2:
+                del kwargs2['outline']
+            self.draw.polygon(xy, **kwargs2)
+
+        if kwargs.get('outline'):
+            kwargs['fill'] = kwargs['outline']
+            del kwargs['outline']
+            self.line(xy, **kwargs)
+
+    def text(self, xy, string, font, **kwargs):
+        fill = kwargs.get('fill')
+
+        if font.path:
+            path, index = parse_fontpath(font.path)
+            if index:
+                ttfont = ImageFont.truetype(path, font.size, index=index)
+            else:
+                ttfont = ImageFont.truetype(path, font.size)
+        else:
+            ttfont = None
+
+        if ttfont is None:
+            if self.scale_ratio == 1:
+                self.draw.text(xy, string, fill=fill)
+            else:
+                size = self.draw.textsize(string)
+                image = Image.new('RGBA', size)
+                draw = ImageDraw.Draw(image)
+                draw.text((0, 0), string, fill=fill)
+                del draw
+
+                basesize = (size[0] * self.scale_ratio,
+                            size[1] * self.scale_ratio)
+                text_image = image.resize(basesize, Image.ANTIALIAS)
+
+                self.image.paste(text_image, xy, text_image)
+        else:
+            size = self.draw.textsize(string, font=ttfont)
+
+            # Generate mask to support BDF(bitmap font)
+            mask = Image.new('1', size)
+            draw = ImageDraw.Draw(mask)
+            draw.text((0, 0), string, fill='white', font=ttfont)
+
+            # Rendering text
+            filler = Image.new('RGB', size, fill)
+            self.image.paste(filler, xy, mask)
+
+            self.draw = ImageDraw.ImageDraw(self.image, self.mode)
+
+    def textarea(self, box, string, font, **kwargs):
+        if 'rotate' in kwargs and kwargs['rotate'] != 0:
+            angle = 360 - int(kwargs['rotate']) % 360
+            del kwargs['rotate']
+
+            if angle in (90, 270):
+                _box = Box(0, 0, box.height, box.width)
+            else:
+                _box = box
+
+            text = ImageDrawEx(None, _box.size, parent=self, mode=self.mode)
+            textbox = (0, 0, _box.width, _box.height)
+            text.textarea(textbox, string, font, **kwargs)
+
+            filter = Image.new('RGB', box.size, kwargs.get('fill'))
+            self.image.paste(filter, box.topleft, text.image.rotate(angle))
+            self.draw = ImageDraw.ImageDraw(self.image, self.mode)
+            return
+
+        lines = TextFolder(box, string, font, scale=self.scale_ratio, **kwargs)
+
+        if kwargs.get('outline'):
+            outline = kwargs.get('outline')
+            self.rectangle(lines.outlinebox, fill='white', outline=outline)
+
+        for string, xy in lines.lines:
+            self.text(xy, string, font, **kwargs)
+
+    def loadImage(self, filename, box):
+        box_width = box[2] - box[0]
+        box_height = box[3] - box[1]
+
+        if urlutil.isurl(filename):
+            import cStringIO
+            import urllib
+            try:
+                filename = cStringIO.StringIO(urllib.urlopen(filename).read())
+            except:
+                import sys
+                msg = "WARNING: Could not retrieve: %s\n" % filename
+                sys.stderr.write(msg)
+                return
+        image = Image.open(filename)
+
+        # 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]
+
+        self.image.paste(image, (x, y))
+        self.draw = ImageDraw.ImageDraw(self.image, self.mode)
+
+    def save(self, filename, size, format):
+        if filename:
+            self.filename = filename
+
+        if size is None:
+            x = int(self.image.size[0] / self.scale_ratio)
+            y = int(self.image.size[1] / self.scale_ratio)
+            size = (x, y)
+
+        self.image.thumbnail(size, Image.ANTIALIAS)
+
+        if self.filename:
+            self.image.save(self.filename, format)
+            image = None
+        else:
+            import cStringIO
+            tmp = cStringIO.StringIO()
+            self.image.save(tmp, format)
+            image = tmp.getvalue()
+
+        return image
diff --git a/src/blockdiag/imagedraw/simplesvg.py b/src/blockdiag/imagedraw/simplesvg.py
new file mode 100644
index 0000000..ade3943
--- /dev/null
+++ b/src/blockdiag/imagedraw/simplesvg.py
@@ -0,0 +1,231 @@
+# -*- 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.
+
+import re
+import cStringIO
+
+
+def _escape(s):
+    if not isinstance(s, (str, unicode)):
+        s = str(s)
+    return s.replace("&", "&").replace("<", "<").replace(">", ">")
+
+
+def _quote(s):
+    return '"%s"' % _escape(s).replace('"', """)
+
+
+class base(object):
+    def __init__(self, *args, **kwargs):
+        self.text = None
+        self.elements = []
+        self.attributes = {}
+        for key, value in kwargs.items():
+            self.add_attribute(key, value)
+
+    def add_attribute(self, key, value):
+        setter = 'set_%s' % key
+        if hasattr(self, setter):
+            getattr(self, setter)(value)
+        else:
+            key = re.sub('_', '-', key)
+            self.attributes[key] = value
+
+    def addElement(self, element):
+        self.elements.append(element)
+
+    def set_text(self, text):
+        self.text = text
+
+    def to_xml(self, io, level=0):
+        clsname = self.__class__.__name__
+        indent = '  ' * level
+
+        io.write('%s<%s' % (indent, clsname))
+        for key in sorted(self.attributes):
+            value = self.attributes[key]
+            if value is not None:
+                io.write(' %s=%s' % (_escape(key), _quote(value)))
+
+        if self.elements == [] and self.text is None:
+            io.write(" />\n")
+        elif self.text is not None:
+            text = _escape(self.text).encode('utf-8')
+            io.write(">%s</%s>\n" % (text, clsname))
+        elif self.elements:
+            io.write(">\n")
+            for e in self.elements:
+                e.to_xml(io, level + 1)
+            io.write('%s</%s>\n' % (indent, clsname))
+
+
+class element(base):
+    def __init__(self, x, y, width=None, height=None, *args, **kwargs):
+        super(element, self).__init__(*args, **kwargs)
+        self.attributes['x'] = x
+        self.attributes['y'] = y
+        if width is not None:
+            self.attributes['width'] = width
+        if height is not None:
+            self.attributes['height'] = height
+
+
+class svg(base):
+    def __init__(self, x, y, width, height):
+        viewbox = "%d %d %d %d" % (x, y, width, height)
+        super(svg, self).__init__(viewBox=viewbox)
+
+        self.use_doctype = True
+        self.add_attribute('xmlns', 'http://www.w3.org/2000/svg')
+
+    def to_xml(self):
+        io = cStringIO.StringIO()
+
+        if self.use_doctype:
+            url = "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"
+            io.write("<?xml version='1.0' encoding='UTF-8'?>\n")
+            io.write('<!DOCTYPE svg PUBLIC '
+                     '"-//W3C//DTD SVG 1.0//EN" "%s">\n' % url)
+
+        super(svg, self).to_xml(io)
+
+        return io.getvalue()
+
+
+class title(base):
+    def __init__(self, _title):
+        super(title, self).__init__(text=_title)
+
+
+class text(element):
+    def __init__(self, x, y, _text, **kwargs):
+        super(text, self).__init__(x, y, text=_text, **kwargs)
+
+
+class rect(element):
+    pass
+
+
+class ellipse(base):
+    def __init__(self, cx, cy, rx, ry, **kwargs):
+        super(ellipse, self).__init__(cx=cx, cy=cy, rx=rx, ry=ry, **kwargs)
+
+
+class image(element):
+    def __init__(self, uri, x, y, width, height, **kwargs):
+        super(image, self).__init__(x, y, width, height, **kwargs)
+        self.add_attribute('xlink:href', uri)
+
+
+class polygon(base):
+    def __init__(self, points, **kwargs):
+        xylist = " ".join('%d,%d' % pt for pt in points)
+        super(polygon, self).__init__(points=xylist, **kwargs)
+
+
+class path(base):
+    def __init__(self, data, **kwargs):
+        super(path, self).__init__(d=data, **kwargs)
+
+
+class pathdata:
+    def __init__(self, x=None, y=None):
+        self.path = []
+        if x is not None and y is not None:
+            self.move(x, y)
+
+    def closepath(self):
+        self.path.append('z')
+
+    def move(self, x, y):
+        self.path.append('M %s %s' % (x, y))
+
+    def relmove(self, x, y):
+        self.path.append('m %s %s' % (x, y))
+
+    def line(self, x, y):
+        self.path.append('L %s %s' % (x, y))
+
+    def relline(self, x, y):
+        self.path.append('l %s %s' % (x, y))
+
+    def hline(self, x):
+        self.path.append('H%s' % (x,))
+
+    def relhline(self, x):
+        self.path.append('h%s %s' % (x,))
+
+    def vline(self, y):
+        self.path.append('V%s' % (y,))
+
+    def relvline(self, y):
+        self.path.append('v%s' % (y,))
+
+    def bezier(self, x1, y1, x2, y2, x, y):
+        self.path.append('C%s,%s %s,%s %s,%s' % (x1, y1, x2, y2, x, y))
+
+    def relbezier(self, x1, y1, x2, y2, x, y):
+        self.path.append('c%s,%s %s,%s %s,%s' % (x1, y1, x2, y2, x, y))
+
+    def smbezier(self, x2, y2, x, y):
+        self.path.append('S%s,%s %s,%s' % (x2, y2, x, y))
+
+    def relsmbezier(self, x2, y2, x, y):
+        self.path.append('s%s,%s %s,%s' % (x2, y2, x, y))
+
+    def qbezier(self, x1, y1, x, y):
+        self.path.append('Q%s,%s %s,%s' % (x1, y1, x, y))
+
+    def qrelbezier(self, x1, y1, x, y):
+        self.path.append('q%s,%s %s,%s' % (x1, y1, x, y))
+
+    def smqbezier(self, x, y):
+        self.path.append('T%s %s' % (x, y))
+
+    def relsmqbezier(self, x, y):
+        self.path.append('t%s %s' % (x, y))
+
+    def ellarc(self, rx, ry, xrot, laf, sf, x, y):
+        self.path.append('A%s,%s %s %s %s %s %s' % \
+                         (rx, ry, xrot, laf, sf, x, y))
+
+    def relellarc(self, rx, ry, xrot, laf, sf, x, y):
+        self.path.append('a%s,%s %s %s %s %s %s' % \
+                         (rx, ry, xrot, laf, sf, x, y))
+
+    def __repr__(self):
+        return ' '.join(self.path)
+
+
+class defs(base):
+    pass
+
+
+class g(base):
+    pass
+
+
+class a(base):
+    pass
+
+
+class filter(element):
+    def __init__(self, x, y, width, height, **kwargs):
+        super(filter, self).__init__(x, y, width, height, **kwargs)
+
+
+def svgclass(name):
+    """ svg class generating function """
+    return type(name, (base,), {})
diff --git a/src/blockdiag/imagedraw/svg.py b/src/blockdiag/imagedraw/svg.py
new file mode 100644
index 0000000..86c608d
--- /dev/null
+++ b/src/blockdiag/imagedraw/svg.py
@@ -0,0 +1,286 @@
+# -*- 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.
+
+import re
+import base64
+from blockdiag.utils import urlutil, Box, XY
+from simplesvg import *
+
+try:
+    from blockdiag.utils.PILTextFolder import PILTextFolder as TextFolder
+except ImportError:
+    from blockdiag.utils.TextFolder import TextFolder
+
+feGaussianBlur = svgclass('feGaussianBlur')
+
+
+class SVGImageDrawElement(object):
+    self_generative_methods = ['group', 'anchor']
+
+    def __init__(self, svg, parent=None):
+        self.svg = svg
+
+    def rgb(self, color):
+        if isinstance(color, tuple):
+            color = 'rgb(%d,%d,%d)' % color
+
+        return color
+
+    def filter(self, name):
+        if name == 'blur':
+            filter = "filter:url(#filter_blur)"
+        elif name == 'transp-blur':
+            filter = "filter:url(#filter_blur);opacity:0.7;fill-opacity:1"
+        else:
+            filter = None
+
+        return filter
+
+    def style(self, name, thick):
+        if thick is None:
+            thick = 1
+
+        if name == 'dotted':
+            length = 2 * thick
+        elif name == 'dashed':
+            length = 4 * thick
+        elif name == 'none':
+            length = "%d %d" % (0, 65535 * thick)
+        elif re.search('^\d+(,\d+)*$', name or ""):
+            l = [int(n) * thick for n in name.split(",")]
+            length = " ".join(str(n) for n in l)
+        else:
+            length = None
+
+        return length
+
+    def path(self, pd, **kwargs):
+        thick = kwargs.get('thick')
+        fill = kwargs.get('fill')
+        outline = kwargs.get('outline')
+        style = kwargs.get('style')
+        filter = kwargs.get('filter')
+
+        p = path(pd, fill=self.rgb(fill), stroke=self.rgb(outline),
+                 stroke_dasharray=self.style(style, thick),
+                 style=self.filter(filter))
+        self.svg.addElement(p)
+
+    def rectangle(self, box, **kwargs):
+        thick = kwargs.get('thick')
+        fill = kwargs.get('fill', 'none')
+        outline = kwargs.get('outline')
+        style = kwargs.get('style')
+        filter = kwargs.get('filter')
+
+        x = box[0]
+        y = box[1]
+        width = box[2] - box[0]
+        height = box[3] - box[1]
+
+        r = rect(x, y, width, height, fill=self.rgb(fill),
+                 stroke=self.rgb(outline), stroke_width=thick,
+                 stroke_dasharray=self.style(style, thick),
+                 style=self.filter(filter))
+        self.svg.addElement(r)
+
+    def text(self, xy, string, font, **kwargs):
+        fill = kwargs.get('fill')
+
+        t = text(xy[0], xy[1], string, fill=self.rgb(fill),
+                 font_family=font.generic_family, font_size=font.size,
+                 font_weight=font.weight, font_style=font.style)
+        self.svg.addElement(t)
+
+    def textarea(self, box, string, font, **kwargs):
+        if 'rotate' in kwargs and kwargs['rotate'] != 0:
+            angle = int(kwargs['rotate']) % 360
+            del kwargs['rotate']
+
+            if angle in (90, 270):
+                _box = Box(box[0], box[1],
+                           box[0] + box.height, box[1] + box.width)
+                if angle == 90:
+                    _box = _box.shift(x=box.width)
+                elif angle == 270:
+                    _box = _box.shift(y=box.height)
+            elif angle == 180:
+                _box = Box(box[2], box[3],
+                           box[2] + box.width, box[3] + box.height)
+            else:
+                _box = Box(box[0], box[1],
+                           box[0] + box.width, box[1] + box.height)
+
+            rotate = "rotate(%d,%d,%d)" % (angle, _box[0], _box[1])
+            group = g(transform="%s" % rotate)
+            self.svg.addElement(group)
+
+            elem = SVGImageDrawElement(group, self)
+            elem.textarea(_box, string, font, **kwargs)
+            return
+
+        lines = TextFolder(box, string, font, adjustBaseline=True, **kwargs)
+
+        if kwargs.get('outline'):
+            outline = kwargs.get('outline')
+            self.rectangle(lines.outlinebox, fill='white', outline=outline)
+
+        for string, xy in lines.lines:
+            self.text(xy, string, font, **kwargs)
+
+    def line(self, xy, **kwargs):
+        fill = kwargs.get('fill')
+        style = kwargs.get('style')
+        thick = kwargs.get('thick')
+
+        pd = pathdata(xy[0].x, xy[0].y)
+        for pt in xy[1:]:
+            pd.line(pt.x, pt.y)
+
+        p = path(pd, fill="none", stroke=self.rgb(fill),
+                 stroke_width=thick, stroke_dasharray=self.style(style, thick))
+        self.svg.addElement(p)
+
+    def arc(self, xy, start, end, **kwargs):
+        thick = kwargs.get('thick')
+        fill = kwargs.get('fill')
+        style = kwargs.get('style')
+
+        w = (xy[2] - xy[0]) / 2
+        h = (xy[3] - xy[1]) / 2
+
+        if start > end:
+            end += 360
+
+        from blockdiag.utils import ellipse
+
+        coord = ellipse.coordinate(1, w, h, start, start + 1)
+        point = iter(coord).next()
+        pt1 = XY(xy[0] + w + round(point[0], 0),
+                 xy[1] + h + round(point[1], 0))
+
+        coord = ellipse.coordinate(1, w, h, end, end + 1)
+        point = iter(coord).next()
+        pt2 = XY(xy[0] + w + round(point[0], 0),
+                 xy[1] + h + round(point[1], 0))
+
+        if end - start > 180:
+            largearc = 1
+        else:
+            largearc = 0
+
+        pd = pathdata(pt1[0], pt1[1])
+        pd.ellarc(w, h, 0, largearc, 1, pt2[0], pt2[1])
+        p = path(pd, fill="none", stroke=self.rgb(fill),
+                 stroke_dasharray=self.style(style, thick))
+        self.svg.addElement(p)
+
+    def ellipse(self, xy, **kwargs):
+        thick = kwargs.get('thick')
+        fill = kwargs.get('fill')
+        outline = kwargs.get('outline')
+        style = kwargs.get('style')
+        filter = kwargs.get('filter')
+
+        w = (xy[2] - xy[0]) / 2
+        h = (xy[3] - xy[1]) / 2
+        pt = XY(xy[0] + w, xy[1] + h)
+
+        e = ellipse(pt.x, pt.y, w, h, fill=self.rgb(fill),
+                    stroke=self.rgb(outline),
+                    stroke_dasharray=self.style(style, thick),
+                    style=self.filter(filter))
+        self.svg.addElement(e)
+
+    def polygon(self, xy, **kwargs):
+        thick = kwargs.get('thick')
+        fill = kwargs.get('fill')
+        outline = kwargs.get('outline')
+        style = kwargs.get('style')
+        filter = kwargs.get('filter')
+
+        pg = polygon(xy, fill=self.rgb(fill), stroke=self.rgb(outline),
+                     stroke_dasharray=self.style(style, thick),
+                     style=self.filter(filter))
+        self.svg.addElement(pg)
+
+    def loadImage(self, filename, box):
+        if urlutil.isurl(filename):
+            url = filename
+        else:
+            string = open(filename).read()
+            url = "data:;base64," + base64.b64encode(string)
+
+        x = box[0]
+        y = box[1]
+        w = box[2] - box[0]
+        h = box[3] - box[1]
+
+        im = image(url, x, y, w, h)
+        self.svg.addElement(im)
+
+    def anchor(self, url):
+        a_node = a(url)
+        a_node.add_attribute('xlink:href', url)
+        self.svg.addElement(a_node)
+
+        return SVGImageDrawElement(a_node)
+
+    def group(self):
+        group = g()
+        self.svg.addElement(group)
+
+        return SVGImageDrawElement(group)
+
+
+class SVGImageDraw(SVGImageDrawElement):
+    def __init__(self, filename, size, **kwargs):
+        self.filename = filename
+        super(SVGImageDraw, self).__init__(svg(0, 0, size[0], size[1]))
+        self.svg.use_doctype = not kwargs.get('nodoctype')
+
+        uri = 'http://www.inkscape.org/namespaces/inkscape'
+        self.svg.add_attribute('xmlns:inkspace', uri)
+        uri = 'http://www.w3.org/1999/xlink'
+        self.svg.add_attribute('xmlns:xlink', uri)
+
+        # inkspace's Gaussian filter
+        fgb = feGaussianBlur(id='feGaussianBlur3780', stdDeviation=4.2)
+        fgb.add_attribute('inkspace:collect', 'always')
+        f = filter(-0.07875, -0.252, 1.1575, 1.504, id='filter_blur')
+        f.add_attribute('inkspace:collect', 'always')
+        f.addElement(fgb)
+        d = defs(id='defs_block')
+        d.addElement(f)
+        self.svg.addElement(d)
+
+        self.svg.addElement(title('blockdiag'))
+
+    def save(self, filename, size, format):
+        # Ignore format parameter; compatibility for ImageDrawEx.
+
+        if filename:
+            self.filename = filename
+
+        if size:
+            self.svg.attributes['width'] = size[0]
+            self.svg.attributes['height'] = size[1]
+
+        svg = self.svg.to_xml()
+
+        if self.filename:
+            open(self.filename, 'w').write(svg)
+
+        return svg
diff --git a/src/blockdiag/metrics.py b/src/blockdiag/metrics.py
new file mode 100644
index 0000000..8f5d86b
--- /dev/null
+++ b/src/blockdiag/metrics.py
@@ -0,0 +1,974 @@
+# -*- 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.
+
+import copy
+from elements import DiagramNode
+import noderenderer
+from utils import Box, Size, XY
+from utils.fontmap import FontInfo, FontMap
+from utils.collections import defaultdict, namedtuple
+
+cellsize = 8
+
+
+class EdgeLines(object):
+    def __init__(self):
+        self.xy = None
+        self.stroking = False
+        self.polylines = []
+
+    def moveTo(self, x, y=None):
+        self.stroking = False
+        if y is None:
+            self.xy = x
+        else:
+            self.xy = XY(x, y)
+
+    def lineTo(self, x, y=None):
+        if y is None:
+            elem = x
+        else:
+            elem = XY(x, y)
+
+        if self.stroking == False:
+            self.stroking = True
+            polyline = []
+            if self.xy:
+                polyline.append(self.xy)
+            self.polylines.append(polyline)
+
+        if len(self.polylines[-1]) > 0:
+            if self.polylines[-1][-1] == elem:
+                return
+
+        self.polylines[-1].append(elem)
+
+    def lines(self):
+        lines = []
+        for line in self.polylines:
+            start = line[0]
+            for elem in list(line[1:]):
+                lines.append((start, elem))
+                start = elem
+
+        return lines
+
+
+class AutoScaler(object):
+    def __init__(self, subject, scale_ratio):
+        self.subject = subject
+        self.scale_ratio = scale_ratio
+
+    def __getattr__(self, name):
+        ratio = self.scale_ratio
+        attr = getattr(self.subject, name)
+
+        if not callable(attr):
+            return self.scale(attr, ratio)
+        else:
+            def _(*args, **kwargs):
+                ret = attr(*args, **kwargs)
+                return self.scale(ret, ratio)
+
+            return _
+
+    @classmethod
+    def scale(cls, value, ratio):
+        if ratio == 1:
+            return value
+
+        klass = value.__class__
+        if klass == XY:
+            ret = XY(value.x * ratio, value.y * ratio)
+        elif klass == Size:
+            ret = Size(value.width * ratio, value.height * ratio)
+        elif klass == Box:
+            ret = Box(value[0] * ratio, value[1] * ratio,
+                      value[2] * ratio, value[3] * ratio)
+        elif klass == tuple:
+            ret = tuple([cls.scale(x, ratio) for x in value])
+        elif klass == list:
+            ret = [cls.scale(x, ratio) for x in value]
+        elif klass == EdgeLines:
+            ret = EdgeLines()
+            ret.polylines = cls.scale(value.polylines, ratio)
+        elif klass == FontInfo:
+            ret = FontInfo(value.familyname, value.path, value.size * ratio)
+        elif klass == int:
+            ret = value * ratio
+        elif klass == str:
+            ret = value
+        else:
+            ret = cls(value, ratio)
+
+        return ret
+
+    @property
+    def original_metrics(self):
+        return self.subject
+
+
+class DiagramMetrics(object):
+    cellsize = cellsize
+    edge_layout = 'normal'
+    node_padding = 4
+    line_spacing = 2
+    shadow_offset = XY(3, 6)
+    font = None
+    page_margin = XY(0, 0)
+    page_padding = [0, 0, 0, 0]
+    node_width = cellsize * 16
+    node_height = cellsize * 5
+    span_width = cellsize * 8
+    span_height = cellsize * 5
+
+    def __init__(self, diagram, **kwargs):
+        self.format = kwargs.get('format')
+
+        if diagram.node_width is not None:
+            self.node_width = diagram.node_width
+
+        if diagram.node_height is not None:
+            self.node_height = diagram.node_height
+
+        if diagram.span_width is not None:
+            self.span_width = diagram.span_width
+
+        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:
+            self.fontmap = FontMap()
+
+        if diagram.page_padding is not None:
+            self.page_padding = diagram.page_padding
+
+        if diagram.edge_layout is not None:
+            self.edge_layout = diagram.edge_layout
+
+        # setup spreadsheet
+        sheet = self.spreadsheet = SpreadSheetMetrics(self)
+        nodes = [n for n in diagram.traverse_nodes() if n.drawable]
+
+        node_width = self.node_width
+        for x in range(diagram.colwidth):
+            widths = [n.width for n in nodes if n.xy.x == x]
+            if widths:
+                width = max(n or node_width for n in widths)
+                sheet.set_node_width(x, width)
+
+        node_height = self.node_height
+        for y in range(diagram.colheight):
+            heights = [n.height for n in nodes if n.xy.y == y]
+            if heights:
+                height = max(n or node_height for n in heights)
+                sheet.set_node_height(y, height)
+
+    @property
+    def original_metrics(self):
+        return self
+
+    def shift(self, x, y):
+        metrics = copy.copy(self)
+        metrics.spreadsheet = copy.copy(self.spreadsheet)
+        metrics.spreadsheet.metrics = metrics
+        metrics.page_margin = XY(x, y)
+
+        return metrics
+
+    def textsize(self, string, width=65535, font=None):
+        try:
+            if self.font is None:
+                from utils.TextFolder import TextFolder
+            elif self.format == 'PDF':
+                from utils.PDFTextFolder import PDFTextFolder as TextFolder
+            else:
+                from utils.PILTextFolder import PILTextFolder as TextFolder
+        except ImportError:
+            from utils.TextFolder import TextFolder
+
+        font = font or self.font
+        lines = TextFolder((0, 0, width, 65535), string, font)
+        textbox = lines.outlinebox
+        return XY(textbox.width, textbox.height + self.line_spacing)
+
+    def node(self, node):
+        renderer = noderenderer.get(node.shape)
+
+        if hasattr(renderer, 'render'):
+            return renderer(node, self)
+        else:
+            return self.cell(node)
+
+    def cell(self, node, use_padding=True):
+        return self.spreadsheet.node(node, use_padding)
+
+    def group(self, group):
+        return self.spreadsheet.node(group, use_padding=False)
+
+    def edge(self, edge):
+        if self.edge_layout == 'flowchart':
+            if edge.node1.group.orientation == 'landscape':
+                return FlowchartLandscapeEdgeMetrics(edge, self)
+            else:
+                return FlowchartPortraitEdgeMetrics(edge, self)
+        else:
+            if edge.node1.group.orientation == 'landscape':
+                return LandscapeEdgeMetrics(edge, self)
+            else:
+                return PortraitEdgeMetrics(edge, self)
+
+    def font_for(self, element):
+        return self.fontmap.find(element)
+
+    def pagesize(self, width, height):
+        return self.spreadsheet.pagesize(width, height)
+
+
+class SpreadSheetMetrics(object):
+    def __init__(self, metrics):
+        self.metrics = metrics
+        self.node_width = defaultdict(lambda: metrics.node_width)
+        self.node_height = defaultdict(lambda: metrics.node_height)
+        self.span_width = defaultdict(lambda: metrics.span_width)
+        self.span_height = defaultdict(lambda: metrics.span_height)
+
+    def set_node_width(self, x, width):
+        if width is not None and 0 < width and \
+           (x not in self.node_width or self.node_width[x] < width):
+            self.node_width[x] = width
+
+    def set_node_height(self, y, height):
+        if height is not None and 0 < height and \
+           (y not in self.node_height or self.node_height[y] < height):
+            self.node_height[y] = height
+
+    def set_span_width(self, x, width):
+        if width is not None and 0 < width and \
+           (x not in self.span_width or self.span_width[x] < width):
+            self.span_width[x] = width
+
+    def set_span_height(self, y, height):
+        if height is not None and 0 < height and \
+           (y not in self.span_height or self.span_height[y] < height):
+            self.span_height[y] = height
+
+    def node(self, node, use_padding=True):
+        x, y = node.xy
+        x1, y1 = self._node_topleft(node, use_padding)
+        x2, y2 = self._node_bottomright(node, use_padding)
+
+        return NodeMetrics(self.metrics, x1, y1, x2, y2)
+
+    def _node_topleft(self, node, use_padding=True):
+        m = self.metrics
+        x, y = node.xy
+        margin = m.page_margin
+        padding = m.page_padding
+
+        node_width = sum(self.node_width[i] for i in range(x))
+        node_height = sum(self.node_height[i] for i in range(y))
+        span_width = sum(self.span_width[i] for i in range(x + 1))
+        span_height = sum(self.span_height[i] for i in range(y + 1))
+
+        if use_padding:
+            xdiff = (self.node_width[x] - (node.width or m.node_width)) / 2
+            if xdiff < 0:
+                xdiff = 0
+
+            ydiff = (self.node_height[y] - (node.height or m.node_height)) / 2
+            if ydiff < 0:
+                ydiff = 0
+        else:
+            xdiff = 0
+            ydiff = 0
+
+        x1 = margin.x + padding[3] + node_width + span_width + xdiff
+        y1 = margin.y + padding[0] + node_height + span_height + ydiff
+
+        return XY(x1, y1)
+
+    def _node_bottomright(self, node, use_padding=True):
+        m = self.metrics
+        x = node.xy.x + node.colwidth - 1
+        y = node.xy.y + node.colheight - 1
+        margin = m.page_margin
+        padding = m.page_padding
+
+        node_width = sum(self.node_width[i] for i in range(x + 1))
+        node_height = sum(self.node_height[i] for i in range(y + 1))
+        span_width = sum(self.span_width[i] for i in range(x + 1))
+        span_height = sum(self.span_height[i] for i in range(y + 1))
+
+        if use_padding:
+            xdiff = (self.node_width[x] - (node.width or m.node_width)) / 2
+            if xdiff < 0:
+                xdiff = 0
+
+            ydiff = (self.node_height[y] - (node.height or m.node_height)) / 2
+            if ydiff < 0:
+                ydiff = 0
+        else:
+            xdiff = 0
+            ydiff = 0
+
+        x2 = margin.x + padding[3] + node_width + span_width - xdiff
+        y2 = margin.y + padding[0] + node_height + span_height - ydiff
+
+        return XY(x2, y2)
+
+    def pagesize(self, width, height):
+        margin = self.metrics.page_margin
+        padding = self.metrics.page_padding
+
+        dummy = DiagramNode(None)
+        dummy.xy = XY(width - 1, height - 1)
+        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)
+
+
+class NodeMetrics(Box):
+    def __init__(self, metrics, x1, y1, x2, y2):
+        self.metrics = metrics
+        super(NodeMetrics, self).__init__(x1, y1, x2, y2)
+
+    @property
+    def box(self):
+        return Box(self.x1, self.y1, self.x2, self.y2)
+
+    @property
+    def marginbox(self):
+        return Box(self.x1 - self.metrics.span_width / 8,
+                   self.y1 - self.metrics.span_height / 4,
+                   self.x2 + self.metrics.span_width / 8,
+                   self.y2 + self.metrics.span_height / 4)
+
+    @property
+    def corebox(self):
+        return Box(self.x1 + self.metrics.node_padding,
+                   self.y1 + self.metrics.node_padding,
+                   self.x2 - self.metrics.node_padding * 2,
+                   self.y2 - self.metrics.node_padding * 2)
+
+    @property
+    def grouplabelbox(self):
+        return Box(self.x1, self.y1 - self.metrics.span_height / 2,
+                   self.x2, self.y1)
+
+
+class EdgeMetrics(object):
+    def __init__(self, edge, metrics):
+        self.metrics = metrics
+        self.edge = edge
+
+    @property
+    def heads(self):
+        heads = []
+        head1, head2 = self.headshapes
+
+        if head1:
+            heads.append(self._head(self.edge.node1, head1))
+
+        if head2:
+            heads.append(self._head(self.edge.node2, head2))
+
+        return heads
+
+    def _head(self, node, direct):
+        head = []
+        cell = self.metrics.cellsize
+        node = self.metrics.node(node)
+
+        if direct == 'up':
+            xy = node.bottom
+            head.append(XY(xy.x, xy.y + 1))
+            head.append(XY(xy.x - cell / 2, xy.y + cell))
+            head.append(XY(xy.x, xy.y + cell * 2))
+            head.append(XY(xy.x + cell / 2, xy.y + cell))
+            head.append(XY(xy.x, xy.y + 1))
+        elif direct == 'down':
+            xy = node.top
+            head.append(XY(xy.x, xy.y - 1))
+            head.append(XY(xy.x - cell / 2, xy.y - cell))
+            head.append(XY(xy.x, xy.y - cell * 2))
+            head.append(XY(xy.x + cell / 2, xy.y - cell))
+            head.append(XY(xy.x, xy.y - 1))
+        elif direct == 'right':
+            xy = node.left
+            head.append(XY(xy.x - 1, xy.y))
+            head.append(XY(xy.x - cell, xy.y - cell / 2))
+            head.append(XY(xy.x - cell * 2, xy.y))
+            head.append(XY(xy.x - cell, xy.y + cell / 2))
+            head.append(XY(xy.x - 1, xy.y))
+        elif direct == 'left':
+            xy = node.right
+            head.append(XY(xy.x + 1, xy.y))
+            head.append(XY(xy.x + cell, xy.y - cell / 2))
+            head.append(XY(xy.x + cell * 2, xy.y))
+            head.append(XY(xy.x + cell, xy.y + cell / 2))
+            head.append(XY(xy.x + 1, xy.y))
+
+        if self.edge.hstyle not in ('composition', 'aggregation'):
+            head.pop(2)
+
+        return head
+
+    @property
+    def shaft(self):
+        cell = self.metrics.cellsize
+        lines = self._shaft
+        head1, head2 = self.headshapes
+
+        if head1:
+            pt = lines.polylines[0].pop(0)
+            if head1 == 'up':
+                lines.polylines[0].insert(0, XY(pt.x, pt.y + cell))
+            elif head1 == 'right':
+                lines.polylines[0].insert(0, XY(pt.x - cell, pt.y))
+            elif head1 == 'left':
+                lines.polylines[0].insert(0, XY(pt.x + cell, pt.y))
+            elif head1 == 'down':
+                lines.polylines[0].insert(0, XY(pt.x, pt.y - cell))
+
+        if head2:
+            pt = lines.polylines[-1].pop()
+            if head2 == 'up':
+                lines.polylines[-1].append(XY(pt.x, pt.y + cell))
+            elif head2 == 'right':
+                lines.polylines[-1].append(XY(pt.x - cell, pt.y))
+            elif head2 == 'left':
+                lines.polylines[-1].append(XY(pt.x + cell, pt.y))
+            elif head2 == 'down':
+                lines.polylines[-1].append(XY(pt.x, pt.y - cell))
+
+        return lines
+
+    @property
+    def labelbox(self):
+        pass
+
+
+class LandscapeEdgeMetrics(EdgeMetrics):
+    @property
+    def headshapes(self):
+        heads = []
+        dir = self.edge.direction
+
+        if self.edge.dir in ('back', 'both'):
+            if dir in ('left-up', 'left', 'same',
+                       'right-up', 'right', 'right-down'):
+                heads.append('left')
+            elif dir == 'up':
+                if self.edge.skipped:
+                    heads.append('left')
+                else:
+                    heads.append('down')
+            elif dir in ('left-down', 'down'):
+                if self.edge.skipped:
+                    heads.append('left')
+                else:
+                    heads.append('up')
+        else:
+            heads.append(None)
+
+        if self.edge.dir in ('forward', 'both'):
+            if dir in ('right-up', 'right', 'right-down'):
+                heads.append('right')
+            elif dir == 'up':
+                heads.append('up')
+            elif dir in ('left-up', 'left', 'left-down', 'down', 'same'):
+                heads.append('down')
+        else:
+            heads.append(None)
+
+        return heads
+
+    @property
+    def _shaft(self):
+        span = XY(self.metrics.span_width, self.metrics.span_height)
+        dir = self.edge.direction
+
+        node1 = self.metrics.node(self.edge.node1)
+        cell1 = self.metrics.cell(self.edge.node1, use_padding=False)
+        node2 = self.metrics.node(self.edge.node2)
+        cell2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+        shaft = EdgeLines()
+        if dir == 'right':
+            shaft.moveTo(node1.right)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.right.x + span.x / 2, cell1.right.y)
+                shaft.lineTo(cell1.right.x + span.x / 2,
+                             cell1.bottomright.y + span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4,
+                             cell2.bottomright.y + span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4, cell2.left.y)
+
+            shaft.lineTo(node2.left)
+
+        elif dir == 'right-up':
+            shaft.moveTo(node1.right)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.right.x + span.x / 2, cell1.right.y)
+                shaft.lineTo(cell1.right.x + span.x / 2,
+                             cell2.bottomleft.y + span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4,
+                             cell2.bottomleft.y + span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4, cell2.left.y)
+            else:
+                shaft.lineTo(cell2.left.x - span.x / 4, cell1.right.y)
+                shaft.lineTo(cell2.left.x - span.x / 4, cell2.left.y)
+
+            shaft.lineTo(node2.left)
+
+        elif dir == 'right-down':
+            shaft.moveTo(node1.right)
+            shaft.lineTo(cell1.right.x + span.x / 2, cell1.right.y)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.right.x + span.x / 2,
+                             cell2.topleft.y - span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4,
+                             cell2.topleft.y - span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4, cell2.left.y)
+            else:
+                shaft.lineTo(cell1.right.x + span.x / 2, cell2.left.y)
+
+            shaft.lineTo(node2.left)
+
+        elif dir == 'up':
+            if self.edge.skipped:
+                shaft.moveTo(node1.right)
+                shaft.lineTo(cell1.right.x + span.x / 4, cell1.right.y)
+                shaft.lineTo(cell1.right.x + span.x / 4,
+                             cell2.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.bottom.x, cell2.bottom.y + span.y / 2)
+            else:
+                shaft.moveTo(node1.top)
+
+            shaft.lineTo(node2.bottom)
+
+        elif dir in ('left-up', 'left', 'same'):
+            shaft.moveTo(node1.right)
+            shaft.lineTo(cell1.right.x + span.x / 4, cell1.right.y)
+            shaft.lineTo(cell1.right.x + span.x / 4,
+                         cell2.top.y - span.y / 2 + span.y / 8)
+            shaft.lineTo(cell2.top.x,
+                         cell2.top.y - span.y / 2 + span.y / 8)
+            shaft.lineTo(node2.top)
+
+        elif dir == 'left-down':
+            if self.edge.skipped:
+                shaft.moveTo(node1.right)
+                shaft.lineTo(cell1.right.x + span.x / 2, cell1.right.y)
+                shaft.lineTo(cell1.right.x + span.x / 2,
+                             cell2.top.y - span.y / 2)
+                shaft.lineTo(cell2.top.x, cell2.top.y - span.y / 2)
+            else:
+                shaft.moveTo(node1.bottom)
+                shaft.lineTo(cell1.bottom.x,
+                             cell2.top.y - span.y / 2)
+                shaft.lineTo(cell2.top.x, cell2.top.y - span.y / 2)
+
+            shaft.lineTo(node2.top)
+
+        elif dir == 'down':
+            if self.edge.skipped:
+                shaft.moveTo(node1.right)
+                shaft.lineTo(cell1.right.x + span.x / 2, cell1.right.y)
+                shaft.lineTo(cell1.right.x + span.x / 2,
+                             cell2.top.y - span.y / 2 + span.y / 8)
+                shaft.lineTo(cell2.top.x,
+                             cell2.top.y - span.y / 2 + span.y / 8)
+            else:
+                shaft.moveTo(node1.bottom)
+
+            shaft.lineTo(node2.top)
+
+        return shaft
+
+    @property
+    def labelbox(self):
+        span = XY(self.metrics.span_width, self.metrics.span_height)
+        node = XY(self.metrics.node_width, self.metrics.node_height)
+
+        dir = self.edge.direction
+        node1 = self.metrics.cell(self.edge.node1, use_padding=False)
+        node2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+        if dir == 'right':
+            if self.edge.skipped:
+                box = Box(node1.bottomright.x + span.x,
+                          node1.bottomright.y,
+                          node2.bottomleft.x - span.x,
+                          node2.bottomleft.y + span.y / 2)
+            else:
+                box = Box(node1.topright.x, node1.topright.y - span.y / 8,
+                          node2.left.x, node2.left.y - span.y / 8)
+
+        elif dir == 'right-up':
+            box = Box(node2.left.x - span.x, node1.top.y - node.y / 2,
+                      node2.bottomleft.x, node1.top.y)
+
+        elif dir == 'right-down':
+            box = Box(node1.right.x, node2.topleft.y - span.y / 8,
+                      node1.right.x + span.x, node2.left.y - span.y / 8)
+
+        elif dir in ('up', 'left-up', 'left', 'same'):
+            if self.edge.node2.xy.y < self.edge.node1.xy.y:
+                box = Box(node1.topright.x - span.x / 2 + span.x / 4,
+                          node1.topright.y - span.y / 2,
+                          node1.topright.x + span.x / 2 + span.x / 4,
+                          node1.topright.y)
+            else:
+                box = Box(node1.top.x + span.x / 4,
+                          node1.top.y - span.y,
+                          node1.topright.x + span.x / 4,
+                          node1.topright.y - span.y / 2)
+
+        elif dir in ('left-down', 'down'):
+            box = Box(node2.top.x + span.x / 4,
+                      node2.top.y - span.y,
+                      node2.topright.x + span.x / 4,
+                      node2.topright.y - span.y / 2)
+
+        # shrink box
+        box = Box(box[0] + span.x / 8, box[1],
+                  box[2] - span.x / 8, box[3])
+
+        return box
+
+
+class PortraitEdgeMetrics(EdgeMetrics):
+    @property
+    def headshapes(self):
+        heads = []
+        dir = self.edge.direction
+
+        if self.edge.dir in ('back', 'both'):
+            if dir == 'right':
+                if self.edge.skipped:
+                    heads.append('up')
+                else:
+                    heads.append('left')
+            elif dir in ('up', 'right-up', 'same'):
+                heads.append('up')
+            elif dir in ('left-up', 'left'):
+                heads.append('left')
+            elif dir in ('left-down', 'down', 'right-down'):
+                if self.edge.skipped:
+                    heads.append('left')
+                else:
+                    heads.append('up')
+        else:
+            heads.append(None)
+
+        if self.edge.dir in ('forward', 'both'):
+            if dir == 'right':
+                if self.edge.skipped:
+                    heads.append('down')
+                else:
+                    heads.append('right')
+            elif dir in ('up', 'right-up', 'same'):
+                heads.append('down')
+            elif dir in ('left-up', 'left', 'left-down', 'down', 'right-down'):
+                heads.append('down')
+        else:
+            heads.append(None)
+
+        return heads
+
+    @property
+    def _shaft(self):
+        span = XY(self.metrics.span_width, self.metrics.span_height)
+        dir = self.edge.direction
+
+        node1 = self.metrics.node(self.edge.node1)
+        cell1 = self.metrics.cell(self.edge.node1, use_padding=False)
+        node2 = self.metrics.node(self.edge.node2)
+        cell2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+        shaft = EdgeLines()
+        if dir in ('up', 'right-up', 'same', 'right'):
+            if dir == 'right' and not self.edge.skipped:
+                shaft.moveTo(node1.right)
+                shaft.lineTo(node2.left)
+            else:
+                shaft.moveTo(node1.bottom)
+                shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.right.x + span.x / 4,
+                             cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.right.x + span.x / 4,
+                             cell2.top.y - span.y / 2 + span.y / 8)
+                shaft.lineTo(cell2.top.x,
+                             cell2.top.y - span.y / 2 + span.y / 8)
+                shaft.lineTo(node2.top)
+
+        elif dir == 'right-down':
+            shaft.moveTo(node1.bottom)
+            shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y / 2)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell2.left.x - span.x / 2,
+                             cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.topleft.x - span.x / 2,
+                             cell2.topleft.y - span.y / 2)
+                shaft.lineTo(cell2.top.x, cell2.top.y - span.y / 2)
+            else:
+                shaft.lineTo(cell2.top.x, cell1.bottom.y + span.y / 2)
+
+            shaft.lineTo(node2.top)
+
+        elif dir in ('left-up', 'left', 'same'):
+            shaft.moveTo(node1.right)
+            shaft.lineTo(cell1.right.x + span.x / 4, cell1.right.y)
+            shaft.lineTo(cell1.right.x + span.x / 4,
+                         cell2.top.y - span.y / 2 + span.y / 8)
+            shaft.lineTo(cell2.top.x,
+                         cell2.top.y - span.y / 2 + span.y / 8)
+            shaft.lineTo(node2.top)
+
+        elif dir == 'left-down':
+            shaft.moveTo(node1.bottom)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.right.x + span.x / 2,
+                             cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.right.x + span.x / 2,
+                             cell2.top.y - span.y / 2)
+            else:
+                shaft.lineTo(cell1.bottom.x, cell2.top.y - span.y / 2)
+
+            shaft.lineTo(cell2.top.x, cell2.top.y - span.y / 2)
+            shaft.lineTo(node2.top)
+
+        elif dir == 'down':
+            shaft.moveTo(node1.bottom)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell1.right.x + span.x / 2,
+                             cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.right.x + span.x / 2,
+                             cell2.top.y - span.y / 2)
+                shaft.lineTo(cell2.top.x, cell2.top.y - span.y / 2)
+
+            shaft.lineTo(node2.top)
+
+        return shaft
+
+    @property
+    def labelbox(self):
+        span = XY(self.metrics.span_width, self.metrics.span_height)
+
+        dir = self.edge.direction
+        node1 = self.metrics.cell(self.edge.node1, use_padding=False)
+        node2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+        if dir == 'right':
+            if self.edge.skipped:
+                box = Box(node1.bottomright.x + span.x,
+                          node1.bottomright.y,
+                          node2.bottomleft.x - span.x,
+                          node2.bottomleft.y + span.y / 2)
+            else:
+                box = Box(node1.topright.x, node1.topright.y - span.y / 8,
+                          node2.left.x, node2.left.y - span.y / 8)
+
+        elif dir == 'right-up':
+            box = Box(node2.left.x - span.x, node2.left.y,
+                      node2.bottomleft.x, node2.bottomleft.y)
+
+        elif dir == 'right-down':
+            box = Box(node2.topleft.x, node2.topleft.y - span.y / 2,
+                      node2.top.x, node2.top.y)
+
+        elif dir in ('up', 'left-up', 'left', 'same'):
+            if self.edge.node2.xy.y < self.edge.node1.xy.y:
+                box = Box(node1.topright.x - span.x / 2 + span.x / 4,
+                          node1.topright.y - span.y / 2,
+                          node1.topright.x + span.x / 2 + span.x / 4,
+                          node1.topright.y)
+            else:
+                box = Box(node1.top.x + span.x / 4,
+                          node1.top.y - span.y,
+                          node1.topright.x + span.x / 4,
+                          node1.topright.y - span.y / 2)
+
+        elif dir == 'down':
+            box = Box(node2.top.x + span.x / 4,
+                      node2.top.y - span.y / 2,
+                      node2.topright.x + span.x / 4,
+                      node2.topright.y)
+
+        elif dir == 'left-down':
+            box = Box(node1.bottomleft.x, node1.bottomleft.y,
+                      node1.bottom.x, node1.bottom.y + span.y / 2)
+
+        # shrink box
+        box = Box(box[0] + span.x / 8, box[1],
+                  box[2] - span.x / 8, box[3])
+
+        return box
+
+
+class FlowchartLandscapeEdgeMetrics(LandscapeEdgeMetrics):
+    @property
+    def headshapes(self):
+        heads = []
+
+        if self.edge.direction == 'right-down':
+            if self.edge.dir in ('back', 'both'):
+                heads.append('up')
+            else:
+                heads.append(None)
+
+            if self.edge.dir in ('forward', 'both'):
+                heads.append('right')
+            else:
+                heads.append(None)
+        else:
+            heads = super(FlowchartLandscapeEdgeMetrics, self).headshapes
+
+        return heads
+
+    @property
+    def _shaft(self):
+        if self.edge.direction == 'right-down':
+            span = XY(self.metrics.span_width, self.metrics.span_height)
+            node1 = self.metrics.node(self.edge.node1)
+            cell1 = self.metrics.cell(self.edge.node1, use_padding=False)
+            node2 = self.metrics.node(self.edge.node2)
+            cell2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+            shaft = EdgeLines()
+            shaft.moveTo(node1.bottom)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4,
+                             cell1.bottom.y + span.y / 2)
+                shaft.lineTo(cell2.left.x - span.x / 4, cell2.left.y)
+            else:
+                shaft.lineTo(cell1.bottom.x, cell2.left.y)
+
+            shaft.lineTo(node2.left)
+        else:
+            shaft = super(FlowchartLandscapeEdgeMetrics, self)._shaft
+
+        return shaft
+
+    @property
+    def labelbox(self):
+        dir = self.edge.direction
+        if dir == 'right':
+            span = XY(self.metrics.span_width, self.metrics.span_height)
+            node1 = self.metrics.node(self.edge.node1)
+            cell1 = self.metrics.cell(self.edge.node1, use_padding=False)
+            node2 = self.metrics.node(self.edge.node2)
+            cell2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+            if self.edge.skipped:
+                box = Box(cell1.bottom.x, cell1.bottom.y,
+                          cell1.bottomright.x,
+                          cell1.bottomright.y + span.y / 2)
+            else:
+                box = Box(cell1.bottom.x, cell2.left.y - span.y / 2,
+                          cell1.bottom.x, cell2.left.y)
+        else:
+            box = super(FlowchartLandscapeEdgeMetrics, self).labelbox
+
+        return box
+
+
+class FlowchartPortraitEdgeMetrics(PortraitEdgeMetrics):
+    @property
+    def headshapes(self):
+        heads = []
+
+        if self.edge.direction == 'right-down':
+            if self.edge.dir in ('back', 'both'):
+                heads.append('left')
+            else:
+                heads.append(None)
+
+            if self.edge.dir in ('forward', 'both'):
+                heads.append('down')
+            else:
+                heads.append(None)
+        else:
+            heads = super(FlowchartPortraitEdgeMetrics, self).headshapes
+
+        return heads
+
+    @property
+    def _shaft(self):
+        if self.edge.direction == 'right-down':
+            span = XY(self.metrics.span_width, self.metrics.span_height)
+            node1 = self.metrics.node(self.edge.node1)
+            cell1 = self.metrics.cell(self.edge.node1, use_padding=False)
+            node2 = self.metrics.node(self.edge.node2)
+            cell2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+            shaft = EdgeLines()
+            shaft.moveTo(node1.right)
+
+            if self.edge.skipped:
+                shaft.lineTo(cell1.right.x + span.x * 3 / 4, cell1.right.y)
+                shaft.lineTo(cell1.right.x + span.x * 3 / 4,
+                             cell2.topleft.y - span.y / 2)
+                shaft.lineTo(cell2.top.x, cell2.top.y - span.y / 2)
+            else:
+                shaft.lineTo(cell2.top.x, cell1.right.y)
+
+            shaft.lineTo(node2.top)
+        else:
+            shaft = super(FlowchartPortraitEdgeMetrics, self)._shaft
+
+        return shaft
+
+    @property
+    def labelbox(self):
+        dir = self.edge.direction
+        span = XY(self.metrics.span_width, self.metrics.span_height)
+        node1 = self.metrics.node(self.edge.node1)
+        cell1 = self.metrics.cell(self.edge.node1, use_padding=False)
+        node2 = self.metrics.node(self.edge.node2)
+        cell2 = self.metrics.cell(self.edge.node2, use_padding=False)
+
+        if dir == 'down':
+            box = Box(cell2.topleft.x, cell2.top.y - span.y / 2,
+                      cell2.top.x, cell2.top.y)
+        elif dir == 'right':
+            if self.edge.skipped:
+                box = Box(cell1.bottom.x, cell1.bottom.y,
+                          cell1.bottomright.x,
+                          cell1.bottomright.y + span.y / 2)
+            else:
+                box = Box(cell1.bottom.x, cell2.left.y - span.y / 2,
+                          cell1.bottom.x, cell2.left.y)
+        else:
+            box = super(FlowchartPortraitEdgeMetrics, self).labelbox
+
+        return box
diff --git a/src/blockdiag/noderenderer/__init__.py b/src/blockdiag/noderenderer/__init__.py
new file mode 100644
index 0000000..ad3d3b2
--- /dev/null
+++ b/src/blockdiag/noderenderer/__init__.py
@@ -0,0 +1,166 @@
+# -*- 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.
+
+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[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 != None and kwargs.get('shadow') != True:
+            drawer.loadImage(self.node.icon, self.iconbox)
+
+    def render_shape(self, drawer, format, **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)
+
+    def render_number_badge(self, drawer, **kwargs):
+        if self.node.numbered != None and kwargs.get('shadow') != True:
+            badgeFill = kwargs.get('badgeFill')
+
+            xy = self.metrics.cell(self.node).topleft
+            r = self.metrics.cellsize * 3 / 2
+
+            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
new file mode 100644
index 0000000..cae08b6
--- /dev/null
+++ b/src/blockdiag/noderenderer/actor.py
@@ -0,0 +1,106 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY, Box
+
+
+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)
+        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 - self.radius * 4)
+        self.connectors[1] = XY(self.center.x + self.radius * 4, self.center.y)
+        self.connectors[2] = XY(self.center.x, self.center.y + self.radius * 4)
+        self.connectors[3] = XY(self.center.x - self.radius * 4, self.center.y)
+
+    def head_part(self):
+        r = self.radius
+        pt = self.metrics.cell(self.node).center
+        return Box(pt.x - r, pt.y - r * 4, pt.x + r, pt.y - r * 2)
+
+    def body_part(self):
+        r = self.radius
+        m = self.metrics.cell(self.node)
+
+        bodyC = m.center
+        neckWidth = r * 2 / 3  # neck size
+        arm = r * 4  # arm length
+        armWidth = r
+        bodyWidth = r * 2 / 3  # half of body width
+        bodyHeight = r
+        legXout = r * 7 / 2  # toe outer position
+        legYout = bodyHeight + r * 3
+        legXin = r * 2  # toe inner position
+        legYin = bodyHeight + r * 3
+
+        return [XY(bodyC.x + neckWidth, bodyC.y - r * 2),
+                XY(bodyC.x + neckWidth, bodyC.y - armWidth),  # neck end
+                XY(bodyC.x + arm, bodyC.y - armWidth),
+                XY(bodyC.x + arm, bodyC.y),  # right arm end
+                XY(bodyC.x + bodyWidth, bodyC.y),   # right body end
+                XY(bodyC.x + bodyWidth, bodyC.y + bodyHeight),
+                XY(bodyC.x + legXout, bodyC.y + legYout),
+                XY(bodyC.x + legXin, bodyC.y + legYin),
+
+                XY(bodyC.x, bodyC.y + (bodyHeight * 2)),  # body bottom center
+
+                XY(bodyC.x - legXin, bodyC.y + legYin),
+                XY(bodyC.x - legXout, bodyC.y + legYout),
+                XY(bodyC.x - bodyWidth, bodyC.y + bodyHeight),
+                XY(bodyC.x - bodyWidth, bodyC.y),  # left body end
+                XY(bodyC.x - arm, bodyC.y),
+                XY(bodyC.x - arm, bodyC.y - armWidth),
+                XY(bodyC.x - neckWidth, bodyC.y - armWidth),  # left arm end
+                XY(bodyC.x - neckWidth, bodyC.y - r * 2)]
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # FIXME: Actor does not support
+        #  - background image
+        #  - textarea
+
+        # draw body part
+        body = self.body_part()
+        if kwargs.get('shadow'):
+            body = self.shift_shadow(body)
+            drawer.polygon(body, fill=fill, filter='transp-blur')
+        else:
+            drawer.polygon(body, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+        # draw head part
+        head = self.head_part()
+        if kwargs.get('shadow'):
+            head = self.shift_shadow(head)
+            drawer.ellipse(head, fill=fill, outline=self.node.linecolor,
+                           filter='transp-blur')
+        else:
+            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/beginpoint.py b/src/blockdiag/noderenderer/beginpoint.py
new file mode 100644
index 0000000..09a2643
--- /dev/null
+++ b/src/blockdiag/noderenderer/beginpoint.py
@@ -0,0 +1,57 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY, Box
+
+
+class BeginPoint(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(BeginPoint, self).__init__(node, metrics)
+
+        m = metrics.cell(node)
+
+        self.radius = metrics.cellsize
+        self.center = m.center
+        self.textbox = Box(m.top.x, m.top.y, m.right.x, m.right.y)
+        self.textalign = 'left'
+        self.connectors = [XY(self.center.x, self.center.y - self.radius),
+                           XY(self.center.x + self.radius, self.center.y),
+                           XY(self.center.x, self.center.y + self.radius),
+                           XY(self.center.x - self.radius, self.center.y)]
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outline
+        r = self.radius
+        box = Box(self.center.x - r, self.center.y - r,
+                  self.center.x + r, self.center.y + r)
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+            drawer.ellipse(box, fill=fill, outline=fill, filter='transp-blur')
+        else:
+            if self.node.color == self.node.basecolor:
+                color = self.node.linecolor
+            else:
+                color = self.node.color
+
+            drawer.ellipse(box, fill=color, outline=self.node.linecolor,
+                           style=self.node.style)
+
+
+def setup(self):
+    install_renderer('beginpoint', BeginPoint)
diff --git a/src/blockdiag/noderenderer/box.py b/src/blockdiag/noderenderer/box.py
new file mode 100644
index 0000000..7ce04d6
--- /dev/null
+++ b/src/blockdiag/noderenderer/box.py
@@ -0,0 +1,43 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+
+
+class Box(NodeShape):
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outline
+        box = self.metrics.cell(self.node).box
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+            drawer.rectangle(box, fill=fill, outline=fill,
+                             filter='transp-blur')
+        elif self.node.background:
+            drawer.rectangle(box, fill=self.node.color,
+                             outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.rectangle(box, outline=self.node.linecolor,
+                             style=self.node.style)
+        else:
+            drawer.rectangle(box, fill=self.node.color,
+                             outline=self.node.linecolor,
+                             style=self.node.style)
+
+
+def setup(self):
+    install_renderer('box', Box)
diff --git a/src/blockdiag/noderenderer/circle.py b/src/blockdiag/noderenderer/circle.py
new file mode 100644
index 0000000..c82934a
--- /dev/null
+++ b/src/blockdiag/noderenderer/circle.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from blockdiag.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class Circle(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Circle, self).__init__(node, metrics)
+
+        r = min(metrics.node_width, metrics.node_height) / 2 + \
+            metrics.cellsize / 2
+        pt = metrics.cell(node).center
+        self.connectors = [XY(pt.x, pt.y - r),  # top
+                           XY(pt.x + r, pt.y),  # right
+                           XY(pt.x, pt.y + r),  # bottom
+                           XY(pt.x - r, pt.y)]  # left
+        self.textbox = Box(pt.x - r, pt.y - r, pt.x + r, pt.y + r)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outline
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(self.textbox)
+            drawer.ellipse(box, fill=fill, outline=fill, filter='transp-blur')
+        elif self.node.background:
+            drawer.ellipse(self.textbox, fill=self.node.color,
+                           outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.ellipse(self.textbox, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.ellipse(self.textbox, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('circle', Circle)
diff --git a/src/blockdiag/noderenderer/cloud.py b/src/blockdiag/noderenderer/cloud.py
new file mode 100644
index 0000000..5720c60
--- /dev/null
+++ b/src/blockdiag/noderenderer/cloud.py
@@ -0,0 +1,124 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box
+from blockdiag.imagedraw.simplesvg import pathdata
+
+
+class Cloud(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Cloud, self).__init__(node, metrics)
+
+        pt = metrics.cell(node).topleft
+        rx = (self.node.width or self.metrics.node_width) / 12
+        ry = (self.node.height or self.metrics.node_height) / 5
+        self.textbox = Box(pt.x + rx * 2, pt.y + ry,
+                           pt.x + rx * 11, pt.y + ry * 4)
+
+    def render_shape(self, drawer, format, **kwargs):
+        # draw background
+        self.render_shape_background(drawer, format, **kwargs)
+
+        if not kwargs.get('shadow') and self.node.background:
+            drawer.loadImage(self.node.background, self.textbox)
+
+    def render_shape_background(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        pt = m.topleft
+        rx = (self.node.width or self.metrics.node_width) / 12
+        ry = (self.node.height or self.metrics.node_height) / 5
+
+        ellipses = [Box(pt.x + rx * 2, pt.y + ry,
+                        pt.x + rx * 5, pt.y + ry * 3),
+                    Box(pt.x + rx * 4, pt.y,
+                        pt.x + rx * 9, pt.y + ry * 2),
+                    Box(pt.x + rx * 8, pt.y + ry,
+                        pt.x + rx * 11, pt.y + ry * 3),
+                    Box(pt.x + rx * 9, pt.y + ry * 2,
+                        pt.x + rx * 13, pt.y + ry * 4),
+                    Box(pt.x + rx * 8, pt.y + ry * 2,
+                        pt.x + rx * 11, pt.y + ry * 5),
+                    Box(pt.x + rx * 5, pt.y + ry * 2,
+                        pt.x + rx * 8, pt.y + ry * 5),
+                    Box(pt.x + rx * 2, pt.y + ry * 2,
+                        pt.x + rx * 5, pt.y + ry * 5),
+                    Box(pt.x + rx * 0, pt.y + ry * 2,
+                        pt.x + rx * 4, pt.y + ry * 4)]
+
+        for e in ellipses:
+            if kwargs.get('shadow'):
+                e = self.shift_shadow(e)
+                drawer.ellipse(e, fill=fill, outline=fill,
+                               filter='transp-blur')
+            else:
+                drawer.ellipse(e, fill=self.node.color,
+                               outline=self.node.linecolor,
+                               style=self.node.style)
+
+        rects = [Box(pt.x + rx * 2, pt.y + ry * 2,
+                     pt.x + rx * 11, pt.y + ry * 4),
+                 Box(pt.x + rx * 4, pt.y + ry,
+                     pt.x + rx * 9, pt.y + ry * 2)]
+        for rect in rects:
+            if kwargs.get('shadow'):
+                rect = self.shift_shadow(rect)
+                drawer.rectangle(rect, fill=fill, outline=fill,
+                                 filter='transp-blur')
+            else:
+                drawer.rectangle(rect, fill=self.node.color,
+                                 outline=self.node.color)
+
+    def render_vector_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # create pathdata
+        m = self.metrics.cell(self.node)
+        rx = (self.node.width or self.metrics.node_width) / 12
+        ry = (self.node.height or self.metrics.node_height) / 5
+
+        pt = m.topleft
+        if kwargs.get('shadow'):
+            pt = self.shift_shadow(pt)
+
+        path = pathdata(pt.x + rx * 2, pt.y + ry * 2)
+        path.ellarc(rx * 2, ry, 0, 0, 1, pt.x + rx * 4, pt.y + ry)
+        path.ellarc(rx * 2, ry * 3 / 4, 0, 0, 1, pt.x + rx * 9, pt.y + ry)
+        path.ellarc(rx * 2, ry, 0, 0, 1, pt.x + rx * 11, pt.y + ry * 2)
+        path.ellarc(rx * 2, ry, 0, 0, 1, pt.x + rx * 11, pt.y + ry * 4)
+        path.ellarc(rx * 2, ry * 5 / 2, 0, 0, 1, pt.x + rx * 8, pt.y + ry * 4)
+        path.ellarc(rx * 2, ry * 5 / 2, 0, 0, 1, pt.x + rx * 5, pt.y + ry * 4)
+        path.ellarc(rx * 2, ry * 5 / 2, 0, 0, 1, pt.x + rx * 2, pt.y + ry * 4)
+        path.ellarc(rx * 2, ry, 0, 0, 1, pt.x + rx * 2, pt.y + ry * 2)
+
+        # draw outline
+        if kwargs.get('shadow'):
+            drawer.path(path, fill=fill, outline=fill,
+                        filter='transp-blur')
+        elif self.node.background:
+            drawer.path(path, fill=self.node.color, outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.path(path, fill="none", outline=self.node.linecolor,
+                        style=self.node.style)
+        else:
+            drawer.path(path, fill=self.node.color,
+                        outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('cloud', Cloud)
diff --git a/src/blockdiag/noderenderer/diamond.py b/src/blockdiag/noderenderer/diamond.py
new file mode 100644
index 0000000..1f46ea6
--- /dev/null
+++ b/src/blockdiag/noderenderer/diamond.py
@@ -0,0 +1,58 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class Diamond(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Diamond, self).__init__(node, metrics)
+
+        r = metrics.cellsize
+        m = metrics.cell(node)
+        self.connectors = [XY(m.top.x, m.top.y - r),
+                           XY(m.right.x + r, m.right.y),
+                           XY(m.bottom.x, m.bottom.y + r),
+                           XY(m.left.x - r, m.left.y),
+                           XY(m.top.x, m.top.y - r)]
+        self.textbox = Box((self.connectors[0].x + self.connectors[3].x) / 2,
+                           (self.connectors[0].y + self.connectors[3].y) / 2,
+                           (self.connectors[1].x + self.connectors[2].x) / 2,
+                           (self.connectors[1].y + self.connectors[2].y) / 2)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outline
+        if kwargs.get('shadow'):
+            diamond = self.shift_shadow(self.connectors)
+            drawer.polygon(diamond, fill=fill, outline=fill,
+                             filter='transp-blur')
+        elif self.node.background:
+            drawer.polygon(self.connectors, fill=self.node.color,
+                           outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.polygon(self.connectors, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.polygon(self.connectors, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('diamond', Diamond)
+    install_renderer('flowchart.condition', Diamond)
diff --git a/src/blockdiag/noderenderer/dots.py b/src/blockdiag/noderenderer/dots.py
new file mode 100644
index 0000000..d57d4d9
--- /dev/null
+++ b/src/blockdiag/noderenderer/dots.py
@@ -0,0 +1,53 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY
+
+
+class Dots(NodeShape):
+    def render_label(self, drawer, **kwargs):
+        pass
+
+    def render_shape(self, drawer, format, **kwargs):
+        if kwargs.get('shadow'):
+            return
+
+        m = self.metrics
+        center = m.cell(self.node).center
+        dots = [center]
+        if self.node.group.orientation == 'landscape':
+            pt = XY(center.x, center.y - m.node_height / 2)
+            dots.append(pt)
+
+            pt = XY(center.x, center.y + m.node_height / 2)
+            dots.append(pt)
+        else:
+            pt = XY(center.x - m.node_width / 3, center.y)
+            dots.append(pt)
+
+            pt = XY(center.x + m.node_width / 3, center.y)
+            dots.append(pt)
+
+        r = m.cellsize / 2
+        for dot in dots:
+            box = (dot.x - r, dot.y - r, dot.x + r, dot.y + r)
+            drawer.ellipse(box, fill=self.node.linecolor,
+                           outline=self.node.linecolor)
+
+
+def setup(self):
+    install_renderer('dots', Dots)
diff --git a/src/blockdiag/noderenderer/ellipse.py b/src/blockdiag/noderenderer/ellipse.py
new file mode 100644
index 0000000..baba42d
--- /dev/null
+++ b/src/blockdiag/noderenderer/ellipse.py
@@ -0,0 +1,50 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box
+
+
+class Ellipse(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Ellipse, self).__init__(node, metrics)
+
+        r = metrics.cellsize
+        box = metrics.cell(node).box
+        self.textbox = Box(box[0] + r, box[1] + r, box[2] - r, box[3] - r)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outline
+        box = self.metrics.cell(self.node).box
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+            drawer.ellipse(box, fill=fill, outline=fill,
+                           filter='transp-blur')
+        elif self.node.background:
+            drawer.ellipse(box, fill=self.node.color,
+                           outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.ellipse(box, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.ellipse(box, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('ellipse', Ellipse)
diff --git a/src/blockdiag/noderenderer/endpoint.py b/src/blockdiag/noderenderer/endpoint.py
new file mode 100644
index 0000000..68dec65
--- /dev/null
+++ b/src/blockdiag/noderenderer/endpoint.py
@@ -0,0 +1,64 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY, Box
+
+
+class EndPoint(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(EndPoint, self).__init__(node, metrics)
+
+        m = metrics.cell(node)
+
+        self.radius = metrics.cellsize
+        self.center = m.center
+        self.textbox = Box(m.top.x, m.top.y, m.right.x, m.right.y)
+        self.textalign = 'left'
+        self.connectors = [XY(self.center.x, self.center.y - self.radius),
+                           XY(self.center.x + self.radius, self.center.y),
+                           XY(self.center.x, self.center.y + self.radius),
+                           XY(self.center.x - self.radius, self.center.y)]
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outer circle
+        r = self.radius
+        box = Box(self.center.x - r, self.center.y - r,
+                  self.center.x + r, self.center.y + r)
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+            drawer.ellipse(box, fill=fill, outline=fill, filter='transp-blur')
+        else:
+            drawer.ellipse(box, fill='white', outline=self.node.linecolor,
+                           style=self.node.style)
+
+        # draw inner circle
+        box = Box(self.center.x - r / 2, self.center.y - r / 2,
+                  self.center.x + r / 2, self.center.y + r / 2)
+        if not kwargs.get('shadow'):
+            if self.node.color == self.node.basecolor:
+                color = self.node.linecolor
+            else:
+                color = self.node.color
+
+            drawer.ellipse(box, fill=color, outline=self.node.linecolor,
+                           style=self.node.style)
+
+
+def setup(self):
+    install_renderer('endpoint', EndPoint)
diff --git a/src/blockdiag/noderenderer/flowchart/__init__.py b/src/blockdiag/noderenderer/flowchart/__init__.py
new file mode 100644
index 0000000..bd36e96
--- /dev/null
+++ b/src/blockdiag/noderenderer/flowchart/__init__.py
@@ -0,0 +1,14 @@
+# -*- 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.
diff --git a/src/blockdiag/noderenderer/flowchart/database.py b/src/blockdiag/noderenderer/flowchart/database.py
new file mode 100644
index 0000000..dc3174e
--- /dev/null
+++ b/src/blockdiag/noderenderer/flowchart/database.py
@@ -0,0 +1,121 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY, Box
+from blockdiag.imagedraw.simplesvg import pathdata
+
+
+class Database(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Database, self).__init__(node, metrics)
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize
+        self.textbox = Box(m.topleft.x, m.topleft.y + r * 3 / 2,
+                           m.bottomright.x, m.bottomright.y - r / 2)
+
+    def render_shape(self, drawer, format, **kwargs):
+        # draw background
+        self.render_shape_background(drawer, format, **kwargs)
+
+        # draw background image
+        if self.node.background:
+            drawer.loadImage(self.node.background, self.textbox)
+
+    def render_shape_background(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize
+        box = m.box
+
+        ellipse = Box(box[0], box[3] - r * 2, box[2], box[3])
+        if kwargs.get('shadow'):
+            ellipse = self.shift_shadow(ellipse)
+            drawer.ellipse(ellipse, fill=fill, outline=fill,
+                           filter='transp-blur')
+        else:
+            drawer.ellipse(ellipse, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+        rect = Box(box[0], box[1] + r, box[2], box[3] - r)
+        if kwargs.get('shadow'):
+            rect = self.shift_shadow(rect)
+            drawer.rectangle(rect, fill=fill, outline=fill,
+                             filter='transp-blur')
+        else:
+            drawer.rectangle(rect, fill=self.node.color,
+                             outline=self.node.color)
+
+        ellipse = Box(box[0], box[1], box[2], box[1] + r * 2)
+        if kwargs.get('shadow'):
+            ellipse = self.shift_shadow(ellipse)
+            drawer.ellipse(ellipse, fill=fill, outline=fill,
+                           filter='transp-blur')
+        else:
+            drawer.ellipse(ellipse, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+        # line both side
+        lines = [(XY(box[0], box[1] + r), XY(box[0], box[3] - r)),
+                 (XY(box[2], box[1] + r), XY(box[2], box[3] - r))]
+        for line in lines:
+            if not kwargs.get('shadow'):
+                drawer.line(line, fill=self.node.linecolor,
+                            style=self.node.style)
+
+    def render_vector_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize
+        width = self.metrics.node_width
+
+        box = m.box
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+
+        path = pathdata(box[0], box[1] + r)
+        path.ellarc(width / 2, r, 0, 0, 1, box[2], box[1] + r)
+        path.line(box[2], box[3] - r)
+        path.ellarc(width / 2, r, 0, 0, 1, box[0], box[3] - r)
+        path.line(box[0], box[1] + r)
+
+        # draw outline
+        if kwargs.get('shadow'):
+            drawer.path(path, fill=fill, outline=fill,
+                        filter='transp-blur')
+        elif self.node.background:
+            drawer.path(path, fill=self.node.color,
+                        outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.path(path, fill="none",
+                        outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.path(path, fill=self.node.color,
+                        outline=self.node.linecolor, style=self.node.style)
+
+        # draw cap of cylinder
+        if not kwargs.get('shadow'):
+            path = pathdata(box[2], box[1] + r)
+            path.ellarc(width / 2, r, 0, 0, 1, box[0], box[1] + r)
+            drawer.path(path, fill=self.node.color,
+                        outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('flowchart.database', Database)
diff --git a/src/blockdiag/noderenderer/flowchart/input.py b/src/blockdiag/noderenderer/flowchart/input.py
new file mode 100644
index 0000000..5e9b0ce
--- /dev/null
+++ b/src/blockdiag/noderenderer/flowchart/input.py
@@ -0,0 +1,60 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class Input(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Input, self).__init__(node, metrics)
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 3
+
+        self.textbox = Box(m.topleft.x + r, m.topleft.y,
+                           m.bottomright.x - r, m.bottomright.y)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 3
+
+        shape = [XY(m.topleft.x + r,  m.topleft.y),
+                 XY(m.topright.x, m.topright.y),
+                 XY(m.bottomright.x - r, m.bottomright.y),
+                 XY(m.bottomleft.x,  m.bottomleft.y),
+                 XY(m.topleft.x + r,  m.topleft.y)]
+
+        # draw outline
+        if kwargs.get('shadow'):
+            shape = self.shift_shadow(shape)
+            drawer.polygon(shape, fill=fill, outline=fill,
+                           filter='transp-blur')
+        elif self.node.background:
+            drawer.polygon(shape, fill=self.node.color,
+                             outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.polygon(shape, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.polygon(shape, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('flowchart.input', Input)
diff --git a/src/blockdiag/noderenderer/flowchart/loopin.py b/src/blockdiag/noderenderer/flowchart/loopin.py
new file mode 100644
index 0000000..106318e
--- /dev/null
+++ b/src/blockdiag/noderenderer/flowchart/loopin.py
@@ -0,0 +1,63 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class LoopIn(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(LoopIn, self).__init__(node, metrics)
+
+        m = self.metrics.cell(self.node)
+        ydiff = self.metrics.node_height / 4
+
+        self.textbox = Box(m.topleft.x, m.topleft.y + ydiff,
+                           m.bottomright.x, m.bottomright.y)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        xdiff = self.metrics.node_width / 4
+        ydiff = self.metrics.node_height / 4
+
+        shape = [XY(m.topleft.x + xdiff, m.topleft.y),
+                 XY(m.topright.x - xdiff, m.topleft.y),
+                 XY(m.topright.x, m.topright.y + ydiff),
+                 XY(m.topright.x, m.bottomright.y),
+                 XY(m.topleft.x, m.bottomleft.y),
+                 XY(m.topleft.x, m.topleft.y + ydiff),
+                 XY(m.topleft.x + xdiff, m.topleft.y)]
+
+        # draw outline
+        if kwargs.get('shadow'):
+            shape = self.shift_shadow(shape)
+            drawer.polygon(shape, fill=fill, outline=fill,
+                           filter='transp-blur')
+        elif self.node.background:
+            drawer.polygon(shape, fill=self.node.color,
+                           outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.polygon(shape, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.polygon(shape, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('flowchart.loopin', LoopIn)
diff --git a/src/blockdiag/noderenderer/flowchart/loopout.py b/src/blockdiag/noderenderer/flowchart/loopout.py
new file mode 100644
index 0000000..edfe072
--- /dev/null
+++ b/src/blockdiag/noderenderer/flowchart/loopout.py
@@ -0,0 +1,63 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class LoopOut(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(LoopOut, self).__init__(node, metrics)
+
+        m = self.metrics.cell(self.node)
+        ydiff = self.metrics.node_height / 4
+
+        self.textbox = Box(m.topleft.x, m.topleft.y,
+                           m.bottomright.x, m.bottomright.y - ydiff)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        xdiff = self.metrics.node_width / 4
+        ydiff = self.metrics.node_height / 4
+
+        shape = [XY(m.topleft.x, m.topleft.y),
+                XY(m.topright.x, m.topright.y),
+                XY(m.bottomright.x, m.bottomright.y - ydiff),
+                XY(m.bottomright.x - xdiff, m.bottomright.y),
+                XY(m.bottomleft.x + xdiff, m.bottomleft.y),
+                XY(m.bottomleft.x, m.bottomleft.y - ydiff),
+                XY(m.topleft.x, m.topleft.y)]
+
+        # draw outline
+        if kwargs.get('shadow'):
+            shape = self.shift_shadow(shape)
+            drawer.polygon(shape, fill=fill, outline=fill,
+                           filter='transp-blur')
+        elif self.node.background:
+            drawer.polygon(shape, fill=self.node.color,
+                             outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.polygon(shape, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.polygon(shape, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('flowchart.loopout', LoopOut)
diff --git a/src/blockdiag/noderenderer/flowchart/terminator.py b/src/blockdiag/noderenderer/flowchart/terminator.py
new file mode 100644
index 0000000..015e555
--- /dev/null
+++ b/src/blockdiag/noderenderer/flowchart/terminator.py
@@ -0,0 +1,109 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY, Box
+from blockdiag.imagedraw.simplesvg import pathdata
+
+
+class Terminator(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Terminator, self).__init__(node, metrics)
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 2
+        self.textbox = Box(m.topleft.x + r, m.topleft.y,
+                           m.bottomright.x - r, m.bottomright.y)
+
+    def render_shape(self, drawer, format, **kwargs):
+        # draw background
+        self.render_shape_background(drawer, format, **kwargs)
+
+        # draw outline
+        if not kwargs.get('shadow') and self.node.background:
+            drawer.loadImage(self.node.background, self.textbox)
+
+    def render_shape_background(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 2
+
+        box = m.box
+        ellipses = [Box(box[0], box[1], box[0] + r * 2, box[3]),
+                    Box(box[2] - r * 2, box[1], box[2], box[3])]
+
+        for e in ellipses:
+            if kwargs.get('shadow'):
+                e = self.shift_shadow(e)
+                drawer.ellipse(e, fill=fill, outline=fill,
+                               filter='transp-blur')
+            else:
+                drawer.ellipse(e, fill=self.node.color,
+                               outline=self.node.linecolor,
+                               style=self.node.style)
+
+        rect = Box(box[0] + r, box[1], box[2] - r, box[3])
+        if kwargs.get('shadow'):
+            rect = self.shift_shadow(rect)
+            drawer.rectangle(rect, fill=fill, outline=fill,
+                             filter='transp-blur')
+        else:
+            drawer.rectangle(rect, fill=self.node.color,
+                             outline=self.node.color)
+
+        lines = [(XY(box[0] + r, box[1]), XY(box[2] - r, box[1])),
+                 (XY(box[0] + r, box[3]), XY(box[2] - r, box[3]))]
+        for line in lines:
+            if not kwargs.get('shadow'):
+                drawer.line(line, fill=self.node.linecolor,
+                            style=self.node.style)
+
+    def render_vector_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # create pathdata
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 2
+        height = self.metrics.node_height
+
+        box = Box(m.topleft.x + r, m.topleft.y,
+                  m.bottomright.x - r, m.bottomright.y)
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+
+        path = pathdata(box[0], box[1])
+        path.line(box[2], box[1])
+        path.ellarc(r, height / 2, 0, 0, 1, box[2], box[3])
+        path.line(box[0], box[3])
+        path.ellarc(r, height / 2, 0, 0, 1, box[0], box[1])
+
+        # draw outline
+        if kwargs.get('shadow'):
+            drawer.path(path, fill=fill, outline=fill,
+                        filter='transp-blur')
+        elif self.node.background:
+            drawer.path(path, fill=self.node.color, outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.path(path, fill="none",
+                        outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.path(path, fill=self.node.color,
+                        outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('flowchart.terminator', Terminator)
diff --git a/src/blockdiag/noderenderer/mail.py b/src/blockdiag/noderenderer/mail.py
new file mode 100644
index 0000000..d5f84ae
--- /dev/null
+++ b/src/blockdiag/noderenderer/mail.py
@@ -0,0 +1,60 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class Mail(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Mail, self).__init__(node, metrics)
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 2
+        self.textbox = Box(m.topleft.x, m.topleft.y + r,
+                           m.bottomright.x, m.bottomright.y)
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 2
+
+        # draw outline
+        box = self.metrics.cell(self.node).box
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+            drawer.rectangle(box, fill=fill, outline=fill,
+                             filter='transp-blur')
+        elif self.node.background:
+            drawer.rectangle(box, fill=self.node.color,
+                             outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.rectangle(box, outline=self.node.linecolor,
+                             style=self.node.style)
+        else:
+            drawer.rectangle(box, fill=self.node.color,
+                             outline=self.node.linecolor,
+                             style=self.node.style)
+
+        # draw flap
+        if not kwargs.get('shadow'):
+            flap = [m.topleft, XY(m.top.x, m.top.y + r), m.topright]
+            drawer.line(flap, fill=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('mail', Mail)
diff --git a/src/blockdiag/noderenderer/minidiamond.py b/src/blockdiag/noderenderer/minidiamond.py
new file mode 100644
index 0000000..9ef04b1
--- /dev/null
+++ b/src/blockdiag/noderenderer/minidiamond.py
@@ -0,0 +1,50 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class MiniDiamond(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(MiniDiamond, self).__init__(node, metrics)
+
+        r = metrics.cellsize
+        m = metrics.cell(node)
+        c = m.center
+        self.connectors = (XY(c.x, c.y - r),
+                           XY(c.x + r, c.y),
+                           XY(c.x, c.y + r),
+                           XY(c.x - r, c.y),
+                           XY(c.x, c.y - r))
+        self.textbox = Box(m.top.x, m.top.y, m.right.x, m.right.y)
+        self.textalign = 'left'
+
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # draw outline
+        if kwargs.get('shadow'):
+            diamond = self.shift_shadow(self.connectors)
+            drawer.polygon(diamond, fill=fill, outline=fill,
+                             filter='transp-blur')
+        else:
+            drawer.polygon(self.connectors, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('minidiamond', MiniDiamond)
diff --git a/src/blockdiag/noderenderer/none.py b/src/blockdiag/noderenderer/none.py
new file mode 100644
index 0000000..e8a9824
--- /dev/null
+++ b/src/blockdiag/noderenderer/none.py
@@ -0,0 +1,35 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+
+
+class NoneShape(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(NoneShape, self).__init__(node, metrics)
+
+        p = metrics.cell(node).center
+        self.connectors = [p, p, p, p]
+
+    def render_label(self, drawer, **kwargs):
+        pass
+
+    def render_shape(self, drawer, format, **kwargs):
+        pass
+
+
+def setup(self):
+    install_renderer('none', NoneShape)
diff --git a/src/blockdiag/noderenderer/note.py b/src/blockdiag/noderenderer/note.py
new file mode 100644
index 0000000..b0eea3d
--- /dev/null
+++ b/src/blockdiag/noderenderer/note.py
@@ -0,0 +1,58 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY
+
+
+class Note(NodeShape):
+    def render_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize * 2
+
+        tr = m.topright
+        note = [m.topleft, XY(tr.x - r, tr.y), XY(tr.x, tr.y + r),
+                m.bottomright, m.bottomleft, m.topleft]
+        box = self.metrics.cell(self.node).box
+
+        # draw outline
+        if kwargs.get('shadow'):
+            note = self.shift_shadow(note)
+            drawer.polygon(note, fill=fill, outline=fill,
+                           filter='transp-blur')
+        elif self.node.background:
+            drawer.polygon(note, fill=self.node.color,
+                           outline=self.node.color)
+            drawer.loadImage(self.node.background, box)
+            drawer.polygon(note, fill="none",
+                           outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.polygon(note, fill=self.node.color,
+                           outline=self.node.linecolor, style=self.node.style)
+
+        # draw folded
+        if not kwargs.get('shadow'):
+            folded = [XY(tr.x - r, tr.y),
+                      XY(tr.x - r, tr.y + r),
+                      XY(tr.x, tr.y + r)]
+            drawer.line(folded, fill=self.node.linecolor,
+                        style=self.node.style)
+
+
+def setup(self):
+    install_renderer('note', Note)
diff --git a/src/blockdiag/noderenderer/roundedbox.py b/src/blockdiag/noderenderer/roundedbox.py
new file mode 100644
index 0000000..fcc50cd
--- /dev/null
+++ b/src/blockdiag/noderenderer/roundedbox.py
@@ -0,0 +1,122 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import XY, Box
+from blockdiag.imagedraw.simplesvg import pathdata
+
+
+class RoundedBox(NodeShape):
+    def render_shape(self, drawer, format, **kwargs):
+        # draw background
+        self.render_shape_background(drawer, format, **kwargs)
+
+        # draw outline
+        box = self.metrics.cell(self.node).box
+        if not kwargs.get('shadow'):
+            if self.node.background:
+                drawer.loadImage(self.node.background, box)
+
+            self.render_shape_outline(drawer, format, **kwargs)
+
+    def render_shape_outline(self, drawer, format, **kwargs):
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize
+        box = m.box
+
+        lines = [(XY(box[0] + r, box[1]), XY(box[2] - r, box[1])),
+                 (XY(box[2], box[1] + r), XY(box[2], box[3] - r)),
+                 (XY(box[0] + r, box[3]), XY(box[2] - r, box[3])),
+                 (XY(box[0], box[1] + r), XY(box[0], box[3] - r))]
+        for line in lines:
+            drawer.line(line, fill=self.node.linecolor, style=self.node.style)
+
+        arcs = [((box[0], box[1], box[0] + r * 2, box[1] + r * 2), 180, 270),
+                ((box[2] - r * 2, box[1], box[2], box[1] + r * 2), 270, 360),
+                ((box[2] - r * 2, box[3] - r * 2, box[2], box[3]), 0, 90),
+                ((box[0], box[3] - r * 2, box[0] + r * 2, box[3]), 90, 180)]
+        for arc in arcs:
+            drawer.arc(arc[0], arc[1], arc[2],
+                       fill=self.node.linecolor, style=self.node.style)
+
+    def render_shape_background(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        m = self.metrics.cell(self.node)
+        r = self.metrics.cellsize
+
+        box = m.box
+        ellipses = [Box(box[0], box[1], box[0] + r * 2, box[1] + r * 2),
+                    Box(box[2] - r * 2, box[1], box[2], box[1] + r * 2),
+                    Box(box[0], box[3] - r * 2, box[0] + r * 2, box[3]),
+                    Box(box[2] - r * 2, box[3] - r * 2, box[2], box[3])]
+
+        for e in ellipses:
+            if kwargs.get('shadow'):
+                e = self.shift_shadow(e)
+                drawer.ellipse(e, fill=fill, outline=fill,
+                               filter='transp-blur')
+            else:
+                drawer.ellipse(e, fill=self.node.color,
+                               outline=self.node.color)
+
+        rects = [Box(box[0] + r, box[1], box[2] - r, box[3]),
+                 Box(box[0], box[1] + r, box[2], box[3] - r)]
+        for rect in rects:
+            if kwargs.get('shadow'):
+                rect = self.shift_shadow(rect)
+                drawer.rectangle(rect, fill=fill, outline=fill,
+                                 filter='transp-blur')
+            else:
+                drawer.rectangle(rect, fill=self.node.color,
+                                 outline=self.node.color)
+
+    def render_vector_shape(self, drawer, format, **kwargs):
+        fill = kwargs.get('fill')
+
+        # create pathdata
+        box = self.metrics.cell(self.node).box
+        r = self.metrics.cellsize
+
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(box)
+
+        path = pathdata(box[0] + r, box[1])
+        path.line(box[2] - r, box[1])
+        path.ellarc(r, r, 0, 0, 1, box[2], box[1] + r)
+        path.line(box[2], box[3] - r)
+        path.ellarc(r, r, 0, 0, 1, box[2] - r, box[3])
+        path.line(box[0] + r, box[3])
+        path.ellarc(r, r, 0, 0, 1, box[0], box[3] - r)
+        path.line(box[0], box[1] + r)
+        path.ellarc(r, r, 0, 0, 1, box[0] + r, box[1])
+
+        # draw outline
+        if kwargs.get('shadow'):
+            drawer.path(path, fill=fill, outline=fill,
+                        filter='transp-blur')
+        elif self.node.background:
+            drawer.path(path, fill=self.node.color, outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.path(path, fill="none",
+                        outline=self.node.linecolor, style=self.node.style)
+        else:
+            drawer.path(path, fill=self.node.color,
+                        outline=self.node.linecolor, style=self.node.style)
+
+
+def setup(self):
+    install_renderer('roundedbox', RoundedBox)
diff --git a/src/blockdiag/noderenderer/square.py b/src/blockdiag/noderenderer/square.py
new file mode 100644
index 0000000..52d6972
--- /dev/null
+++ b/src/blockdiag/noderenderer/square.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+from blockdiag.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import Box, XY
+
+
+class Square(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(Square, self).__init__(node, metrics)
+
+        r = min(metrics.node_width, metrics.node_height) / 2 + \
+            metrics.cellsize / 2
+        pt = metrics.cell(node).center
+        self.connectors = [XY(pt.x, pt.y - r),  # top
+                           XY(pt.x + r, pt.y),  # right
+                           XY(pt.x, pt.y + r),  # bottom
+                           XY(pt.x - r, pt.y)]  # left
+        self.textbox = Box(pt.x - r, pt.y - r, pt.x + r, pt.y + r)
+
+    def render_shape(self, drawer, format, **kwargs):
+        outline = kwargs.get('outline')
+        fill = kwargs.get('fill')
+
+        # draw outline
+        if kwargs.get('shadow'):
+            box = self.shift_shadow(self.textbox)
+            drawer.rectangle(box, fill=fill, outline=fill,
+                             filter='transp-blur')
+        elif self.node.background:
+            drawer.rectangle(self.textbox, fill=self.node.color,
+                             outline=self.node.color)
+            drawer.loadImage(self.node.background, self.textbox)
+            drawer.rectangle(self.textbox, fill="none",
+                             outline=self.node.linecolor,
+                             style=self.node.style)
+        else:
+            drawer.rectangle(self.textbox, fill=self.node.color,
+                             outline=self.node.linecolor,
+                             style=self.node.style)
+
+
+def setup(self):
+    install_renderer('square', Square)
diff --git a/src/blockdiag/noderenderer/textbox.py b/src/blockdiag/noderenderer/textbox.py
new file mode 100644
index 0000000..7e86674
--- /dev/null
+++ b/src/blockdiag/noderenderer/textbox.py
@@ -0,0 +1,47 @@
+# -*- 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.noderenderer import NodeShape
+from blockdiag.noderenderer import install_renderer
+from blockdiag.utils import images, Box, XY
+
+
+class TextBox(NodeShape):
+    def __init__(self, node, metrics=None):
+        super(TextBox, self).__init__(node, metrics)
+
+        if self.node.background:
+            size = images.get_image_size(self.node.background)
+            size = images.calc_image_size(size, self.textbox.size)
+
+            pt = self.textbox.center
+            self.textbox = Box(pt.x - size[0] / 2, pt.y - size[1] / 2,
+                               pt.x + size[0] / 2, pt.y + size[1] / 2)
+
+            self.connectors[0] = XY(pt.x, self.textbox[1])
+            self.connectors[1] = XY(self.textbox[2], pt.y)
+            self.connectors[2] = XY(pt.x, self.textbox[3])
+            self.connectors[3] = XY(self.textbox[0], pt.y)
+
+        if self.node.icon:
+            self.connectors[3] = XY(self.iconbox[0], pt.y)
+
+    def render_shape(self, drawer, format, **kwargs):
+        if not kwargs.get('shadow') and self.node.background:
+            drawer.loadImage(self.node.background, self.textbox)
+
+
+def setup(self):
+    install_renderer('textbox', TextBox)
diff --git a/src/blockdiag/parser.py b/src/blockdiag/parser.py
new file mode 100644
index 0000000..34a85f7
--- /dev/null
+++ b/src/blockdiag/parser.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2008/2009 Andrey Vlasovskikh
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+r'''A DOT language parser using funcparserlib.
+
+The parser is based on [the DOT grammar][1]. It is pretty complete with a few
+not supported things:
+
+* Ports and compass points
+* XML identifiers
+
+At the moment, the parser builds only a parse tree, not an abstract syntax tree
+(AST) or an API for dealing with DOT.
+
+  [1]: http://www.graphviz.org/doc/info/lang.html
+'''
+
+import codecs
+from re import MULTILINE, DOTALL
+from funcparserlib.lexer import make_tokenizer, Token, LexerError
+from funcparserlib.parser import (some, a, maybe, many, finished, skip,
+    oneplus, forward_decl, NoParseError)
+
+from utils.collections import namedtuple
+
+ENCODING = 'utf-8'
+
+Graph = namedtuple('Graph', 'type id stmts')
+SubGraph = namedtuple('SubGraph', 'id stmts')
+Node = namedtuple('Node', 'id attrs')
+Attr = namedtuple('Attr', 'name value')
+Edge = namedtuple('Edge', 'nodes attrs')
+DefAttrs = namedtuple('DefAttrs', 'object attrs')
+AttrPlugin = namedtuple('AttrPlugin', 'name attrs')
+AttrClass = namedtuple('AttrClass', 'name attrs')
+Statements = namedtuple('Statements', 'stmts')
+
+
+class ParseException(Exception):
+    pass
+
+
+def tokenize(str):
+    'str -> Sequence(Token)'
+    specs = [
+        ('Comment', (r'/\*(.|[\r\n])*?\*/', MULTILINE)),
+        ('Comment', (r'(//|#).*',)),
+        ('NL',      (r'[\r\n]+',)),
+        ('Space',   (r'[ \t\r\n]+',)),
+        ('Name',    (ur'[A-Za-z_0-9\u0080-\uffff]'
+                     ur'[A-Za-z_\-.0-9\u0080-\uffff]*',)),
+        ('Op',      (r'[{};,=\[\]]|(<->)|(<-)|(--)|(->)',)),
+        ('Number',  (r'-?(\.[0-9]+)|([0-9]+(\.[0-9]*)?)',)),
+        ('String',  (r'(?P<quote>"|\').*?(?<!\\)(?P=quote)', DOTALL)),
+    ]
+    useless = ['Comment', 'NL', 'Space']
+    t = make_tokenizer(specs)
+    return [x for x in t(str) if x.type not in useless]
+
+
+def parse(seq):
+    'Sequence(Token) -> object'
+    unarg = lambda f: lambda args: f(*args)
+    tokval = lambda x: x.value
+    flatten = lambda list: sum(list, [])
+    node_flatten = lambda l: sum([[l[0]]] + list(l[1:]), [])
+    n = lambda s: a(Token('Name', s)) >> tokval
+    op = lambda s: a(Token('Op', s)) >> tokval
+    op_ = lambda s: skip(op(s))
+    id = some(lambda t:
+        t.type in ['Name', 'Number', 'String']).named('id') >> tokval
+    make_nodes = lambda args: Statements([Node(n, args[-1]) for n in args[0]])
+    make_graph_attr = lambda args: DefAttrs(u'graph', [Attr(*args)])
+    make_edge = lambda x, x2, xs, attrs: Edge([x, x2] + xs, attrs)
+
+    node_id = id  # + maybe(port)
+    node_list = (
+        node_id +
+        many(op_(',') + node_id)
+        >> node_flatten)
+    a_list = (
+        id +
+        maybe(op_('=') + id) +
+        skip(maybe(op(',')))
+        >> unarg(Attr))
+    attr_list = (
+        many(op_('[') + many(a_list) + op_(']'))
+        >> flatten)
+    graph_attr = id + op_('=') + id >> make_graph_attr
+    multi_node_stmt = node_list + attr_list >> make_nodes
+    # We use a forward_decl becaue of circular definitions like (stmt_list ->
+    # stmt -> group -> stmt_list)
+    group = forward_decl()
+    edge_rhs = (op('->') | op('--') | op('<-') | op('<->')) + node_list
+    edge_stmt = (
+        node_list +
+        edge_rhs +
+        many(edge_rhs) +
+        attr_list
+        >> unarg(make_edge))
+    class_stmt = (
+        skip(n('class')) +
+        node_id +
+        attr_list
+        >> unarg(AttrClass))
+    plugin_stmt = (
+        skip(n('plugin')) +
+        node_id +
+        attr_list
+        >> unarg(AttrPlugin))
+    stmt = (
+          edge_stmt
+        | class_stmt
+        | plugin_stmt
+        | group
+        | graph_attr
+        | multi_node_stmt
+    )
+    stmt_list = many(stmt + skip(maybe(op(';'))))
+    group.define(
+        skip(n('group')) +
+        maybe(id) +
+        op_('{') +
+        stmt_list +
+        op_('}')
+        >> unarg(SubGraph))
+    graph = (
+        maybe(n('diagram') | n('blockdiag')) +
+        maybe(id) +
+        op_('{') +
+        stmt_list +
+        op_('}')
+        >> unarg(Graph))
+    dotfile = graph + skip(finished)
+
+    return dotfile.parse(seq)
+
+
+def sort_tree(tree):
+    def weight(node):
+        if isinstance(node, (Attr, DefAttrs, AttrPlugin, AttrClass)):
+            return 1
+        else:
+            return 2
+
+    def compare(a, b):
+        return cmp(weight(a), weight(b))
+
+    if hasattr(tree, 'stmts'):
+        tree.stmts.sort(compare)
+        for stmt in tree.stmts:
+            sort_tree(stmt)
+
+    return tree
+
+
+def parse_string(string):
+    try:
+        tree = parse(tokenize(string))
+        return sort_tree(tree)
+    except LexerError, e:
+        message = "Got unexpected token at line %d column %d" % e.place
+        raise ParseException(message)
+    except Exception, e:
+        raise ParseException(str(e))
+
+
+def parse_file(path):
+    input = codecs.open(path, 'r', 'utf-8').read()
+    return parse_string(input)
diff --git a/src/blockdiag/plugins/__init__.py b/src/blockdiag/plugins/__init__.py
new file mode 100644
index 0000000..8dffd4f
--- /dev/null
+++ b/src/blockdiag/plugins/__init__.py
@@ -0,0 +1,52 @@
+# -*- 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 pkg_resources import iter_entry_points
+
+node_handlers = []
+
+
+def load(plugins, diagram, **kwargs):
+    for name in plugins:
+        for ep in iter_entry_points('blockdiag_plugins', name):
+            module = ep.load()
+            if hasattr(module, 'setup'):
+                module.setup(module, diagram, **kwargs)
+            break
+        else:
+            msg = "WARNING: unknown plugin: %s\n" % name
+            raise AttributeError(msg)
+
+
+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)
+
+
+class NodeHandler(object):
+    def __init__(self, diagram, **kwargs):
+        self.diagram = diagram
+
+    def on_created(self, node):
+        pass
+
+    def on_attr_changed(self, node, attr):
+        pass
diff --git a/src/blockdiag/plugins/attributes.py b/src/blockdiag/plugins/attributes.py
new file mode 100644
index 0000000..5a89097
--- /dev/null
+++ b/src/blockdiag/plugins/attributes.py
@@ -0,0 +1,36 @@
+# -*- 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 import plugins
+
+
+class NodeAttributes(plugins.NodeHandler):
+    def __init__(self, diagram, **kwargs):
+        super(NodeAttributes, self).__init__(diagram, **kwargs)
+
+        node_klass = diagram._DiagramNode
+        for name, label in kwargs.items():
+            if label is None:
+                label = name
+            if name not in node_klass.desctable:
+                node_klass.desctable.insert(-1, name)
+
+            node_klass.attrname[name] = label
+            if not hasattr(node_klass, name):
+                setattr(node_klass, name, None)
+
+
+def setup(self, diagram, **kwargs):
+    plugins.install_node_handler(NodeAttributes(diagram, **kwargs))
diff --git a/src/blockdiag/plugins/autoclass.py b/src/blockdiag/plugins/autoclass.py
new file mode 100644
index 0000000..2e74c1d
--- /dev/null
+++ b/src/blockdiag/plugins/autoclass.py
@@ -0,0 +1,34 @@
+# -*- 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.
+
+import re
+from blockdiag import plugins
+
+
+class AutoClass(plugins.NodeHandler):
+    def on_created(self, node):
+        if node.id is None:
+            return
+
+        for name, klass in self.diagram.classes.items():
+            pattern = "_%s$" % re.escape(name)
+
+            if re.search(pattern, node.id):
+                node.label = re.sub(pattern, '', node.id)
+                node.set_attributes(klass.attrs)
+
+
+def setup(self, diagram, **kwargs):
+    plugins.install_node_handler(AutoClass(diagram, **kwargs))
diff --git a/src/blockdiag/tests/__init__.py b/src/blockdiag/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/blockdiag/tests/diagrams/auto_jumping_edge.diag b/src/blockdiag/tests/diagrams/auto_jumping_edge.diag
new file mode 100644
index 0000000..2041357
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/auto_jumping_edge.diag
@@ -0,0 +1,4 @@
+{
+  A -> B -> C, D;
+  A, C -> E;
+}
diff --git a/src/blockdiag/tests/diagrams/background_url_image.diag b/src/blockdiag/tests/diagrams/background_url_image.diag
new file mode 100644
index 0000000..6d53242
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/background_url_image.diag
@@ -0,0 +1,5 @@
+{
+  A [background = "http://python.org/images/python-logo.gif"];
+  B [background = "https://si2.twimg.com/profile_images/1287595787/tk0miya.jpg"];
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/beginpoint_color.diag b/src/blockdiag/tests/diagrams/beginpoint_color.diag
new file mode 100644
index 0000000..e0e3ae0
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/beginpoint_color.diag
@@ -0,0 +1,3 @@
+{
+  A [shape = beginpoint, color = pink];
+}
diff --git a/src/blockdiag/tests/diagrams/branched.diag b/src/blockdiag/tests/diagrams/branched.diag
new file mode 100644
index 0000000..5a9aae7
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/branched.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C;
+  A -> D -> E;
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/circular_ref.diag b/src/blockdiag/tests/diagrams/circular_ref.diag
new file mode 100644
index 0000000..a028996
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/circular_ref.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C -> B
+       B -> D
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/circular_ref_and_parent_node.diag b/src/blockdiag/tests/diagrams/circular_ref_and_parent_node.diag
new file mode 100644
index 0000000..2b376c7
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/circular_ref_and_parent_node.diag
@@ -0,0 +1,5 @@
+{
+  A -> B, C;
+  D -> B -> C -> D;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/circular_ref_to_root.diag b/src/blockdiag/tests/diagrams/circular_ref_to_root.diag
new file mode 100644
index 0000000..bb7d0bb
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/circular_ref_to_root.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C -> A
+       B -> D
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/circular_skipped_edge.diag b/src/blockdiag/tests/diagrams/circular_skipped_edge.diag
new file mode 100644
index 0000000..9afb986
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/circular_skipped_edge.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C -> A
+  A      -> C
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/define_class.diag b/src/blockdiag/tests/diagrams/define_class.diag
new file mode 100644
index 0000000..0a5397c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/define_class.diag
@@ -0,0 +1,8 @@
+{
+  class emphasis [style = dashed, color = red];
+
+  A -> B -> C;
+
+  A -> B [class = emphasis];
+  A [class = emphasis];
+}
diff --git a/src/blockdiag/tests/diagrams/diagram_attributes.diag b/src/blockdiag/tests/diagrams/diagram_attributes.diag
new file mode 100644
index 0000000..b5f20d9
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/diagram_attributes.diag
@@ -0,0 +1,17 @@
+blockdiag {
+  node_width = 160;
+  node_height = 160;
+  span_width = 32;
+  span_height = 32;
+  default_fontsize = 16;
+  default_shape = diamond
+  default_node_color = red
+  default_group_color = blue
+  default_linecolor = gray
+  default_textcolor = green
+
+  A -> B;
+  group {
+    B;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/diagram_attributes_order.diag b/src/blockdiag/tests/diagrams/diagram_attributes_order.diag
new file mode 100644
index 0000000..fb2d601
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/diagram_attributes_order.diag
@@ -0,0 +1,6 @@
+{
+  A;
+  default_node_color = red;
+  default_linecolor = red;
+  B;
+}
diff --git a/src/blockdiag/tests/diagrams/diagram_orientation.diag b/src/blockdiag/tests/diagrams/diagram_orientation.diag
new file mode 100644
index 0000000..e0b27a3
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/diagram_orientation.diag
@@ -0,0 +1,7 @@
+{
+  orientation = portrait;
+
+  A -> B -> C;
+       B -> D;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/edge_attribute.diag b/src/blockdiag/tests/diagrams/edge_attribute.diag
new file mode 100644
index 0000000..da131c3
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/edge_attribute.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C [color = red]
+  D -> E [dir = none]
+  F -> G [thick]
+}
diff --git a/src/blockdiag/tests/diagrams/edge_label.diag b/src/blockdiag/tests/diagrams/edge_label.diag
new file mode 100644
index 0000000..2fbd6ed
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/edge_label.diag
@@ -0,0 +1,3 @@
+{
+  A -> B [label = "test label"];
+}
diff --git a/src/blockdiag/tests/diagrams/edge_layout_landscape.diag b/src/blockdiag/tests/diagrams/edge_layout_landscape.diag
new file mode 100644
index 0000000..b627d41
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/edge_layout_landscape.diag
@@ -0,0 +1,6 @@
+{
+  edge_layout = flowchart;
+
+  A [shape = diamond];
+  A -> B, C;
+}
diff --git a/src/blockdiag/tests/diagrams/edge_layout_portrait.diag b/src/blockdiag/tests/diagrams/edge_layout_portrait.diag
new file mode 100644
index 0000000..ad3aaed
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/edge_layout_portrait.diag
@@ -0,0 +1,7 @@
+{
+  orientation = portrait
+  edge_layout = flowchart;
+
+  A [shape = diamond];
+  A -> B, C;
+}
diff --git a/src/blockdiag/tests/diagrams/edge_shape.diag b/src/blockdiag/tests/diagrams/edge_shape.diag
new file mode 100644
index 0000000..cfdae2c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/edge_shape.diag
@@ -0,0 +1,3 @@
+{
+  A -- B -> C <- D <-> E;
+}
diff --git a/src/blockdiag/tests/diagrams/edge_styles.diag b/src/blockdiag/tests/diagrams/edge_styles.diag
new file mode 100644
index 0000000..074c367
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/edge_styles.diag
@@ -0,0 +1,9 @@
+diagram {
+  A -> B [style = "none"];
+  B -> C [style = "solid"];
+  C -> D [style = "dashed"];
+  D -> E [style = "dotted"];
+  E -> F [hstyle = "generalization"];
+  F -> H [hstyle = "composition"];
+  H -> I [hstyle = "aggregation"];
+}
diff --git a/src/blockdiag/tests/diagrams/empty_group.diag b/src/blockdiag/tests/diagrams/empty_group.diag
new file mode 100644
index 0000000..35c4f88
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/empty_group.diag
@@ -0,0 +1,5 @@
+diagram {
+  group {
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/empty_group_declaration.diag b/src/blockdiag/tests/diagrams/empty_group_declaration.diag
new file mode 100644
index 0000000..9a87530
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/empty_group_declaration.diag
@@ -0,0 +1,10 @@
+{
+  group foo {
+    color = red;
+  }
+
+  group foo {
+    A;
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/empty_nested_group.diag b/src/blockdiag/tests/diagrams/empty_nested_group.diag
new file mode 100644
index 0000000..63fa0f4
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/empty_nested_group.diag
@@ -0,0 +1,7 @@
+diagram {
+  group {
+    group {
+    }
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/endpoint_color.diag b/src/blockdiag/tests/diagrams/endpoint_color.diag
new file mode 100644
index 0000000..14ff78d
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/endpoint_color.diag
@@ -0,0 +1,3 @@
+{
+  A [shape = endpoint, color = pink];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/belongs_to_two_groups.diag b/src/blockdiag/tests/diagrams/errors/belongs_to_two_groups.diag
new file mode 100644
index 0000000..d2f9deb
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/belongs_to_two_groups.diag
@@ -0,0 +1,9 @@
+diagram {
+  group {
+    A
+  }
+  group {
+    A
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/errors/group_follows_node.diag b/src/blockdiag/tests/diagrams/errors/group_follows_node.diag
new file mode 100644
index 0000000..0ba78f2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/group_follows_node.diag
@@ -0,0 +1,6 @@
+diagram {
+  A -> group {
+    B
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/errors/lexer_error.diag b/src/blockdiag/tests/diagrams/errors/lexer_error.diag
new file mode 100644
index 0000000..e625d1d
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/lexer_error.diag
@@ -0,0 +1,3 @@
+{
+  A - B
+}
diff --git a/src/blockdiag/tests/diagrams/errors/node_follows_group.diag b/src/blockdiag/tests/diagrams/errors/node_follows_group.diag
new file mode 100644
index 0000000..0ba78f2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/node_follows_group.diag
@@ -0,0 +1,6 @@
+diagram {
+  A -> group {
+    B
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_diagram_default_shape.diag b/src/blockdiag/tests/diagrams/errors/unknown_diagram_default_shape.diag
new file mode 100644
index 0000000..52dec66
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_diagram_default_shape.diag
@@ -0,0 +1,6 @@
+{
+  default_shape = "test_unknown_shape";
+
+  A;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_diagram_edge_layout.diag b/src/blockdiag/tests/diagrams/errors/unknown_diagram_edge_layout.diag
new file mode 100644
index 0000000..ab5f559
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_diagram_edge_layout.diag
@@ -0,0 +1,3 @@
+{
+  edge_layout = unknown;
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_diagram_orientation.diag b/src/blockdiag/tests/diagrams/errors/unknown_diagram_orientation.diag
new file mode 100644
index 0000000..420e857
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_diagram_orientation.diag
@@ -0,0 +1,3 @@
+{
+  orientation = unknown;
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_edge_class.diag b/src/blockdiag/tests/diagrams/errors/unknown_edge_class.diag
new file mode 100644
index 0000000..4010754
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_edge_class.diag
@@ -0,0 +1,3 @@
+{
+  A -> B [class = unknown];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_edge_dir.diag b/src/blockdiag/tests/diagrams/errors/unknown_edge_dir.diag
new file mode 100644
index 0000000..1186fc0
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_edge_dir.diag
@@ -0,0 +1,3 @@
+{
+  A -> B [dir = unknown];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_edge_hstyle.diag b/src/blockdiag/tests/diagrams/errors/unknown_edge_hstyle.diag
new file mode 100644
index 0000000..f3cf86d
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_edge_hstyle.diag
@@ -0,0 +1,3 @@
+{
+  A -> B [hstyle = unknown];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_edge_style.diag b/src/blockdiag/tests/diagrams/errors/unknown_edge_style.diag
new file mode 100644
index 0000000..96a0d57
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_edge_style.diag
@@ -0,0 +1,3 @@
+{
+  A -> B [style = unknown];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_group_class.diag b/src/blockdiag/tests/diagrams/errors/unknown_group_class.diag
new file mode 100644
index 0000000..afe6217
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_group_class.diag
@@ -0,0 +1,6 @@
+{
+  group {
+    class = unknown;
+    A;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_group_orientation.diag b/src/blockdiag/tests/diagrams/errors/unknown_group_orientation.diag
new file mode 100644
index 0000000..582e270
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_group_orientation.diag
@@ -0,0 +1,6 @@
+{
+  group {
+    orientation = unknown;
+    A;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_group_shape.diag b/src/blockdiag/tests/diagrams/errors/unknown_group_shape.diag
new file mode 100644
index 0000000..0ac1b68
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_group_shape.diag
@@ -0,0 +1,8 @@
+{
+  group {
+    shape = "test_unknown_shape";
+    A;
+  }
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_node_attribute.diag b/src/blockdiag/tests/diagrams/errors/unknown_node_attribute.diag
new file mode 100644
index 0000000..b2a35de
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_node_attribute.diag
@@ -0,0 +1,3 @@
+{
+  A [unknown_attribute = True];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_node_class.diag b/src/blockdiag/tests/diagrams/errors/unknown_node_class.diag
new file mode 100644
index 0000000..7d3683d
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_node_class.diag
@@ -0,0 +1,3 @@
+{
+  A [class = unknown];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_node_shape.diag b/src/blockdiag/tests/diagrams/errors/unknown_node_shape.diag
new file mode 100644
index 0000000..d140ec4
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_node_shape.diag
@@ -0,0 +1,5 @@
+{
+  A [shape = "test_unknown_shape"];
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_node_style.diag b/src/blockdiag/tests/diagrams/errors/unknown_node_style.diag
new file mode 100644
index 0000000..48a3cdd
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_node_style.diag
@@ -0,0 +1,3 @@
+{
+  A [style = unknown];
+}
diff --git a/src/blockdiag/tests/diagrams/errors/unknown_plugin.diag b/src/blockdiag/tests/diagrams/errors/unknown_plugin.diag
new file mode 100644
index 0000000..daff482
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/errors/unknown_plugin.diag
@@ -0,0 +1,5 @@
+{
+  plugin unknown_plugin;
+
+  A;
+}
diff --git a/src/blockdiag/tests/diagrams/flowable_node.diag b/src/blockdiag/tests/diagrams/flowable_node.diag
new file mode 100644
index 0000000..a44399c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/flowable_node.diag
@@ -0,0 +1,5 @@
+diagram {
+  B -> C
+  A -> B
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/folded_edge.diag b/src/blockdiag/tests/diagrams/folded_edge.diag
new file mode 100644
index 0000000..58d4f35
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/folded_edge.diag
@@ -0,0 +1,6 @@
+diagram {
+  A -> B -> C [nofolded]
+       B -> D -> E[folded]
+  D -> F
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/group_and_skipped_edge.diag b/src/blockdiag/tests/diagrams/group_and_skipped_edge.diag
new file mode 100644
index 0000000..81ad64f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_and_skipped_edge.diag
@@ -0,0 +1,9 @@
+{
+  A -> B -> C -> D;
+  A -> E -> D;
+  Z;
+
+  group {
+    B; C;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/group_attribute.diag b/src/blockdiag/tests/diagrams/group_attribute.diag
new file mode 100644
index 0000000..44133b7
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_attribute.diag
@@ -0,0 +1,10 @@
+{
+  group {
+    color = "red";
+    label = "group label";
+    shape = "line";
+
+    A;
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/group_children_height.diag b/src/blockdiag/tests/diagrams/group_children_height.diag
new file mode 100644
index 0000000..9c2b863
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_children_height.diag
@@ -0,0 +1,12 @@
+{
+  group {
+    A -> B;
+    A -> C;
+    A -> D;
+  }
+
+  B -> E;
+  D -> F;
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/group_children_order.diag b/src/blockdiag/tests/diagrams/group_children_order.diag
new file mode 100644
index 0000000..38480d2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_children_order.diag
@@ -0,0 +1,12 @@
+{
+  group {
+    A -> B;
+    A -> C;
+    A -> D;
+  }
+
+  D -> G;
+  B -> E;
+  C -> F;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/group_children_order2.diag b/src/blockdiag/tests/diagrams/group_children_order2.diag
new file mode 100644
index 0000000..17a95ac
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_children_order2.diag
@@ -0,0 +1,14 @@
+{
+  group {
+    A -> B;
+    A -> C;
+    A -> D;
+  }
+
+  A -> F;
+  D -> G;
+  B -> E;
+  C -> F;
+  Z;
+
+}
diff --git a/src/blockdiag/tests/diagrams/group_children_order3.diag b/src/blockdiag/tests/diagrams/group_children_order3.diag
new file mode 100644
index 0000000..484e648
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_children_order3.diag
@@ -0,0 +1,19 @@
+{
+  group {
+    A -> B;
+    A -> C;
+    A -> D;
+  }
+  group {
+    Q;
+  }
+
+  D -> G;
+  B -> E;
+  C -> F;
+
+  Q -> F;
+
+  Z;
+
+}
diff --git a/src/blockdiag/tests/diagrams/group_children_order4.diag b/src/blockdiag/tests/diagrams/group_children_order4.diag
new file mode 100644
index 0000000..513ea4c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_children_order4.diag
@@ -0,0 +1,9 @@
+diagram {
+  A -> B, C, D -> E;
+  Z;
+
+  group A { A }
+  group B { B }
+  group C { C }
+  group D { D }
+}
diff --git a/src/blockdiag/tests/diagrams/group_declare_as_node_attribute.diag b/src/blockdiag/tests/diagrams/group_declare_as_node_attribute.diag
new file mode 100644
index 0000000..22b22cd
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_declare_as_node_attribute.diag
@@ -0,0 +1,11 @@
+{
+  C [group = foo];
+  D [group = foo];
+
+  A -> B -> C;
+  D;
+  group foo {
+    E;
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/group_height.diag b/src/blockdiag/tests/diagrams/group_height.diag
new file mode 100644
index 0000000..240601f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_height.diag
@@ -0,0 +1,9 @@
+{
+  group {
+    B; C; D;
+  }
+  A -> B -> C;
+       B -> D;
+  A -> E;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/group_id_and_node_id_are_not_conflicted.diag b/src/blockdiag/tests/diagrams/group_id_and_node_id_are_not_conflicted.diag
new file mode 100644
index 0000000..fd7789f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_id_and_node_id_are_not_conflicted.diag
@@ -0,0 +1,7 @@
+diagram {
+  A -> B
+  group B {
+    C -> D
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/group_label.diag b/src/blockdiag/tests/diagrams/group_label.diag
new file mode 100644
index 0000000..050bf60
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_label.diag
@@ -0,0 +1,7 @@
+{
+  group {
+    label = "test label";
+
+    A;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/group_order.diag b/src/blockdiag/tests/diagrams/group_order.diag
new file mode 100644
index 0000000..6c8614e
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_order.diag
@@ -0,0 +1,8 @@
+diagram{
+  A; B; C;
+  group { B; }
+
+  A -> B;
+  A -> C;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/group_order2.diag b/src/blockdiag/tests/diagrams/group_order2.diag
new file mode 100644
index 0000000..c2a34ba
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_order2.diag
@@ -0,0 +1,13 @@
+{
+  A -> B;
+  A -> C -> D;
+  A -> E -> F;
+  Z;
+
+  group {
+    C; D
+  }
+  group {
+    E; F
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/group_order3.diag b/src/blockdiag/tests/diagrams/group_order3.diag
new file mode 100644
index 0000000..47a81d1
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_order3.diag
@@ -0,0 +1,16 @@
+diagram admin {
+  A;
+  group {
+    B;
+    C;
+    D;
+  }
+  E;
+  Z;
+
+  A -> B;
+  A -> E;
+
+  B -> C -> B;
+  B -> D -> B;
+}
diff --git a/src/blockdiag/tests/diagrams/group_orientation.diag b/src/blockdiag/tests/diagrams/group_orientation.diag
new file mode 100644
index 0000000..9a85351
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_orientation.diag
@@ -0,0 +1,10 @@
+{
+  A -> B;
+  Z;
+
+  group {
+    orientation = portrait
+    B -> C;
+    B -> D;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/group_sibling.diag b/src/blockdiag/tests/diagrams/group_sibling.diag
new file mode 100644
index 0000000..91f2fac
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_sibling.diag
@@ -0,0 +1,10 @@
+{
+  A -> B, C;
+       B -> D, E;
+
+  group {
+    C -> F;
+  }
+  Z;
+}
+
diff --git a/src/blockdiag/tests/diagrams/group_works_node_decorator.diag b/src/blockdiag/tests/diagrams/group_works_node_decorator.diag
new file mode 100644
index 0000000..1c87a02
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/group_works_node_decorator.diag
@@ -0,0 +1,9 @@
+diagram {
+  A -> B -> C
+  A -> B -> D
+  A -> E
+  group {
+    A; B; D; E
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/labeled_circular_ref.diag b/src/blockdiag/tests/diagrams/labeled_circular_ref.diag
new file mode 100644
index 0000000..de0f425
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/labeled_circular_ref.diag
@@ -0,0 +1,7 @@
+{
+  A [label = "foo"];
+  B [label = "bar"];
+  C [label = "baz"];
+
+  A -> C -> B -> C;
+}
diff --git a/src/blockdiag/tests/diagrams/large_group_and_node.diag b/src/blockdiag/tests/diagrams/large_group_and_node.diag
new file mode 100644
index 0000000..909b08c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/large_group_and_node.diag
@@ -0,0 +1,10 @@
+diagram {
+  group {
+    A -> B
+    A -> C
+    A -> D
+    A -> E
+  }
+  B -> F
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/large_group_and_node2.diag b/src/blockdiag/tests/diagrams/large_group_and_node2.diag
new file mode 100644
index 0000000..fcf1aaa
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/large_group_and_node2.diag
@@ -0,0 +1,7 @@
+diagram {
+  group {
+    A -> B -> C
+  }
+  C -> D -> E
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/large_group_and_two_nodes.diag b/src/blockdiag/tests/diagrams/large_group_and_two_nodes.diag
new file mode 100644
index 0000000..f4837a4
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/large_group_and_two_nodes.diag
@@ -0,0 +1,11 @@
+diagram {
+  group {
+    A -> B
+    A -> C
+    A -> D
+    A -> E
+  }
+  B -> F
+  C -> G
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/merge_groups.diag b/src/blockdiag/tests/diagrams/merge_groups.diag
new file mode 100644
index 0000000..60fecd2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/merge_groups.diag
@@ -0,0 +1,9 @@
+{
+  group hoge{
+    A -> B;
+  }
+  group hoge{
+    C -> D;
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/multiple_groups.diag b/src/blockdiag/tests/diagrams/multiple_groups.diag
new file mode 100644
index 0000000..17a1b77
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/multiple_groups.diag
@@ -0,0 +1,16 @@
+diagram {
+  group {
+    A;  B;  C;  D
+  }
+  group {
+    E;  F;  G
+  }
+  group {
+    H;  I
+  }
+  group {
+    J
+  }
+  A -> E -> H -> J
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/multiple_nested_groups.diag b/src/blockdiag/tests/diagrams/multiple_nested_groups.diag
new file mode 100644
index 0000000..da88c1f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/multiple_nested_groups.diag
@@ -0,0 +1,14 @@
+diagram {
+  group {
+    A -> B;
+    A -> C;
+
+    group {
+      B
+    }
+    group {
+      C
+    }
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/multiple_node_relation.diag b/src/blockdiag/tests/diagrams/multiple_node_relation.diag
new file mode 100644
index 0000000..1dddada
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/multiple_node_relation.diag
@@ -0,0 +1,4 @@
+{
+  A -> B, C -> D;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/multiple_nodes_definition.diag b/src/blockdiag/tests/diagrams/multiple_nodes_definition.diag
new file mode 100644
index 0000000..4ab717e
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/multiple_nodes_definition.diag
@@ -0,0 +1,4 @@
+{
+  A, B [color = red];
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/multiple_parent_node.diag b/src/blockdiag/tests/diagrams/multiple_parent_node.diag
new file mode 100644
index 0000000..df3152a
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/multiple_parent_node.diag
@@ -0,0 +1,6 @@
+{
+  A -> B;
+  C -> D;
+  E -> B;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/nested_group_orientation.diag b/src/blockdiag/tests/diagrams/nested_group_orientation.diag
new file mode 100644
index 0000000..94b1986
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/nested_group_orientation.diag
@@ -0,0 +1,13 @@
+{
+  group {
+    group {
+      orientation = portrait
+      A -> B;
+
+      group {
+        C;
+      }
+    }
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/nested_group_orientation2.diag b/src/blockdiag/tests/diagrams/nested_group_orientation2.diag
new file mode 100644
index 0000000..e3422f1
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/nested_group_orientation2.diag
@@ -0,0 +1,14 @@
+{
+  orientation = portrait;
+  A -> B;
+
+  group {
+    orientation = portrait
+    C -> D;
+
+    group {
+      E -> F;
+    }
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/nested_groups.diag b/src/blockdiag/tests/diagrams/nested_groups.diag
new file mode 100644
index 0000000..09b46f0
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/nested_groups.diag
@@ -0,0 +1,9 @@
+diagram {
+  group {
+    A;
+    group {
+      B;
+    }
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/nested_groups_and_edges.diag b/src/blockdiag/tests/diagrams/nested_groups_and_edges.diag
new file mode 100644
index 0000000..3de67a5
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/nested_groups_and_edges.diag
@@ -0,0 +1,11 @@
+diagram {
+  A -> B -> C;
+
+  group {
+    B;
+    group {
+      C;
+    }
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/nested_groups_work_node_decorator.diag b/src/blockdiag/tests/diagrams/nested_groups_work_node_decorator.diag
new file mode 100644
index 0000000..f566be0
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/nested_groups_work_node_decorator.diag
@@ -0,0 +1,11 @@
+diagram {
+  A; B;
+
+  group {
+    A
+    group {
+      B
+    }
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/nested_skipped_circular.diag b/src/blockdiag/tests/diagrams/nested_skipped_circular.diag
new file mode 100644
index 0000000..6bad6ee
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/nested_skipped_circular.diag
@@ -0,0 +1,7 @@
+diagram {
+  A -> B                -> F -> G
+       B -> C      -> E -> F
+            C -> D -> E
+  F -> A
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/node_attribute.diag b/src/blockdiag/tests/diagrams/node_attribute.diag
new file mode 100644
index 0000000..8a8e946
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_attribute.diag
@@ -0,0 +1,11 @@
+diagram {
+  A [label="B", color="red"];
+  B [label="double quoted"];
+  C [label='single quoted', color = 'red'];
+  D [label="'\"double\" quoted'", color = 'red'];
+  E [label='"\'single\' quoted"', color = 'red', numbered = "1"];
+  F [textcolor = red];
+  G [stacked];
+  H [fontsize = 16];
+  I [linecolor = red];
+}
diff --git a/src/blockdiag/tests/diagrams/node_attribute_and_group.diag b/src/blockdiag/tests/diagrams/node_attribute_and_group.diag
new file mode 100644
index 0000000..cbed6c9
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_attribute_and_group.diag
@@ -0,0 +1,14 @@
+diagram {
+  A [label = "foo", color = "red"];
+  B [label = "bar", color = "#888888"];
+  C [label = "baz", color = "blue"];
+
+  A -> B -> C;
+
+  group {
+    A;
+    B;
+  }
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_has_multilined_label.diag b/src/blockdiag/tests/diagrams/node_has_multilined_label.diag
new file mode 100644
index 0000000..f923b2b
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_has_multilined_label.diag
@@ -0,0 +1,5 @@
+{
+   A [label="foo
+bar"];
+   Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_height.diag b/src/blockdiag/tests/diagrams/node_height.diag
new file mode 100644
index 0000000..a60c5e4
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_height.diag
@@ -0,0 +1,6 @@
+{
+  A -> B -> C;
+       B -> D;
+  A -> E;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_icon.diag b/src/blockdiag/tests/diagrams/node_icon.diag
new file mode 100644
index 0000000..fc87f99
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_icon.diag
@@ -0,0 +1,6 @@
+{
+  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"];
+}
diff --git a/src/blockdiag/tests/diagrams/node_id_includes_dot.diag b/src/blockdiag/tests/diagrams/node_id_includes_dot.diag
new file mode 100644
index 0000000..e5dda6a
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_id_includes_dot.diag
@@ -0,0 +1,4 @@
+{
+  A.B -> C.D;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_in_group_follows_outer_node.diag b/src/blockdiag/tests/diagrams/node_in_group_follows_outer_node.diag
new file mode 100644
index 0000000..7b364f1
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_in_group_follows_outer_node.diag
@@ -0,0 +1,7 @@
+diagram {
+  group {
+    A -> B
+  }
+  B -> C
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/node_link.diag b/src/blockdiag/tests/diagrams/node_link.diag
new file mode 100644
index 0000000..ff6ad06
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_link.diag
@@ -0,0 +1,9 @@
+{
+  A [href = "http://www.yahoo.co.jp/"];
+  B [href = "http://www.yahoo.co.jp/"];
+
+  group {
+    href = "http://www.disney.co.jp";
+    C; D;
+  }
+}
diff --git a/src/blockdiag/tests/diagrams/node_rotated_labels.diag b/src/blockdiag/tests/diagrams/node_rotated_labels.diag
new file mode 100644
index 0000000..936666f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_rotated_labels.diag
@@ -0,0 +1,7 @@
+{
+  A [rotate = 0];
+  B [rotate = 90];
+  C [rotate = 180];
+  D [rotate = 270];
+  E [rotate = 360];
+}
diff --git a/src/blockdiag/tests/diagrams/node_shape.diag b/src/blockdiag/tests/diagrams/node_shape.diag
new file mode 100644
index 0000000..b19d122
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_shape.diag
@@ -0,0 +1,29 @@
+{
+  A [shape = "box"];
+  B [shape = "roundedbox"];
+  C [shape = "diamond"];
+  D [shape = "ellipse"];
+  E [shape = "note"];
+  F [shape = "cloud"];
+  G [shape = "mail"];
+  H [shape = "beginpoint"];
+  I [shape = "endpoint"];
+  J [shape = "minidiamond"];
+
+  K [shape = "flowchart.condition"];
+  L [shape = "flowchart.database"];
+  M [shape = "flowchart.input"];
+  N [shape = "flowchart.loopin"];
+  O [shape = "flowchart.loopout"];
+
+  P [shape = "actor"];
+  Q [shape = "flowchart.terminator"];
+  R [shape = "textbox"];
+  S [shape = "dots"];
+  T [shape = "none"];
+
+  U [shape = "square"];
+  V [shape = "circle"];
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_shape_background.diag b/src/blockdiag/tests/diagrams/node_shape_background.diag
new file mode 100644
index 0000000..edeef4c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_shape_background.diag
@@ -0,0 +1,29 @@
+{
+  A [shape = "box", background = "src/blockdiag/tests/diagrams/white.gif"];
+  B [shape = "roundedbox", background = "src/blockdiag/tests/diagrams/white.gif"];
+  C [shape = "diamond", background = "src/blockdiag/tests/diagrams/white.gif"];
+  D [shape = "ellipse", background = "src/blockdiag/tests/diagrams/white.gif"];
+  E [shape = "note", background = "src/blockdiag/tests/diagrams/white.gif"];
+  F [shape = "cloud", background = "src/blockdiag/tests/diagrams/white.gif"];
+  G [shape = "mail", background = "src/blockdiag/tests/diagrams/white.gif"];
+  H [shape = "beginpoint", background = "src/blockdiag/tests/diagrams/white.gif"];
+  I [shape = "endpoint", background = "src/blockdiag/tests/diagrams/white.gif"];
+  J [shape = "minidiamond", background = "src/blockdiag/tests/diagrams/white.gif"];
+
+  K [shape = "flowchart.condition", background = "src/blockdiag/tests/diagrams/white.gif"];
+  L [shape = "flowchart.database", background = "src/blockdiag/tests/diagrams/white.gif"];
+  M [shape = "flowchart.input", background = "src/blockdiag/tests/diagrams/white.gif"];
+  N [shape = "flowchart.loopin", background = "src/blockdiag/tests/diagrams/white.gif"];
+  O [shape = "flowchart.loopout", background = "src/blockdiag/tests/diagrams/white.gif"];
+
+  P [shape = "actor", background = "src/blockdiag/tests/diagrams/white.gif"];
+  Q [shape = "flowchart.terminator", background = "src/blockdiag/tests/diagrams/white.gif"];
+  R [shape = "textbox", background = "src/blockdiag/tests/diagrams/white.gif"];
+  S [shape = "dots", background = "src/blockdiag/tests/diagrams/white.gif"];
+  T [shape = "none", background = "src/blockdiag/tests/diagrams/white.gif"];
+
+  U [shape = "square", background = "src/blockdiag/tests/diagrams/white.gif"];
+  V [shape = "circle", background = "src/blockdiag/tests/diagrams/white.gif"];
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_shape_namespace.diag b/src/blockdiag/tests/diagrams/node_shape_namespace.diag
new file mode 100644
index 0000000..2a150d4
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_shape_namespace.diag
@@ -0,0 +1,8 @@
+{
+  shape_namespace = "flowchart";
+
+  A [shape = "flowchart.condition"];
+  B [shape = "condition"];
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/node_style_dasharray.diag b/src/blockdiag/tests/diagrams/node_style_dasharray.diag
new file mode 100644
index 0000000..e75d26e
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_style_dasharray.diag
@@ -0,0 +1,7 @@
+{
+  A [style = "2,2,4,2"];
+  B [shape = diamond, style = "2,2,4,2"];
+  C [shape = ellipse, style = "2,2,4,2"];
+  D [shape = flowchart.database, style = "2,2,4,2"];
+}
+
diff --git a/src/blockdiag/tests/diagrams/node_styles.diag b/src/blockdiag/tests/diagrams/node_styles.diag
new file mode 100644
index 0000000..e94e61b
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_styles.diag
@@ -0,0 +1,5 @@
+diagram {
+  A [shape = "roundedbox", style = "dotted"];
+  B [shape = "ellipse", style = "dashed"];
+  C [shape = "flowchart.database", style = "dashed"];
+}
diff --git a/src/blockdiag/tests/diagrams/node_width_and_height.diag b/src/blockdiag/tests/diagrams/node_width_and_height.diag
new file mode 100644
index 0000000..0ad9fb1
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/node_width_and_height.diag
@@ -0,0 +1,6 @@
+{
+  A -> B, C;
+
+  A [height = 80];
+  C [width = 256];
+}
diff --git a/src/blockdiag/tests/diagrams/non_rhombus_relation_height.diag b/src/blockdiag/tests/diagrams/non_rhombus_relation_height.diag
new file mode 100644
index 0000000..7ab9310
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/non_rhombus_relation_height.diag
@@ -0,0 +1,8 @@
+{
+  A -> B -> C;
+  D;
+  E -> F, G, J;
+       G -> H, I;
+       J -> K;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/outer_node_follows_node_in_group.diag b/src/blockdiag/tests/diagrams/outer_node_follows_node_in_group.diag
new file mode 100644
index 0000000..5fa91fd
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/outer_node_follows_node_in_group.diag
@@ -0,0 +1,7 @@
+diagram {
+  group {
+    B -> C
+  }
+  A -> B
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/plugin_attributes.diag b/src/blockdiag/tests/diagrams/plugin_attributes.diag
new file mode 100644
index 0000000..85f797b
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/plugin_attributes.diag
@@ -0,0 +1,6 @@
+{
+  plugin attributes [test_attr1, test_attr2 = name, test_attr3 = name];
+
+  A [test_attr1 = 1, test_attr2 = 2, test_attr3 = 3];
+  B;
+}
diff --git a/src/blockdiag/tests/diagrams/plugin_autoclass.diag b/src/blockdiag/tests/diagrams/plugin_autoclass.diag
new file mode 100644
index 0000000..132eaf1
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/plugin_autoclass.diag
@@ -0,0 +1,7 @@
+{
+  plugin autoclass;
+  class emphasis [style = dashed, color = red];
+
+  A_emphasis -> B_emphasis;
+  A_emphasis -> C;
+}
diff --git a/src/blockdiag/tests/diagrams/portrait_dots.diag b/src/blockdiag/tests/diagrams/portrait_dots.diag
new file mode 100644
index 0000000..f49238f
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/portrait_dots.diag
@@ -0,0 +1,5 @@
+{
+  orientation = portrait;
+
+  A [shape = dots];
+}
diff --git a/src/blockdiag/tests/diagrams/quoted_node_id.diag b/src/blockdiag/tests/diagrams/quoted_node_id.diag
new file mode 100644
index 0000000..0d69026
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/quoted_node_id.diag
@@ -0,0 +1,4 @@
+{
+  A -> "A" -> 'A' -> "'A'" -> B;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/reverse_multiple_groups.diag b/src/blockdiag/tests/diagrams/reverse_multiple_groups.diag
new file mode 100644
index 0000000..8792260
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/reverse_multiple_groups.diag
@@ -0,0 +1,16 @@
+diagram {
+  group {
+    A;  B;  C;  D
+  }
+  group {
+    E;  F;  G
+  }
+  group {
+    H;  I
+  }
+  group {
+    J
+  }
+  J -> H -> E -> A
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/rhombus_relation_height.diag b/src/blockdiag/tests/diagrams/rhombus_relation_height.diag
new file mode 100644
index 0000000..ae4baf3
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/rhombus_relation_height.diag
@@ -0,0 +1,4 @@
+{
+  A -> B, C -> D -> E, F;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/self_ref.diag b/src/blockdiag/tests/diagrams/self_ref.diag
new file mode 100644
index 0000000..d4d953a
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/self_ref.diag
@@ -0,0 +1,4 @@
+diagram {
+  A -> B -> B
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/separate1.diag b/src/blockdiag/tests/diagrams/separate1.diag
new file mode 100644
index 0000000..593ae66
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/separate1.diag
@@ -0,0 +1,16 @@
+{
+  A -> B -> C -> D;
+
+  group outer {
+    label = "outer"
+    B; D;
+
+    group inner {
+      label = "inner"
+      color = skyblue;
+
+      C -> E -> F;
+    }
+  }
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/separate2.diag b/src/blockdiag/tests/diagrams/separate2.diag
new file mode 100644
index 0000000..98450e4
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/separate2.diag
@@ -0,0 +1,22 @@
+diagram {
+  A -> B;
+  A -> C;
+
+  E -> F;
+  C -> G;
+  D -> H;
+
+  group outer {
+    label = "group 1";
+    B -> E -> C;
+
+    group inner {
+      label = "sub group 1";
+      color = skyblue
+
+      C -> D;
+    }
+  }
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/simple_group.diag b/src/blockdiag/tests/diagrams/simple_group.diag
new file mode 100644
index 0000000..68bf4c2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/simple_group.diag
@@ -0,0 +1,7 @@
+diagram {
+  group {
+    A -> B
+    A -> C
+  }
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/single_edge.diag b/src/blockdiag/tests/diagrams/single_edge.diag
new file mode 100644
index 0000000..f36edf5
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/single_edge.diag
@@ -0,0 +1,3 @@
+diagram {
+  A -> B;
+}
diff --git a/src/blockdiag/tests/diagrams/single_node.diag b/src/blockdiag/tests/diagrams/single_node.diag
new file mode 100644
index 0000000..203f740
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/single_node.diag
@@ -0,0 +1,3 @@
+diagram {
+  A;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_circular.diag b/src/blockdiag/tests/diagrams/skipped_circular.diag
new file mode 100644
index 0000000..fd7ce2b
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_circular.diag
@@ -0,0 +1,6 @@
+diagram {
+  A      -> C
+  A -> B -> C
+  C -> A
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge.diag b/src/blockdiag/tests/diagrams/skipped_edge.diag
new file mode 100644
index 0000000..5a99655
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> C
+  A      -> C
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_down.diag b/src/blockdiag/tests/diagrams/skipped_edge_down.diag
new file mode 100644
index 0000000..3ca87f3
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_down.diag
@@ -0,0 +1,4 @@
+{
+  A; B; C;
+  A -> C [folded];
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown.diag b/src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown.diag
new file mode 100644
index 0000000..2eeaa33
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown.diag
@@ -0,0 +1,8 @@
+{
+  edge_layout = flowchart;
+
+  A -> B, C;
+  C -> D;
+  A -> D;
+}
+
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown2.diag b/src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown2.diag
new file mode 100644
index 0000000..e4e0938
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_flowchart_rightdown2.diag
@@ -0,0 +1,8 @@
+{
+  edge_layout = flowchart;
+
+  A;
+  B -> C;
+  A -> C [folded];
+}
+
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_leftdown.diag b/src/blockdiag/tests/diagrams/skipped_edge_leftdown.diag
new file mode 100644
index 0000000..265d1d2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_leftdown.diag
@@ -0,0 +1,6 @@
+{
+  A -> B -> C, D; 
+  E;
+  F -> G;
+  C -> G [folded];
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_portrait_down.diag b/src/blockdiag/tests/diagrams/skipped_edge_portrait_down.diag
new file mode 100644
index 0000000..f9907f2
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_portrait_down.diag
@@ -0,0 +1,6 @@
+{
+  orientation = portrait;
+
+  A -> B -> C;
+  A -> C;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown.diag b/src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown.diag
new file mode 100644
index 0000000..2f0dc1c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown.diag
@@ -0,0 +1,8 @@
+{
+  orientation = portrait;
+  edge_layout = flowchart;
+
+  A -> B, C;
+  C -> D;
+  A -> D;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown2.diag b/src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown2.diag
new file mode 100644
index 0000000..3cbceab
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_portrait_flowchart_rightdown2.diag
@@ -0,0 +1,9 @@
+{
+  orientation = portrait;
+  edge_layout = flowchart;
+
+  A;
+  B -> C;
+  A -> C [folded];
+}
+
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_portrait_leftdown.diag b/src/blockdiag/tests/diagrams/skipped_edge_portrait_leftdown.diag
new file mode 100644
index 0000000..3e93a9a
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_portrait_leftdown.diag
@@ -0,0 +1,6 @@
+{
+  orientation = portrait;
+
+  A -> B -> C;
+  D -> C, E;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_portrait_right.diag b/src/blockdiag/tests/diagrams/skipped_edge_portrait_right.diag
new file mode 100644
index 0000000..d548025
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_portrait_right.diag
@@ -0,0 +1,6 @@
+{
+  orientation = portrait;
+
+  A; B; C;
+  A -> C [folded];
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_portrait_rightdown.diag b/src/blockdiag/tests/diagrams/skipped_edge_portrait_rightdown.diag
new file mode 100644
index 0000000..03bd9fb
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_portrait_rightdown.diag
@@ -0,0 +1,8 @@
+{
+  orientation = portrait;
+
+  A -> B, C;
+  B -> D;
+  C -> E;
+  A -> E;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_right.diag b/src/blockdiag/tests/diagrams/skipped_edge_right.diag
new file mode 100644
index 0000000..bd961f0
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_right.diag
@@ -0,0 +1,4 @@
+{
+  A -> B -> C;
+  A -> C;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_rightdown.diag b/src/blockdiag/tests/diagrams/skipped_edge_rightdown.diag
new file mode 100644
index 0000000..28734bc
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_rightdown.diag
@@ -0,0 +1,5 @@
+{
+  A -> B -> C, D;
+  A -> C, D;
+}
+
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_rightup.diag b/src/blockdiag/tests/diagrams/skipped_edge_rightup.diag
new file mode 100644
index 0000000..ad7231c
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_rightup.diag
@@ -0,0 +1,4 @@
+{
+  A -> B -> C;
+  D -> C, E;
+}
diff --git a/src/blockdiag/tests/diagrams/skipped_edge_up.diag b/src/blockdiag/tests/diagrams/skipped_edge_up.diag
new file mode 100644
index 0000000..18fd187
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_edge_up.diag
@@ -0,0 +1,5 @@
+{
+  A; B; C;
+  C -> A [folded];
+}
+
diff --git a/src/blockdiag/tests/diagrams/skipped_twin_circular.diag b/src/blockdiag/tests/diagrams/skipped_twin_circular.diag
new file mode 100644
index 0000000..c407963
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/skipped_twin_circular.diag
@@ -0,0 +1,7 @@
+{
+  A -> B      -> E -> B;
+       B -> C -> E;
+       B -> D -> E;
+  Z;
+}
+
diff --git a/src/blockdiag/tests/diagrams/slided_children.diag b/src/blockdiag/tests/diagrams/slided_children.diag
new file mode 100644
index 0000000..5551b94
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/slided_children.diag
@@ -0,0 +1,8 @@
+{
+   C; F;
+
+   A -> B -> C;
+   B -> G -> H;
+   B -> F -> H;
+   A -> D -> E -> F -> H;
+}
diff --git a/src/blockdiag/tests/diagrams/stacked_node_and_edges.diag b/src/blockdiag/tests/diagrams/stacked_node_and_edges.diag
new file mode 100644
index 0000000..b1a3c89
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/stacked_node_and_edges.diag
@@ -0,0 +1,6 @@
+{
+  A -> B;
+  A -> C [folded];
+
+  A [stacked];
+}
diff --git a/src/blockdiag/tests/diagrams/triple_branched.diag b/src/blockdiag/tests/diagrams/triple_branched.diag
new file mode 100644
index 0000000..8155ac9
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/triple_branched.diag
@@ -0,0 +1,6 @@
+diagram {
+  A -> D
+  B -> D
+  C -> D
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/twin_circular_ref.diag b/src/blockdiag/tests/diagrams/twin_circular_ref.diag
new file mode 100644
index 0000000..44c55af
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/twin_circular_ref.diag
@@ -0,0 +1,5 @@
+{
+  A -> B -> C -> B;
+  A -> D -> C -> D;
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/twin_circular_ref_to_root.diag b/src/blockdiag/tests/diagrams/twin_circular_ref_to_root.diag
new file mode 100644
index 0000000..31495dd
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/twin_circular_ref_to_root.diag
@@ -0,0 +1,5 @@
+diagram {
+  A -> B -> A
+  A -> C -> A
+  Z
+}
diff --git a/src/blockdiag/tests/diagrams/twin_forked.diag b/src/blockdiag/tests/diagrams/twin_forked.diag
new file mode 100644
index 0000000..896899d
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/twin_forked.diag
@@ -0,0 +1,7 @@
+{
+  A -> B, C;
+  B -> D -> E, F;
+  C, F -> G;
+
+  Z;
+}
diff --git a/src/blockdiag/tests/diagrams/twin_multiple_parent_node.diag b/src/blockdiag/tests/diagrams/twin_multiple_parent_node.diag
new file mode 100644
index 0000000..a0d0d61
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/twin_multiple_parent_node.diag
@@ -0,0 +1,7 @@
+{
+  A -> B;
+  Z;
+  E -> D;
+  C -> D;
+  C -> B;
+}
diff --git a/src/blockdiag/tests/diagrams/two_edges.diag b/src/blockdiag/tests/diagrams/two_edges.diag
new file mode 100644
index 0000000..6683b17
--- /dev/null
+++ b/src/blockdiag/tests/diagrams/two_edges.diag
@@ -0,0 +1,3 @@
+diagram {
+  A -> B -> C;
+}
diff --git a/src/blockdiag/tests/test_boot_params.py b/src/blockdiag/tests/test_boot_params.py
new file mode 100644
index 0000000..1555fee
--- /dev/null
+++ b/src/blockdiag/tests/test_boot_params.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import tempfile
+import unittest2
+from utils import argv_wrapper, assertRaises
+import blockdiag
+from blockdiag.command import BlockdiagOptions
+from blockdiag.utils.bootstrap import detectfont
+
+
+class TestBootParams(unittest2.TestCase):
+    def setUp(self):
+        self.parser = BlockdiagOptions(blockdiag)
+
+    @argv_wrapper
+    def test_type_option(self):
+        sys.argv = ['', '-Tsvg', 'input.diag']
+        self.parser.parse()
+
+        sys.argv = ['', '-TSVG', 'input.diag']
+        self.parser.parse()
+
+        sys.argv = ['', '-TSvg', 'input.diag']
+        self.parser.parse()
+
+        sys.argv = ['', '-Tpng', 'input.diag']
+        self.parser.parse()
+
+        sys.argv = ['', '-Tpdf', 'input.diag']
+        self.parser.parse()
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_invalid_type_option(self):
+        sys.argv = ['', '-Tsvgz', 'input.diag']
+        self.parser.parse()
+
+    @argv_wrapper
+    def test_separate_option(self):
+        sys.argv = ['', '-Tsvg', '--separate', 'input.diag']
+        self.parser.parse()
+
+        sys.argv = ['', '-Tpng', '--separate', 'input.diag']
+        self.parser.parse()
+
+        sys.argv = ['', '-Tpdf', '--separate', 'input.diag']
+        self.parser.parse()
+
+    @argv_wrapper
+    def test_svg_nodoctype_option(self):
+        sys.argv = ['', '-Tsvg', '--nodoctype', 'input.diag']
+        self.parser.parse()
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_png_nodoctype_option(self):
+        sys.argv = ['', '-Tpng', '--nodoctype', 'input.diag']
+        self.parser.parse()
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_pdf_nodoctype_option(self):
+        sys.argv = ['', '-Tpdf', '--nodoctype', 'input.diag']
+        self.parser.parse()
+
+    @argv_wrapper
+    def test_config_option(self):
+        try:
+            tmp = tempfile.mkstemp()
+            sys.argv = ['', '-c', tmp[1], 'input.diag']
+            self.parser.parse()
+        finally:
+            os.close(tmp[0])
+            os.unlink(tmp[1])
+
+    @argv_wrapper
+    def test_config_option_with_bom(self):
+        try:
+            tmp = tempfile.mkstemp()
+            fp = os.fdopen(tmp[0], 'wt')
+            fp.write("\xEF\xBB\xBF[blockdiag]\n")
+            fp.close()
+
+            sys.argv = ['', '-c', tmp[1], 'input.diag']
+            self.parser.parse()
+        finally:
+            os.unlink(tmp[1])
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_invalid_config_option(self):
+        sys.argv = ['', '-c', '/unknown_config_file', 'input.diag']
+        self.parser.parse()
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_invalid_dir_config_option(self):
+        try:
+            tmp = tempfile.mkdtemp()
+
+            sys.argv = ['', '-c', tmp, 'input.diag']
+            self.parser.parse()
+        finally:
+            os.rmdir(tmp)
+
+    @argv_wrapper
+    def test_config_option_fontpath(self):
+        try:
+            tmp = tempfile.mkstemp()
+            config = '[blockdiag]\nfontpath = /path/to/font\n'
+            os.fdopen(tmp[0], 'wt').write(config)
+
+            sys.argv = ['', '-c', tmp[1], 'input.diag']
+            options = self.parser.parse()
+            self.assertEqual(options.font, ['/path/to/font'])
+        finally:
+            os.unlink(tmp[1])
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_not_exist_font_config_option(self):
+        sys.argv = ['', '-f', '/font_is_not_exist', 'input.diag']
+        options = self.parser.parse()
+        detectfont(options)
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_not_exist_font_config_option2(self):
+        sys.argv = ['', '-f', '/font_is_not_exist',
+                    '-f', '/font_is_not_exist2', 'input.diag']
+        options = self.parser.parse()
+        detectfont(options)
+
+    @argv_wrapper
+    def test_auto_font_detection(self):
+        sys.argv = ['', 'input.diag']
+        options = self.parser.parse()
+        fontpath = detectfont(options)
+        self.assertTrue(fontpath)
+
+    @assertRaises(RuntimeError)
+    @argv_wrapper
+    def test_not_exist_fontmap_config(self):
+        sys.argv = ['', '--fontmap', '/fontmap_is_not_exist', 'input.diag']
+        options = self.parser.parse()
+        fontpath = detectfont(options)
+        self.assertTrue(fontpath)
+
+    @assertRaises(RuntimeError)
+    def test_unknown_image_driver(self):
+        from blockdiag.drawer import DiagramDraw
+        from blockdiag.elements import Diagram
+
+        DiagramDraw('unknown', Diagram())
diff --git a/src/blockdiag/tests/test_builder.py b/src/blockdiag/tests/test_builder.py
new file mode 100644
index 0000000..3608cc8
--- /dev/null
+++ b/src/blockdiag/tests/test_builder.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utils import __build_diagram, __validate_node_attributes
+
+
+def test_diagram_attributes():
+    diagram = __build_diagram('diagram_attributes.diag')
+
+    eq_(160, diagram.node_width)
+    eq_(160, diagram.node_height)
+    eq_(32, diagram.span_width)
+    eq_(32, diagram.span_height)
+    eq_((128, 128, 128), diagram.linecolor)       # gray
+    eq_('diamond', diagram.nodes[0].shape)
+    eq_((255, 0, 0), diagram.nodes[0].color)      # red
+    eq_((0, 128, 0), diagram.nodes[0].textcolor)  # green
+    eq_(16, diagram.nodes[0].fontsize)
+    eq_((0, 0, 255), diagram.nodes[1].color)      # blue
+    eq_((0, 128, 0), diagram.nodes[1].textcolor)  # green
+    eq_(16, diagram.nodes[1].fontsize)
+
+    eq_((128, 128, 128), diagram.edges[0].color)  # gray
+    eq_((0, 128, 0), diagram.edges[0].textcolor)  # green
+    eq_(16, diagram.edges[0].fontsize)
+
+
+def test_diagram_attributes_order_diagram():
+    colors = {'A': (255, 0, 0), 'B': (255, 0, 0)}
+    linecolors = {'A': (255, 0, 0), 'B': (255, 0, 0)}
+    __validate_node_attributes('diagram_attributes_order.diag',
+                               color=colors, linecolor=linecolors)
+
+
+def test_circular_ref_to_root_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                  'D': (2, 1), 'Z': (0, 2)}
+    __validate_node_attributes('circular_ref_to_root.diag', xy=positions)
+
+
+def test_circular_ref_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                  'D': (2, 1), 'Z': (0, 2)}
+    __validate_node_attributes('circular_ref.diag', xy=positions)
+
+
+def test_circular_ref_and_parent_node_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1),
+                 'D': (2, 1), 'Z': (0, 2)}
+    __validate_node_attributes('circular_ref_and_parent_node.diag',
+                               xy=positions)
+
+
+def test_labeled_circular_ref_diagram():
+    positions = {'A': (0, 0), 'B': (2, 0), 'C': (1, 0),
+                 'Z': (0, 1)}
+    __validate_node_attributes('labeled_circular_ref.diag', xy=positions)
+
+
+def test_twin_forked_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 2), 'D': (2, 0),
+                  'E': (3, 0), 'F': (3, 1), 'G': (4, 1), 'Z': (0, 3)}
+    __validate_node_attributes('twin_forked.diag', xy=positions)
+
+
+def test_skipped_edge_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('skipped_edge.diag', xy=positions)
+
+
+def test_circular_skipped_edge_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('circular_skipped_edge.diag', xy=positions)
+
+
+def test_triple_branched_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'C': (0, 2),
+                 'D': (1, 0), 'Z': (0, 3)}
+    __validate_node_attributes('triple_branched.diag', xy=positions)
+
+
+def test_twin_circular_ref_to_root_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('twin_circular_ref_to_root.diag', xy=positions)
+
+
+def test_twin_circular_ref_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('twin_circular_ref.diag', xy=positions)
+
+
+def test_skipped_circular_diagram():
+    positions = {'A': (0, 0), 'B': (1, 1), 'C': (2, 0),
+                 'Z': (0, 2)}
+    __validate_node_attributes('skipped_circular.diag', xy=positions)
+
+
+def test_skipped_twin_circular_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 1),
+                 'D': (2, 2), 'E': (3, 0), 'Z': (0, 3)}
+    __validate_node_attributes('skipped_twin_circular.diag', xy=positions)
+
+
+def test_nested_skipped_circular_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 1),
+                 'D': (3, 2), 'E': (4, 1), 'F': (5, 0),
+                 'G': (6, 0), 'Z': (0, 3)}
+    __validate_node_attributes('nested_skipped_circular.diag', xy=positions)
+
+
+def test_self_ref_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'Z': (0, 1)}
+    __validate_node_attributes('self_ref.diag', xy=positions)
+
+
+def test_diagram_orientation_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'C': (0, 2),
+                 'D': (1, 2), 'Z': (2, 0)}
+    __validate_node_attributes('diagram_orientation.diag', xy=positions)
+
+
+def test_nested_group_orientation2_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'C': (0, 2), 'D': (1, 2),
+                 'E': (2, 2), 'F': (2, 3), 'Z': (3, 0)}
+    __validate_node_attributes('nested_group_orientation2.diag', xy=positions)
+
+
+def test_slided_children_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'D': (1, 3),
+                 'E': (2, 3), 'F': (3, 2), 'G': (2, 1), 'H': (4, 1)}
+    __validate_node_attributes('slided_children.diag', xy=positions)
+
+
+def test_rhombus_relation_height_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (2, 0),
+                 'E': (3, 0), 'F': (3, 1), 'Z': (0, 2)}
+    __validate_node_attributes('rhombus_relation_height.diag', xy=positions)
+
+
+def test_non_rhombus_relation_height_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'D': (0, 1),
+                 'E': (0, 2), 'F': (1, 2), 'G': (1, 3), 'H': (2, 3),
+                 'I': (2, 4), 'J': (1, 5), 'K': (2, 5), 'Z': (0, 6)}
+    __validate_node_attributes('non_rhombus_relation_height.diag',
+                               xy=positions)
+
+
+def test_define_class_diagram():
+    colors = {'A': (255, 0, 0), 'B': (255, 255, 255), 'C': (255, 255, 255)}
+    styles = {'A': 'dashed', 'B': None, 'C': None}
+
+    edge_colors = {('A', 'B'): (255, 0, 0), ('B', 'C'): (0, 0, 0)}
+    edge_styles = {('A', 'B'): 'dashed', ('B', 'C'): None}
+
+    __validate_node_attributes('define_class.diag',
+                               color=colors, edge_color=edge_colors,
+                               style=styles, edge_style=edge_styles)
diff --git a/src/blockdiag/tests/test_builder_edge.py b/src/blockdiag/tests/test_builder_edge.py
new file mode 100644
index 0000000..fe94146
--- /dev/null
+++ b/src/blockdiag/tests/test_builder_edge.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utils import stderr_wrapper, __build_diagram, __validate_node_attributes
+
+
+def test_single_edge_diagram():
+    diagram = __build_diagram('single_edge.diag')
+
+    assert len(diagram.nodes) == 2
+    assert len(diagram.edges) == 1
+
+    positions = {'A': (0, 0), 'B': (1, 0)}
+    labels = {'A': 'A', 'B': 'B'}
+    __validate_node_attributes('single_edge.diag', xy=positions, label=labels)
+
+
+def test_two_edges_diagram():
+    diagram = __build_diagram('two_edges.diag')
+
+    assert len(diagram.nodes) == 3
+    assert len(diagram.edges) == 2
+
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0)}
+    __validate_node_attributes('two_edges.diag', xy=positions)
+
+
+def test_edge_shape():
+    diagram = __build_diagram('edge_shape.diag')
+
+    for edge in diagram.edges:
+        if edge.node1.id == 'A':
+            assert edge.dir == 'none'
+        elif edge.node1.id == 'B':
+            assert edge.dir == 'forward'
+        elif edge.node1.id == 'C':
+            assert edge.dir == 'back'
+        elif edge.node1.id == 'D':
+            assert edge.dir == 'both'
+
+
+def test_edge_attribute():
+    diagram = __build_diagram('edge_attribute.diag')
+
+    for edge in diagram.edges:
+        if edge.node1.id == 'D':
+            assert edge.dir == 'none'
+            assert edge.color == (0, 0, 0)
+            assert edge.thick == None
+        elif edge.node1.id == 'F':
+            assert edge.dir == 'forward'
+            assert edge.color == (0, 0, 0)
+            assert edge.thick == 3
+        else:
+            assert edge.dir == 'forward'
+            assert edge.color == (255, 0, 0)  # red
+            assert edge.thick == None
+
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (0, 1), 'E': (1, 1), 'F': (0, 2), 'G': (1, 2)}
+    __validate_node_attributes('edge_attribute.diag', xy=positions)
+
+
+def test_folded_edge_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'D': (0, 1),
+                 'E': (0, 2), 'F': (1, 1), 'Z': (0, 3)}
+    __validate_node_attributes('folded_edge.diag', xy=positions)
+
+
+def test_skipped_edge_right_diagram():
+    filename = 'skipped_edge_right.diag'
+    skipped = {('A', 'B'): False, ('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_rightup_diagram():
+    filename = 'skipped_edge_rightup.diag'
+    skipped = {('A', 'B'): False, ('D', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_rightdown_diagram():
+    filename = 'skipped_edge_rightdown.diag'
+    skipped = {('A', 'B'): False, ('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_up_diagram():
+    filename = 'skipped_edge_up.diag'
+    skipped = {('C', 'A'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_down_diagram():
+    filename = 'skipped_edge_down.diag'
+    skipped = {('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_leftdown_diagram():
+    filename = 'skipped_edge_leftdown.diag'
+    skipped = {('A', 'B'): False, ('C', 'G'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+ at stderr_wrapper
+def test_skipped_edge_flowchart_rightdown_diagram():
+    filename = 'skipped_edge_flowchart_rightdown.diag'
+    skipped = {('A', 'B'): False, ('A', 'D'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+ at stderr_wrapper
+def test_skipped_edge_flowchart_rightdown2_diagram():
+    filename = 'skipped_edge_flowchart_rightdown2.diag'
+    skipped = {('B', 'C'): False, ('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_portrait_right_diagram():
+    filename = 'skipped_edge_portrait_right.diag'
+    skipped = {('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_portrait_rightdown_diagram():
+    filename = 'skipped_edge_portrait_rightdown.diag'
+    skipped = {('A', 'B'): False, ('A', 'E'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_portrait_leftdown_diagram():
+    filename = 'skipped_edge_portrait_leftdown.diag'
+    skipped = {('A', 'B'): False, ('D', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+def test_skipped_edge_portrait_down_diagram():
+    filename = 'skipped_edge_portrait_down.diag'
+    skipped = {('A', 'B'): False, ('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+ at stderr_wrapper
+def test_skipped_edge_portrait_flowchart_rightdown_diagram():
+    filename = 'skipped_edge_portrait_flowchart_rightdown.diag'
+    skipped = {('A', 'B'): False, ('A', 'D'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
+
+
+ at stderr_wrapper
+def test_skipped_edge_portrait_flowchart_rightdown2_diagram():
+    filename = 'skipped_edge_portrait_flowchart_rightdown2.diag'
+    skipped = {('B', 'C'): False, ('A', 'C'): True}
+    __validate_node_attributes(filename, edge_skipped=skipped)
diff --git a/src/blockdiag/tests/test_builder_errors.py b/src/blockdiag/tests/test_builder_errors.py
new file mode 100644
index 0000000..4364943
--- /dev/null
+++ b/src/blockdiag/tests/test_builder_errors.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+
+from blockdiag.parser import *
+from nose.tools import raises
+from utils import __build_diagram
+
+
+ at raises(AttributeError)
+def test_unknown_diagram_default_shape_diagram():
+    diagram = __build_diagram('errors/unknown_diagram_default_shape.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_diagram_edge_layout_diagram():
+    diagram = __build_diagram('errors/unknown_diagram_edge_layout.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_diagram_orientation_diagram():
+    diagram = __build_diagram('errors/unknown_diagram_orientation.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_node_shape_diagram():
+    diagram = __build_diagram('errors/unknown_node_shape.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_node_attribute_diagram():
+    diagram = __build_diagram('errors/unknown_node_attribute.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_node_style_diagram():
+    diagram = __build_diagram('errors/unknown_node_style.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_node_class_diagram():
+    diagram = __build_diagram('errors/unknown_node_class.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_edge_dir_diagram():
+    diagram = __build_diagram('errors/unknown_edge_dir.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_edge_style_diagram():
+    diagram = __build_diagram('errors/unknown_edge_style.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_edge_hstyle_diagram():
+    diagram = __build_diagram('errors/unknown_edge_hstyle.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_edge_class_diagram():
+    diagram = __build_diagram('errors/unknown_edge_class.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_group_shape_diagram():
+    diagram = __build_diagram('errors/unknown_group_shape.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_group_class_diagram():
+    diagram = __build_diagram('errors/unknown_group_class.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_group_orientation_diagram():
+    diagram = __build_diagram('errors/unknown_group_orientation.diag')
+
+
+ at raises(RuntimeError)
+def test_belongs_to_two_groups_diagram():
+    diagram = __build_diagram('errors/belongs_to_two_groups.diag')
+
+
+ at raises(AttributeError)
+def test_unknown_plugin_diagram():
+    diagram = __build_diagram('errors/unknown_plugin.diag')
+
+
+ at raises(ParseException)
+def test_node_follows_group_diagram():
+    diagram = __build_diagram('errors/node_follows_group.diag')
+
+
+ at raises(ParseException)
+def test_group_follows_node_diagram():
+    diagram = __build_diagram('errors/group_follows_node.diag')
+
+
+ at raises(ParseException)
+def test_lexer_error_diagram():
+    diagram = __build_diagram('errors/lexer_error.diag')
diff --git a/src/blockdiag/tests/test_builder_group.py b/src/blockdiag/tests/test_builder_group.py
new file mode 100644
index 0000000..8817125
--- /dev/null
+++ b/src/blockdiag/tests/test_builder_group.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utils import __build_diagram, __validate_node_attributes
+
+
+def test_nested_groups_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'Z': (0, 2)}
+    __validate_node_attributes('nested_groups.diag', xy=positions)
+
+
+def test_nested_groups_and_edges_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('nested_groups_and_edges.diag', xy=positions)
+
+
+def test_empty_group_diagram():
+    positions = {'Z': (0, 0)}
+    __validate_node_attributes('empty_group.diag', xy=positions)
+
+
+def test_empty_nested_group_diagram():
+    positions = {'Z': (0, 0)}
+    __validate_node_attributes('empty_nested_group.diag', xy=positions)
+
+
+def test_empty_group_declaration_diagram():
+    positions = {'A': (0, 0), 'Z': (0, 1)}
+    __validate_node_attributes('empty_group_declaration.diag', xy=positions)
+
+
+def test_simple_group_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('simple_group.diag', xy=positions)
+
+
+def test_group_declare_as_node_attribute_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                  'D': (2, 1), 'E': (2, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_declare_as_node_attribute.diag',
+                               xy=positions)
+
+
+def test_group_attribute():
+    diagram = __build_diagram('group_attribute.diag')
+
+    for node in (x for x in diagram.nodes if not x.drawable):
+        node.color = 'red'
+        node.label = 'group label'
+        node.shape = 'line'
+
+
+def test_merge_groups_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (0, 1),
+                 'D': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('merge_groups.diag', xy=positions)
+
+
+def test_node_attribute_and_group_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    labels = {'A': 'foo', 'B': 'bar', 'C': 'baz', 'Z': 'Z'}
+    colors = {'A': (255, 0, 0), 'B': '#888888', 'C': (0, 0, 255),
+              'Z': (255, 255, 255)}
+    __validate_node_attributes('node_attribute_and_group.diag',
+                               xy=positions, label=labels, color=colors)
+
+
+def test_group_sibling_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 2), 'D': (2, 0),
+                 'E': (2, 1), 'F': (2, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_sibling.diag', xy=positions)
+
+
+def test_group_order_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('group_order.diag', xy=positions)
+
+
+def test_group_order2_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1),
+                 'D': (2, 1), 'E': (1, 2), 'F': (2, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_order2.diag', xy=positions)
+
+
+def test_group_order3_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (2, 1), 'E': (1, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_order3.diag', xy=positions)
+
+
+def test_group_children_height_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (2, 0), 'F': (2, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_children_height.diag', xy=positions)
+
+
+def test_group_children_order_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (2, 0), 'F': (2, 1), 'G': (2, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_children_order.diag', xy=positions)
+
+
+def test_group_children_order2_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (2, 1), 'F': (2, 0), 'G': (2, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_children_order2.diag', xy=positions)
+
+
+def test_group_children_order3_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (2, 0), 'F': (2, 1), 'G': (2, 2), 'Q': (0, 3),
+                 'Z': (0, 4)}
+    __validate_node_attributes('group_children_order3.diag', xy=positions)
+
+
+def test_group_children_order4_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (2, 0), 'Z': (0, 3)}
+    __validate_node_attributes('group_children_order4.diag', xy=positions)
+
+
+def test_node_in_group_follows_outer_node_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('node_in_group_follows_outer_node.diag',
+                               xy=positions)
+
+
+def test_group_id_and_node_id_are_not_conflicted_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (0, 1),
+                 'D': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('group_id_and_node_id_are_not_conflicted.diag',
+                               xy=positions)
+
+
+def test_outer_node_follows_node_in_group_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('outer_node_follows_node_in_group.diag',
+                               xy=positions)
+
+
+def test_large_group_and_node_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (1, 3), 'F': (2, 0), 'Z': (0, 4)}
+    __validate_node_attributes('large_group_and_node.diag', xy=positions)
+
+
+def test_large_group_and_node2_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'D': (3, 0),
+                 'E': (4, 0), 'F': (5, 0), 'Z': (0, 1)}
+    __validate_node_attributes('large_group_and_node2.diag', xy=positions)
+
+
+def test_large_group_and_two_nodes_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'D': (1, 2),
+                 'E': (1, 3), 'F': (2, 0), 'G': (2, 1), 'Z': (0, 4)}
+    __validate_node_attributes('large_group_and_two_nodes.diag', xy=positions)
+
+
+def test_group_height_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (2, 1), 'E': (1, 2), 'Z': (0, 3)}
+    __validate_node_attributes('group_height.diag', xy=positions)
+
+
+def test_multiple_groups_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'C': (0, 2), 'D': (0, 3),
+                 'E': (1, 0), 'F': (1, 1), 'G': (1, 2), 'H': (2, 0),
+                 'I': (2, 1), 'J': (3, 0), 'Z': (0, 4)}
+    __validate_node_attributes('multiple_groups.diag', xy=positions)
+
+
+def test_multiple_nested_groups_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('multiple_nested_groups.diag', xy=positions)
+
+
+def test_group_works_node_decorator_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (3, 0),
+                 'D': (2, 0), 'E': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('group_works_node_decorator.diag', xy=positions)
+
+
+def test_nested_groups_work_node_decorator_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'Z': (0, 2)}
+    __validate_node_attributes('nested_groups_work_node_decorator.diag',
+                               xy=positions)
+
+
+def test_reversed_multiple_groups_diagram():
+    positions = {'A': (3, 0), 'B': (3, 1), 'C': (3, 2), 'D': (3, 3),
+                 'E': (2, 0), 'F': (2, 1), 'G': (2, 2), 'H': (1, 0),
+                 'I': (1, 1), 'J': (0, 0), 'Z': (0, 4)}
+    __validate_node_attributes('reverse_multiple_groups.diag', xy=positions)
+
+
+def test_group_and_skipped_edge_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (3, 0), 'E': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('group_and_skipped_edge.diag', xy=positions)
+
+
+def test_group_orientation_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1),
+                 'D': (2, 1), 'Z': (0, 2)}
+    __validate_node_attributes('group_orientation.diag', xy=positions)
+
+
+def test_nested_group_orientation_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'C': (1, 0), 'Z': (0, 2)}
+    __validate_node_attributes('nested_group_orientation.diag', xy=positions)
diff --git a/src/blockdiag/tests/test_builder_node.py b/src/blockdiag/tests/test_builder_node.py
new file mode 100644
index 0000000..69482fb
--- /dev/null
+++ b/src/blockdiag/tests/test_builder_node.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from blockdiag.utils.collections import defaultdict
+from utils import __build_diagram, __validate_node_attributes
+
+
+def test_single_node_diagram():
+    diagram = __build_diagram('single_node.diag')
+
+    assert len(diagram.nodes) == 1
+    assert len(diagram.edges) == 0
+    assert diagram.nodes[0].label == 'A'
+    assert diagram.nodes[0].xy == (0, 0)
+
+
+def test_node_shape_diagram():
+    shapes = {'A': 'box', 'B': 'roundedbox', 'C': 'diamond',
+              'D': 'ellipse', 'E': 'note', 'F': 'cloud',
+              'G': 'mail', 'H': 'beginpoint', 'I': 'endpoint',
+              'J': 'minidiamond', 'K': 'flowchart.condition',
+              'L': 'flowchart.database', 'M': 'flowchart.input',
+              'N': 'flowchart.loopin', 'O': 'flowchart.loopout',
+              'P': 'actor', 'Q': 'flowchart.terminator', 'R': 'textbox',
+              'S': 'dots', 'T': 'none', 'U': 'square', 'V': 'circle',
+              'Z': 'box'}
+    __validate_node_attributes('node_shape.diag', shape=shapes)
+
+
+def test_node_shape_namespace_diagram():
+    shapes = {'A': 'flowchart.condition', 'B': 'condition', 'Z': 'box'}
+    __validate_node_attributes('node_shape_namespace.diag', shape=shapes)
+
+
+def test_node_has_multilined_label_diagram():
+    positions = {'A': (0, 0), 'Z': (0, 1)}
+    labels = {'A': "foo\nbar", 'Z': 'Z'}
+    __validate_node_attributes('node_has_multilined_label.diag',
+                               xy=positions, label=labels)
+
+
+def test_quoted_node_id_diagram():
+    positions = {'A': (0, 0), "'A'": (1, 0), 'B': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('quoted_node_id.diag', xy=positions)
+
+
+def test_node_id_includes_dot_diagram():
+    positions = {'A.B': (0, 0), 'C.D': (1, 0), 'Z': (0, 1)}
+    __validate_node_attributes('node_id_includes_dot.diag', xy=positions)
+
+
+def test_multiple_nodes_definition_diagram():
+    positions = {'A': (0, 0), 'B': (0, 1), 'Z': (0, 2)}
+    colors = {'A': (255, 0, 0), 'B': (255, 0, 0), 'Z': (255, 255, 255)}
+    __validate_node_attributes('multiple_nodes_definition.diag', xy=positions,
+                               color=colors)
+
+
+def test_multiple_node_relation_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (1, 1),
+                 'D': (2, 0), 'Z': (0, 2)}
+    __validate_node_attributes('multiple_node_relation.diag', xy=positions)
+
+
+def test_node_attribute():
+    labels = {'A': 'B', 'B': 'double quoted', 'C': 'single quoted',
+              'D': '\'"double" quoted\'', 'E': '"\'single\' quoted"',
+              'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I'}
+    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)}
+    textcolors = defaultdict(lambda: (0, 0, 0))
+    textcolors['F'] = (255, 0, 0)
+    numbered = defaultdict(lambda: None)
+    numbered['E'] = '1'
+    stacked = defaultdict(lambda: False)
+    stacked['G'] = True
+    fontsize = defaultdict(lambda: None)
+    fontsize['H'] = 16
+    linecolors = defaultdict(lambda: (0, 0, 0))
+    linecolors['I'] = (255, 0, 0)
+
+    __validate_node_attributes('node_attribute.diag', label=labels,
+                               color=colors, textcolor=textcolors,
+                               numbered=numbered, stacked=stacked,
+                               fontsize=fontsize, linecolor=linecolors)
+
+
+def test_node_height_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (2, 1), 'E': (1, 1), 'Z': (0, 2)}
+    __validate_node_attributes('node_height.diag', xy=positions)
+
+
+def test_branched_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0),
+                 'D': (1, 1), 'E': (2, 1), 'Z': (0, 2)}
+    __validate_node_attributes('branched.diag', xy=positions)
+
+
+def test_multiple_parent_node_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (0, 2),
+                 'D': (1, 2), 'E': (0, 1), 'Z': (0, 3)}
+    __validate_node_attributes('multiple_parent_node.diag', xy=positions)
+
+
+def test_twin_multiple_parent_node_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (0, 1),
+                 'D': (1, 1), 'E': (0, 2), 'Z': (0, 3)}
+    __validate_node_attributes('twin_multiple_parent_node.diag', xy=positions)
+
+
+def test_flowable_node_diagram():
+    positions = {'A': (0, 0), 'B': (1, 0), 'C': (2, 0), 'Z': (0, 1)}
+    __validate_node_attributes('flowable_node.diag', xy=positions)
+
+
+def test_plugin_autoclass_diagram():
+    positions = {'A_emphasis': (0, 0), 'B_emphasis': (1, 0), 'C': (1, 1)}
+    styles = {'A_emphasis': 'dashed', 'B_emphasis': 'dashed', 'C': None}
+    colors = {'A_emphasis': (255, 0, 0), 'B_emphasis': (255, 0, 0),
+              'C': (255, 255, 255)}
+
+    __validate_node_attributes('plugin_autoclass.diag', xy=positions,
+                                style=styles, color=colors)
+
+
+def test_plugin_attributes_diagram():
+    attr1 = {'A': "1", 'B': None}
+    attr2 = {'A': "2", 'B': None}
+    attr3 = {'A': "3", 'B': None}
+
+    __validate_node_attributes('plugin_attributes.diag', test_attr1=attr1,
+                               test_attr2=attr2, test_attr3=attr3)
diff --git a/src/blockdiag/tests/test_builder_separate.py b/src/blockdiag/tests/test_builder_separate.py
new file mode 100644
index 0000000..2165695
--- /dev/null
+++ b/src/blockdiag/tests/test_builder_separate.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+
+import tempfile
+from blockdiag.builder import *
+from blockdiag.elements import *
+from blockdiag.parser import parse_string
+
+
+def __build_diagram(filename):
+    import os
+    testdir = os.path.dirname(__file__)
+    pathname = "%s/diagrams/%s" % (testdir, filename)
+
+    str = open(pathname).read()
+    tree = parse_string(str)
+    return SeparateDiagramBuilder.build(tree)
+
+
+def test_separate1_diagram():
+    diagram = __build_diagram('separate1.diag')
+
+    assert_pos = {0: {'B': (0, 0), 'C': (1, 0), 'D': (4, 0),
+                      'E': (2, 0), 'F': (3, 0)},
+                  1: {'A': (0, 0), 'B': (1, 0), 'D': (3, 0)},
+                  2: {'A': (0, 0), 'Z': (0, 1)}}
+
+    for i, diagram in enumerate(diagram):
+        for node in diagram.traverse_nodes():
+            if isinstance(node, DiagramNode):
+                print node, assert_pos[i][node.id]
+                assert node.xy == assert_pos[i][node.id]
+
+
+def test_separate2_diagram():
+    diagram = __build_diagram('separate2.diag')
+
+    assert_pos = {0: {'A': (0, 0), 'C': (1, 0), 'D': (2, 0),
+                      'E': (0, 2), 'G': (3, 0), 'H': (3, 1)},
+                  1: {'A': (0, 0), 'B': (1, 0), 'E': (2, 0),
+                      'F': (4, 2), 'G': (4, 0), 'H': (4, 1)},
+                  2: {'A': (0, 0), 'F': (2, 2), 'G': (2, 0),
+                      'H': (2, 1), 'Z': (0, 3)}}
+
+    for i, diagram in enumerate(diagram):
+        for node in diagram.traverse_nodes():
+            if isinstance(node, DiagramNode):
+                print node, assert_pos[i][node.id]
+                assert node.xy == assert_pos[i][node.id]
diff --git a/src/blockdiag/tests/test_generate_diagram.py b/src/blockdiag/tests/test_generate_diagram.py
new file mode 100644
index 0000000..6824f56
--- /dev/null
+++ b/src/blockdiag/tests/test_generate_diagram.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import re
+import tempfile
+import blockdiag
+import blockdiag.command
+from utils import *
+from blockdiag.elements import *
+
+
+def get_fontpath():
+    filename = "VL-PGothic-Regular.ttf"
+    testdir = os.path.dirname(__file__)
+    return "%s/truetype/%s" % (testdir, filename)
+
+
+def extra_case(func):
+    pathname = get_fontpath()
+
+    if os.path.exists(pathname):
+        func.__test__ = True
+    else:
+        func.__test__ = False
+
+    return func
+
+
+ at argv_wrapper
+ at stderr_wrapper
+def __build_diagram(filename, format, *args):
+    testdir = os.path.dirname(__file__)
+    diagpath = "%s/diagrams/%s" % (testdir, filename)
+    fontpath = get_fontpath()
+
+    try:
+        tmpdir = tempfile.mkdtemp()
+        tmpfile = tempfile.mkstemp(dir=tmpdir)
+        os.close(tmpfile[0])
+
+        sys.argv = ['blockdiag.py', '-T', format, '-o', tmpfile[1], diagpath]
+        if args:
+            sys.argv += args
+        if os.path.exists(fontpath):
+            sys.argv += ['-f', fontpath]
+
+        blockdiag.command.main()
+
+        if re.search('ERROR', sys.stderr.getvalue()):
+            raise RuntimeError(sys.stderr.getvalue())
+    finally:
+        for file in os.listdir(tmpdir):
+            os.unlink(tmpdir + "/" + file)
+        os.rmdir(tmpdir)
+
+
+def diagram_files():
+    testdir = os.path.dirname(__file__)
+    pathname = "%s/diagrams/" % testdir
+
+    skipped = ['errors',
+               'white.gif']
+
+    return [d for d in os.listdir(pathname) if d not in skipped]
+
+
+def test_generator_svg():
+    for testcase in generator_core('svg'):
+        yield testcase
+
+
+ at extra_case
+def test_generator_png():
+    for testcase in generator_core('png'):
+        yield testcase
+
+
+ at extra_case
+def test_generator_pdf():
+    try:
+        import reportlab.pdfgen.canvas
+        for testcase in generator_core('pdf'):
+            yield testcase
+    except ImportError:
+        sys.stderr.write("Skip testing about pdf exporting.\n")
+        pass
+
+
+def generator_core(format):
+    for diagram in diagram_files():
+        yield __build_diagram, diagram, format
+
+        if re.search('separate', diagram):
+            yield __build_diagram, diagram, format, '--separate'
+
+        if format == 'png':
+            yield __build_diagram, diagram, format, '--antialias'
+
+
+ at extra_case
+ at argv_wrapper
+def not_exist_font_config_option_test():
+    fontpath = get_fontpath()
+    sys.argv = ['', '-f', '/font_is_not_exist', '-f', fontpath, 'input.diag']
+    options = blockdiag.command.BlockdiagOptions(blockdiag).parse()
+    blockdiag.command.detectfont(options)
diff --git a/src/blockdiag/tests/test_parser.py b/src/blockdiag/tests/test_parser.py
new file mode 100644
index 0000000..5175378
--- /dev/null
+++ b/src/blockdiag/tests/test_parser.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+
+from blockdiag.parser import *
+from nose.tools import raises
+
+
+def test_parser_basic():
+    # basic digram
+    str = """
+          diagram test {
+             A -> B -> C, D;
+          }
+          """
+
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+
+
+def test_parser_without_diagram_id():
+    str = """
+          diagram {
+             A -> B -> C, D;
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+
+    str = """
+          {
+             A -> B -> C, D;
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+
+
+def test_parser_empty_diagram():
+    str = """
+          diagram {
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+
+    str = """
+          {
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+
+
+def test_parser_diagram_includes_nodes():
+    str = """
+          diagram {
+            A;
+            B [label = "foobar"];
+            C [color = "red"];
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+    assert len(tree.stmts) == 3
+    assert isinstance(tree.stmts[0], Statements)
+    assert isinstance(tree.stmts[0].stmts[0], Node)
+    assert isinstance(tree.stmts[1], Statements)
+    assert isinstance(tree.stmts[1].stmts[0], Node)
+    assert isinstance(tree.stmts[2], Statements)
+    assert isinstance(tree.stmts[2].stmts[0], Node)
+
+
+def test_parser_diagram_includes_edges():
+    str = """
+          diagram {
+            A -> B -> C;
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+    print tree.stmts
+    assert len(tree.stmts) == 1
+    assert isinstance(tree.stmts[0], Edge)
+
+    str = """
+          diagram {
+            A -> B -> C [style = dotted];
+            D -> E, F;
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+    print tree.stmts
+    assert len(tree.stmts) == 2
+    assert isinstance(tree.stmts[0], Edge)
+    assert isinstance(tree.stmts[1], Edge)
+
+
+def test_parser_diagram_includes_groups():
+    str = """
+          diagram {
+            group {
+              A; B;
+            }
+            group {
+              C -> D;
+            }
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+    assert len(tree.stmts) == 2
+
+    assert isinstance(tree.stmts[0], SubGraph)
+    assert len(tree.stmts[0].stmts) == 2
+    assert isinstance(tree.stmts[0].stmts[0], Statements)
+    assert isinstance(tree.stmts[0].stmts[0].stmts[0], Node)
+    assert isinstance(tree.stmts[0].stmts[1], Statements)
+    assert isinstance(tree.stmts[0].stmts[1].stmts[0], Node)
+
+    assert isinstance(tree.stmts[1], SubGraph)
+    assert len(tree.stmts[1].stmts) == 1
+    assert isinstance(tree.stmts[1].stmts[0], Edge)
+
+
+def test_parser_diagram_includes_diagram_attributes():
+    str = """
+          diagram {
+            fontsize = 12;
+            node_width = 80;
+          }
+          """
+    tree = parse_string(str)
+    assert isinstance(tree, Graph)
+    assert len(tree.stmts) == 2
+
+
+ at raises(ParseException)
+def test_parser_parenthesis_ness():
+    str = ""
+    tree = parse_string(str)
diff --git a/src/blockdiag/tests/test_pep8.py b/src/blockdiag/tests/test_pep8.py
new file mode 100644
index 0000000..df254b0
--- /dev/null
+++ b/src/blockdiag/tests/test_pep8.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+import os
+import pep8
+
+CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
+BASE_DIR = os.path.dirname(CURRENT_DIR)
+
+
+def test_pep8():
+    arglist = [
+        '--statistics',
+        '--filename=*.py',
+        '--show-source',
+        '--repeat',
+        '--exclude=SVGdraw.py',
+        #'--show-pep8',
+        #'-qq',
+        #'-v',
+        BASE_DIR,
+    ]
+
+    options, args = pep8.process_options(arglist)
+    runner = pep8.input_file
+
+    for path in args:
+        if os.path.isdir(path):
+            pep8.input_dir(path, runner=runner)
+        elif not pep8.excluded(path):
+            options.counters['files'] += 1
+            runner(path)
+
+    pep8.print_statistics()
+    errors = pep8.get_count('E')
+    warnings = pep8.get_count('W')
+    message = 'pep8: %d errors / %d warnings' % (errors, warnings)
+    print message
+    assert errors + warnings == 0, message
diff --git a/src/blockdiag/tests/test_rst_directives.py b/src/blockdiag/tests/test_rst_directives.py
new file mode 100644
index 0000000..73b9783
--- /dev/null
+++ b/src/blockdiag/tests/test_rst_directives.py
@@ -0,0 +1,365 @@
+# -*- coding: utf-8 -*-
+
+import re
+import os
+import sys
+import tempfile
+import unittest2
+from utils import stderr_wrapper, assertRaises
+from docutils import nodes
+from docutils.core import publish_doctree
+from docutils.parsers.rst import directives as docutils
+from blockdiag.utils.rst import directives
+
+
+def setup_directive_base(func):
+    def _(self):
+        klass = directives.BlockdiagDirectiveBase
+        docutils.register_directive('blockdiag', klass)
+        func(self)
+
+    _.__name__ = func.__name__
+    return _
+
+
+def use_tmpdir(func):
+    def _(self):
+        try:
+            tmpdir = tempfile.mkdtemp()
+            func(self, tmpdir)
+        finally:
+            for file in os.listdir(tmpdir):
+                os.unlink(tmpdir + "/" + file)
+            os.rmdir(tmpdir)
+
+    _.__name__ = func.__name__
+    return _
+
+
+class TestRstDirectives(unittest2.TestCase):
+    def tearDown(self):
+        if 'blockdiag' in docutils._directives:
+            del docutils._directives['blockdiag']
+
+    def test_rst_directives_setup(self):
+        directives.setup()
+
+        self.assertIn('blockdiag', docutils._directives)
+        self.assertEqual(directives.BlockdiagDirective,
+                         docutils._directives['blockdiag'])
+        self.assertEqual('PNG', directives.format)
+        self.assertEqual(False, directives.antialias)
+        self.assertEqual(None, directives.fontpath)
+
+    def test_rst_directives_setup_with_args(self):
+        directives.setup(format='SVG', antialias=True, fontpath='/dev/null')
+
+        self.assertIn('blockdiag', docutils._directives)
+        self.assertEqual(directives.BlockdiagDirective,
+                         docutils._directives['blockdiag'])
+        self.assertEqual('SVG', directives.format)
+        self.assertEqual(True, directives.antialias)
+        self.assertEqual('/dev/null', directives.fontpath)
+
+    @stderr_wrapper
+    @setup_directive_base
+    def test_rst_directives_base_noargs(self):
+        text = ".. blockdiag::"
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    @setup_directive_base
+    def test_rst_directives_base_with_block(self):
+        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'])
+
+    @stderr_wrapper
+    @setup_directive_base
+    def test_rst_directives_base_with_emptyblock(self):
+        text = ".. blockdiag::\n\n   \n"
+        doctree = publish_doctree(text)
+        self.assertEqual(1, len(doctree))
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    @setup_directive_base
+    def test_rst_directives_base_with_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(directives.blockdiag, type(doctree[0]))
+        self.assertEqual(open(filename).read(), doctree[0]['code'])
+        self.assertEqual(None, doctree[0]['alt'])
+        self.assertEqual({}, doctree[0]['options'])
+
+    @stderr_wrapper
+    @setup_directive_base
+    def test_rst_directives_base_with_filename_not_exists(self):
+        text = ".. blockdiag:: unknown.diag"
+        doctree = publish_doctree(text)
+        self.assertEqual(nodes.system_message, type(doctree[0]))
+
+    @stderr_wrapper
+    @setup_directive_base
+    def test_rst_directives_base_with_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]))
+
+    @setup_directive_base
+    def test_rst_directives_base_with_options(self):
+        text = ".. blockdiag::\n   :alt: hello world\n   :desctable:\n" + \
+               "   :maxwidth: 100\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('hello world', doctree[0]['alt'])
+        self.assertEqual(None, doctree[0]['options']['desctable'])
+        self.assertEqual(100, doctree[0]['options']['maxwidth'])
+
+    @use_tmpdir
+    def test_rst_directives_with_block(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        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(path))
+        self.assertFalse('target' in doctree[0])
+
+    @use_tmpdir
+    def test_rst_directives_with_block_alt(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        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(path))
+        self.assertFalse('target' in doctree[0])
+
+    @use_tmpdir
+    @assertRaises(RuntimeError)
+    def test_rst_directives_with_block_fontpath1(self, path):
+        directives.setup(format='SVG', fontpath=['dummy.ttf'],
+                         outputdir=path)
+        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+        doctree = publish_doctree(text)
+
+    @use_tmpdir
+    @assertRaises(RuntimeError)
+    def test_rst_directives_with_block_fontpath2(self, path):
+        directives.setup(format='SVG', fontpath='dummy.ttf',
+                         outputdir=path)
+        text = ".. blockdiag::\n   :alt: hello world\n\n   { A -> B }"
+        doctree = publish_doctree(text)
+
+    @use_tmpdir
+    def test_rst_directives_with_block_maxwidth(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        text = ".. blockdiag::\n   :maxwidth: 100\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(path))
+        self.assertFalse(0, doctree[0]['target'].index(path))
+
+    @use_tmpdir
+    def test_rst_directives_with_block_desctable(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        text = ".. blockdiag::\n   :desctable:\n\n   { A -> B }"
+        doctree = publish_doctree(text)
+        self.assertEqual(2, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(nodes.table, type(doctree[1]))
+
+        self.assertEqual(1, len(doctree[1]))
+        self.assertEqual(nodes.tgroup, type(doctree[1][0]))
+
+        # tgroup
+        self.assertEqual(4, len(doctree[1][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][1]))
+        self.assertEqual(nodes.thead, type(doctree[1][0][2]))
+        self.assertEqual(nodes.tbody, type(doctree[1][0][3]))
+
+        # colspec
+        self.assertEqual(0, len(doctree[1][0][0]))
+        self.assertEqual(50, doctree[1][0][0]['colwidth'])
+
+        self.assertEqual(0, len(doctree[1][0][1]))
+        self.assertEqual(50, doctree[1][0][1]['colwidth'])
+
+        # thead
+        thead = doctree[1][0][2]
+        self.assertEqual(1, len(thead))
+        self.assertEqual(2, len(thead[0]))
+
+        self.assertEqual(1, len(thead[0][0]))
+        self.assertEqual(1, len(thead[0][0][0]))
+        self.assertEqual('Name', thead[0][0][0][0])
+
+        self.assertEqual(1, len(thead[0][1]))
+        self.assertEqual(1, len(thead[0][1][0]))
+        self.assertEqual('Description', thead[0][1][0][0])
+
+        # tbody
+        tbody = doctree[1][0][3]
+        self.assertEqual(2, len(tbody))
+
+        self.assertEqual(2, len(tbody[0]))
+        self.assertEqual(1, len(tbody[0][0]))
+        self.assertEqual(1, len(tbody[0][0][0]))
+        self.assertEqual('A', tbody[0][0][0][0])
+        self.assertEqual(0, len(tbody[0][1]))
+
+        self.assertEqual(2, len(tbody[1]))
+        self.assertEqual(1, len(tbody[1][0]))
+        self.assertEqual(1, len(tbody[1][0][0]))
+        self.assertEqual('B', tbody[1][0][0][0])
+        self.assertEqual(0, len(tbody[1][1]))
+
+    @use_tmpdir
+    def test_rst_directives_with_block_desctable_with_description(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        text = ".. blockdiag::\n   :desctable:\n\n" + \
+               "   { A [description = foo]; B [description = bar]; }"
+        doctree = publish_doctree(text)
+        self.assertEqual(2, len(doctree))
+        self.assertEqual(nodes.image, type(doctree[0]))
+        self.assertEqual(nodes.table, type(doctree[1]))
+
+        # tgroup
+        self.assertEqual(4, len(doctree[1][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][1]))
+        self.assertEqual(nodes.thead, type(doctree[1][0][2]))
+        self.assertEqual(nodes.tbody, type(doctree[1][0][3]))
+
+        # colspec
+        self.assertEqual(50, doctree[1][0][0]['colwidth'])
+        self.assertEqual(50, doctree[1][0][1]['colwidth'])
+
+        # thead
+        thead = doctree[1][0][2]
+        self.assertEqual(2, len(thead[0]))
+        self.assertEqual('Name', thead[0][0][0][0])
+        self.assertEqual('Description', thead[0][1][0][0])
+
+        # tbody
+        tbody = doctree[1][0][3]
+        self.assertEqual(2, len(tbody))
+        self.assertEqual('A', tbody[0][0][0][0])
+        self.assertEqual('foo', tbody[0][1][0][0])
+        self.assertEqual('B', tbody[1][0][0][0])
+        self.assertEqual('bar', tbody[1][1][0][0])
+
+    @use_tmpdir
+    def test_rst_directives_with_block_desctable_with_rest_markups(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        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]))
+        self.assertEqual(nodes.table, type(doctree[1]))
+
+        # tgroup
+        self.assertEqual(4, len(doctree[1][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][1]))
+        self.assertEqual(nodes.thead, type(doctree[1][0][2]))
+        self.assertEqual(nodes.tbody, type(doctree[1][0][3]))
+
+        # colspec
+        self.assertEqual(50, doctree[1][0][0]['colwidth'])
+        self.assertEqual(50, doctree[1][0][1]['colwidth'])
+
+        # thead
+        thead = doctree[1][0][2]
+        self.assertEqual(2, len(thead[0]))
+        self.assertEqual('Name', thead[0][0][0][0])
+        self.assertEqual('Description', thead[0][1][0][0])
+
+        # tbody
+        tbody = doctree[1][0][3]
+        self.assertEqual(2, len(tbody))
+        self.assertEqual('A', tbody[0][0][0][0])
+        self.assertEqual(4, len(tbody[0][1][0]))
+        self.assertEqual(nodes.Text, type(tbody[0][1][0][0]))
+        self.assertEqual('foo ', str(tbody[0][1][0][0]))
+        self.assertEqual(nodes.emphasis, type(tbody[0][1][0][1]))
+        self.assertEqual(nodes.Text, type(tbody[0][1][0][1][0]))
+        self.assertEqual('bar', tbody[0][1][0][1][0])
+        self.assertEqual(nodes.Text, type(tbody[0][1][0][2]))
+        self.assertEqual(' ', str(tbody[0][1][0][2]))
+        self.assertEqual(nodes.strong, type(tbody[0][1][0][3]))
+        self.assertEqual(nodes.Text, type(tbody[0][1][0][3][0]))
+        self.assertEqual('baz', str(tbody[0][1][0][3][0]))
+
+        self.assertEqual('B', tbody[1][0][0][0])
+        self.assertEqual(4, len(tbody[1][1][0]))
+        print tbody[1][1][0]
+        self.assertEqual(nodes.strong, type(tbody[1][1][0][0]))
+        self.assertEqual(nodes.Text, type(tbody[1][1][0][0][0]))
+        self.assertEqual('foo', str(tbody[1][1][0][0][0]))
+        self.assertEqual(nodes.Text, type(tbody[1][1][0][1]))
+        self.assertEqual(' ', str(tbody[1][1][0][1]))
+        self.assertEqual(nodes.emphasis, type(tbody[1][1][0][2]))
+        self.assertEqual(nodes.Text, type(tbody[1][1][0][2][0]))
+        self.assertEqual('bar', str(tbody[1][1][0][2][0]))
+        self.assertEqual(nodes.Text, type(tbody[1][1][0][3]))
+        self.assertEqual(' baz', str(tbody[1][1][0][3]))
+
+    @use_tmpdir
+    def test_rst_directives_with_block_desctable_with_numbered(self, path):
+        directives.setup(format='SVG', outputdir=path)
+        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]))
+        self.assertEqual(nodes.table, type(doctree[1]))
+
+        # tgroup
+        self.assertEqual(5, len(doctree[1][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][0]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][1]))
+        self.assertEqual(nodes.colspec, type(doctree[1][0][2]))
+        self.assertEqual(nodes.thead, type(doctree[1][0][3]))
+        self.assertEqual(nodes.tbody, type(doctree[1][0][4]))
+
+        # colspec
+        self.assertEqual(25, doctree[1][0][0]['colwidth'])
+        self.assertEqual(50, doctree[1][0][1]['colwidth'])
+        self.assertEqual(50, doctree[1][0][2]['colwidth'])
+
+        # thead
+        thead = doctree[1][0][3]
+        self.assertEqual(3, len(thead[0]))
+        self.assertEqual('No', thead[0][0][0][0])
+        self.assertEqual('Name', thead[0][1][0][0])
+        self.assertEqual('Description', thead[0][2][0][0])
+
+        # tbody
+        tbody = doctree[1][0][4]
+        self.assertEqual(2, len(tbody))
+        self.assertEqual('1', tbody[0][0][0][0])
+        self.assertEqual('B', tbody[0][1][0][0])
+        self.assertEqual(0, len(tbody[0][2]))
+        self.assertEqual('2', tbody[1][0][0][0])
+        self.assertEqual('A', tbody[1][1][0][0])
+        self.assertEqual(0, len(tbody[1][2]))
diff --git a/src/blockdiag/tests/test_utils_fontmap.py b/src/blockdiag/tests/test_utils_fontmap.py
new file mode 100644
index 0000000..9581c2f
--- /dev/null
+++ b/src/blockdiag/tests/test_utils_fontmap.py
@@ -0,0 +1,340 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import tempfile
+import unittest2
+from utils import stderr_wrapper, assertRaises
+from cStringIO import StringIO
+from blockdiag.utils.collections import namedtuple
+from blockdiag.utils.fontmap import FontInfo, FontMap
+
+
+FontElement = namedtuple('FontElement', 'fontfamily fontsize')
+
+
+class TestUtilsFontmap(unittest2.TestCase):
+    def setUp(self):
+        fontpath1 = __file__
+        fontpath2 = os.path.join(os.path.dirname(__file__), 'utils.py')
+        self.fontpath = [fontpath1, fontpath2]
+
+    def test_fontinfo_new(self):
+        FontInfo("serif", None, 11)
+        FontInfo("sansserif", None, 11)
+        FontInfo("monospace", None, 11)
+        FontInfo("cursive", None, 11)
+        FontInfo("fantasy", None, 11)
+
+        FontInfo("serif-bold", None, 11)
+        FontInfo("sansserif-italic", None, 11)
+        FontInfo("monospace-oblique", None, 11)
+        FontInfo("my-cursive", None, 11)
+        FontInfo("-fantasy", None, 11)
+
+    @assertRaises(AttributeError)
+    def test_fontinfo_invalid_familyname1(self):
+        FontInfo("unknown", None, 11)
+
+    @assertRaises(AttributeError)
+    def test_fontinfo_invalid_familyname2(self):
+        FontInfo("sansserif-", None, 11)
+
+    @assertRaises(AttributeError)
+    def test_fontinfo_invalid_familyname3(self):
+        FontInfo("monospace-unkown", None, 11)
+
+    @assertRaises(AttributeError)
+    def test_fontinfo_invalid_familyname4(self):
+        FontInfo("cursive-bold-bold", None, 11)
+
+    @assertRaises(AttributeError)
+    def test_fontinfo_invalid_familyname4(self):
+        FontInfo("SERIF", None, 11)
+
+    @assertRaises(TypeError)
+    def test_fontinfo_invalid_fontsize1(self):
+        FontInfo("serif", None, None)
+
+    @assertRaises(ValueError)
+    def test_fontinfo_invalid_fontsize2(self):
+        FontInfo("serif", None, '')
+
+    def test_fontinfo_parse(self):
+        font = FontInfo("serif", None, 11)
+        self.assertEqual('', font.name)
+        self.assertEqual('serif', font.generic_family)
+        self.assertEqual('normal', font.weight)
+        self.assertEqual('normal', font.style)
+
+        font = FontInfo("sansserif-bold", None, 11)
+        self.assertEqual('', font.name)
+        self.assertEqual('sansserif', font.generic_family)
+        self.assertEqual('bold', font.weight)
+        self.assertEqual('normal', font.style)
+
+        font = FontInfo("monospace-italic", None, 11)
+        self.assertEqual('', font.name)
+        self.assertEqual('monospace', font.generic_family)
+        self.assertEqual('normal', font.weight)
+        self.assertEqual('italic', font.style)
+
+        font = FontInfo("my-cursive-oblique", None, 11)
+        self.assertEqual('my', font.name)
+        self.assertEqual('cursive', font.generic_family)
+        self.assertEqual('normal', font.weight)
+        self.assertEqual('oblique', font.style)
+
+        font = FontInfo("my-fantasy-bold", None, 11)
+        self.assertEqual('my', font.name)
+        self.assertEqual('fantasy', font.generic_family)
+        self.assertEqual('bold', font.weight)
+        self.assertEqual('normal', font.style)
+
+        font = FontInfo("serif-serif", None, 11)
+        self.assertEqual('serif', font.name)
+        self.assertEqual('serif', font.generic_family)
+        self.assertEqual('normal', font.weight)
+        self.assertEqual('normal', font.style)
+
+    def test_fontinfo_familyname(self):
+        font = FontInfo("serif", None, 11)
+        self.assertEqual('serif-normal', font.familyname)
+
+        font = FontInfo("sansserif-bold", None, 11)
+        self.assertEqual('sansserif-bold', font.familyname)
+
+        font = FontInfo("monospace-italic", None, 11)
+        self.assertEqual('monospace-italic', font.familyname)
+
+        font = FontInfo("my-cursive-oblique", None, 11)
+        self.assertEqual('my-cursive-oblique', font.familyname)
+
+        font = FontInfo("my-fantasy-bold", None, 11)
+        self.assertEqual('my-fantasy-bold', font.familyname)
+
+        font = FontInfo("serif-serif", None, 11)
+        self.assertEqual('serif-serif-normal', font.familyname)
+
+        font = FontInfo("-serif", None, 11)
+        self.assertEqual('serif-normal', font.familyname)
+
+    @stderr_wrapper
+    def test_fontmap_empty_config(self):
+        config = StringIO("")
+        fmap = FontMap(config)
+
+        font1 = fmap.find()
+        self.assertTrue(font1)
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(None, font1.path)
+        self.assertEqual(11, font1.size)
+
+        element = FontElement('sansserif', 11)
+        font2 = fmap.find(element)
+        self.assertEqual(font1.familyname, font2.familyname)
+        self.assertEqual(font1.path, font2.path)
+        self.assertEqual(font1.size, font2.size)
+
+        element = FontElement('sansserif-normal', 11)
+        font3 = fmap.find(element)
+        self.assertEqual(font1.familyname, font3.familyname)
+        self.assertEqual(font1.path, font3.path)
+        self.assertEqual(font1.size, font3.size)
+
+        # non-registered familyname
+        element = FontElement('my-sansserif-normal', 11)
+        font4 = fmap.find(element)
+        self.assertEqual(font1.familyname, font4.familyname)
+        self.assertEqual(font1.path, font4.path)
+        self.assertEqual(font1.size, font4.size)
+
+    @stderr_wrapper
+    def test_fontmap_none_config(self):
+        fmap = FontMap()
+
+        font1 = fmap.find()
+        self.assertTrue(font1)
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(None, font1.path)
+        self.assertEqual(11, font1.size)
+
+    def test_fontmap_normal_config(self):
+        _config = "[fontmap]\nsansserif: %s\nsansserif-bold: %s\n" % \
+                  (self.fontpath[0], self.fontpath[1])
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        font1 = fmap.find()
+        self.assertTrue(font1)
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(self.fontpath[0], font1.path)
+        self.assertEqual(11, font1.size)
+
+        element = FontElement('sansserif', 11)
+        font2 = fmap.find(element)
+        self.assertEqual(font1.familyname, font2.familyname)
+        self.assertEqual(font1.path, font2.path)
+        self.assertEqual(font1.size, font2.size)
+
+        element = FontElement('sansserif-normal', 11)
+        font3 = fmap.find(element)
+        self.assertEqual(font1.familyname, font3.familyname)
+        self.assertEqual(font1.path, font3.path)
+        self.assertEqual(font1.size, font3.size)
+
+        element = FontElement('sansserif-bold', 11)
+        font4 = fmap.find(element)
+        self.assertEqual('sansserif-bold', font4.familyname)
+        self.assertEqual(self.fontpath[1], font4.path)
+        self.assertEqual(font1.size, font4.size)
+
+        element = FontElement(None, None)
+        font5 = fmap.find(element)
+        self.assertEqual(font1.familyname, font5.familyname)
+        self.assertEqual(font1.path, font5.path)
+        self.assertEqual(font1.size, font5.size)
+
+        element = object()
+        font6 = fmap.find(element)
+        self.assertEqual(font1.familyname, font6.familyname)
+        self.assertEqual(font1.path, font6.path)
+        self.assertEqual(font1.size, font6.size)
+
+    def test_fontmap_duplicated_fontentry1(self):
+        _config = "[fontmap]\nsansserif: %s\nsansserif: %s\n" % \
+                  (self.fontpath[0], self.fontpath[1])
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        font1 = fmap.find()
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(self.fontpath[1], font1.path)
+        self.assertEqual(11, font1.size)
+
+    def test_fontmap_duplicated_fontentry1(self):
+        # this testcase is only for python2.6 or later
+        if sys.version_info > (2, 6):
+            _config = "[fontmap]\nsansserif: %s\nsansserif-normal: %s\n" % \
+                      (self.fontpath[0], self.fontpath[1])
+            config = StringIO(_config)
+            fmap = FontMap(config)
+
+            font1 = fmap.find()
+            self.assertEqual('sansserif', font1.generic_family)
+            self.assertEqual(self.fontpath[1], font1.path)
+            self.assertEqual(11, font1.size)
+
+    @stderr_wrapper
+    def test_fontmap_with_nodefault_fontentry(self):
+        _config = "[fontmap]\nserif: %s\n" % self.fontpath[0]
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        font1 = fmap.find()
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(None, font1.path)
+        self.assertEqual(11, font1.size)
+
+        element = FontElement('serif', 11)
+        font2 = fmap.find(element)
+        self.assertEqual('serif', font2.generic_family)
+        self.assertEqual(self.fontpath[0], font2.path)
+        self.assertEqual(font1.size, font2.size)
+
+        element = FontElement('fantasy', 20)
+        font3 = fmap.find(element)
+        self.assertEqual('sansserif', font3.generic_family)
+        self.assertEqual(None, font3.path)
+        self.assertEqual(20, font3.size)
+
+    @stderr_wrapper
+    def test_fontmap_with_nonexistence_fontpath(self):
+        _config = "[fontmap]\nserif: unknown_file\n"
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        font1 = fmap.find()
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(None, font1.path)
+        self.assertEqual(11, font1.size)
+
+    def test_fontmap_switch_defaultfamily(self):
+        _config = "[fontmap]\nserif-bold: %s\n" % self.fontpath[0]
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        font1 = fmap.find()
+        self.assertEqual('sansserif-normal', font1.familyname)
+        self.assertEqual(None, font1.path)
+        self.assertEqual(11, font1.size)
+
+        fmap.set_default_fontfamily('serif-bold')
+        font2 = fmap.find()
+        self.assertEqual('serif-bold', font2.familyname)
+        self.assertEqual(self.fontpath[0], font2.path)
+        self.assertEqual(11, font2.size)
+
+        fmap.set_default_fontfamily('fantasy-italic')
+        font3 = fmap.find()
+        self.assertEqual('fantasy-italic', font3.familyname)
+        self.assertEqual(None, font3.path)
+        self.assertEqual(11, font3.size)
+
+        fmap.fontsize = 20
+        font4 = fmap.find()
+        self.assertEqual('fantasy-italic', font4.familyname)
+        self.assertEqual(None, font4.path)
+        self.assertEqual(20, font4.size)
+
+    def test_fontmap_using_fontalias(self):
+        _config = ("[fontmap]\nserif-bold: %s\n" + \
+                   "[fontalias]\ntest = serif-bold\n") % self.fontpath[0]
+        config = StringIO(_config)
+        fmap = FontMap(config)
+
+        element = FontElement('test', 20)
+        font1 = fmap.find(element)
+        self.assertEqual('serif-bold', font1.familyname)
+        self.assertEqual(self.fontpath[0], font1.path)
+        self.assertEqual(20, font1.size)
+
+    def test_fontmap_by_file(self):
+        tmp = tempfile.mkstemp()
+
+        _config = "[fontmap]\nsansserif: %s\nsansserif-bold: %s\n" % \
+                  (self.fontpath[0], self.fontpath[1])
+
+        fp = os.fdopen(tmp[0], 'wt')
+        fp.write(_config)
+        fp.close()
+        fmap = FontMap(tmp[1])
+
+        font1 = fmap.find()
+        self.assertTrue(font1)
+        self.assertEqual('sansserif', font1.generic_family)
+        self.assertEqual(self.fontpath[0], font1.path)
+        self.assertEqual(11, font1.size)
+
+        os.unlink(tmp[1])
+
+    def test_fontmap_including_bom_by_file(self):
+        tmp = tempfile.mkstemp()
+
+        _config = ("\xEF\xBB\xBF[fontmap]\nsansserif: %s\n"
+                   "sansserif-bold: %s\n") % \
+                  (self.fontpath[0], self.fontpath[1])
+
+        try:
+            fp = os.fdopen(tmp[0], 'wt')
+            fp.write(_config)
+            fp.close()
+            fmap = FontMap(tmp[1])
+
+            font1 = fmap.find()
+            self.assertTrue(font1)
+            self.assertEqual('sansserif', font1.generic_family)
+            self.assertEqual(self.fontpath[0], font1.path)
+            self.assertEqual(11, font1.size)
+        finally:
+            os.unlink(tmp[1])
diff --git a/src/blockdiag/tests/utils.py b/src/blockdiag/tests/utils.py
new file mode 100644
index 0000000..c3009ef
--- /dev/null
+++ b/src/blockdiag/tests/utils.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+import re
+from StringIO import StringIO
+from nose.tools import eq_
+from blockdiag.builder import *
+from blockdiag.parser import parse_string
+
+
+def argv_wrapper(func, argv=[]):
+    def wrap(*args, **kwargs):
+        try:
+            argv = sys.argv
+            sys.argv = []
+            func(*args, **kwargs)
+        finally:
+            sys.argv = argv
+
+    wrap.__name__ = func.__name__
+    return wrap
+
+
+def stderr_wrapper(func):
+    def wrap(*args, **kwargs):
+        try:
+            stderr = sys.stderr
+            sys.stderr = StringIO()
+
+            print args, kwargs
+            func(*args, **kwargs)
+        finally:
+            if sys.stderr.getvalue():
+                print "---[ stderr ] ---"
+                print sys.stderr.getvalue()
+
+            sys.stderr = stderr
+
+    wrap.__name__ = func.__name__
+    return wrap
+
+
+def assertRaises(exc):
+    def decorator(func):
+        def fn(self, *args, **kwargs):
+            try:
+                func(self, *args, **kwargs)
+            except exc:
+                pass
+            else:
+                msg = '%s does not raise exceptions: %s' % \
+                      (func.__name__, str(exc))
+                self.fail(msg)
+
+        fn.__name__ = func.__name__
+        return fn
+
+    return decorator
+
+
+def __build_diagram(filename):
+    import os
+    testdir = os.path.dirname(__file__)
+    pathname = "%s/diagrams/%s" % (testdir, filename)
+
+    str = open(pathname).read()
+    tree = parse_string(str)
+    return ScreenNodeBuilder.build(tree)
+
+
+def __validate_node_attributes(filename, **kwargs):
+    diagram = __build_diagram(filename)
+
+    for name, values in kwargs.items():
+        if re.match('edge_', name):
+            print "[%s]" % name
+            name = re.sub('edge_', '', name)
+            for (id1, id2), value in values.items():
+                found = False
+                for edge in diagram.edges:
+                    if edge.node1.id == id1 and edge.node2.id == id2:
+                        print edge
+                        eq_(value, getattr(edge, name))
+                        found = True
+
+                if not found:
+                    raise RuntimeError('edge (%s -> %s) is not found' % \
+                                       (id1, id2))
+        else:
+            print "[node.%s]" % name
+            for node in (n for n in diagram.nodes  if n.drawable):
+                print node
+                value = getattr(node, name)
+                eq_(values[node.id], value)
diff --git a/src/blockdiag/utils/PDFTextFolder.py b/src/blockdiag/utils/PDFTextFolder.py
new file mode 100644
index 0000000..1f9b49a
--- /dev/null
+++ b/src/blockdiag/utils/PDFTextFolder.py
@@ -0,0 +1,29 @@
+# -*- 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.
+
+import math
+from TextFolder import TextFolder
+
+
+class PDFTextFolder(TextFolder):
+    def __init__(self, box, string, font, **kwargs):
+        self.canvas = kwargs.get('canvas')
+        self.font = font
+
+        TextFolder.__init__(self, box, string, font, **kwargs)
+
+    def textsize(self, string):
+        width = self.canvas.stringWidth(string, self.font.path, self.font.size)
+        return (int(math.ceil(width)), self.font.size)
diff --git a/src/blockdiag/utils/PILTextFolder.py b/src/blockdiag/utils/PILTextFolder.py
new file mode 100644
index 0000000..7400adb
--- /dev/null
+++ b/src/blockdiag/utils/PILTextFolder.py
@@ -0,0 +1,45 @@
+# -*- 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:
+    from PIL import Image
+    from PIL import ImageDraw
+    from PIL import ImageFont
+except ImportError:
+    import Image
+    import ImageDraw
+    import ImageFont
+from TextFolder import TextFolder
+from fontmap import parse_fontpath
+
+
+class PILTextFolder(TextFolder):
+    def __init__(self, box, string, font, **kwargs):
+        if font.path:
+            path, index = parse_fontpath(font.path)
+            if index:
+                self.ttfont = ImageFont.truetype(path, font.size, index=index)
+            else:
+                self.ttfont = ImageFont.truetype(path, font.size)
+        else:
+            self.ttfont = None
+
+        image = Image.new('1', (1, 1))
+        self.draw = ImageDraw.Draw(image)
+
+        super(PILTextFolder, self).__init__(box, string, font, **kwargs)
+
+    def textsize(self, string):
+        return self.draw.textsize(string, font=self.ttfont)
diff --git a/src/blockdiag/utils/TextFolder.py b/src/blockdiag/utils/TextFolder.py
new file mode 100644
index 0000000..7827be6
--- /dev/null
+++ b/src/blockdiag/utils/TextFolder.py
@@ -0,0 +1,303 @@
+# -*- 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.
+
+import re
+import math
+import unicodedata
+from blockdiag.utils import Box, XY
+from blockdiag.utils.fontmap import FontInfo
+
+
+def is_zenkaku(char):
+    u"""
+    Detect given character is Japanese ZENKAKU character
+
+    >>> is_zenkaku(u"A")
+    False
+    >>> is_zenkaku(u"あ")
+    True
+    """
+    char_width = unicodedata.east_asian_width(char)
+    return char_width in u"WFA"
+
+
+def zenkaku_len(string):
+    u"""
+    Count Japanese ZENKAKU characters from string
+
+    >>> zenkaku_len(u"abc")
+    0
+    >>> zenkaku_len(u"あいう")
+    3
+    >>> zenkaku_len(u"あいc")
+    2
+    """
+    return len([x for x in string  if is_zenkaku(x)])
+
+
+def hankaku_len(string):
+    u"""
+    Count non Japanese ZENKAKU characters from string
+
+    >>> hankaku_len(u"abc")
+    3
+    >>> hankaku_len(u"あいう")
+    0
+    >>> hankaku_len(u"あいc")
+    1
+    """
+    return len([x for x in string  if not is_zenkaku(x)])
+
+
+def string_width(string):
+    u"""
+    Measure rendering width of string.
+    Count ZENKAKU-character as 2-point and non ZENKAKU-character as 1-point
+
+    >>> string_width(u"abc")
+    3
+    >>> string_width(u"あいう")
+    6
+    >>> string_width(u"あいc")
+    5
+    """
+    width = 0
+    for c in string:
+        char_width = unicodedata.east_asian_width(c)
+        if char_width in u"WFA":
+            width += 2
+        else:
+            width += 1
+
+    return width
+
+
+class TextFolder(object):
+    def __init__(self, box, string, font, **kwargs):
+        self.box = box
+        self.string = string
+        self.font = font
+        self.scale = 1
+        self.scale = 1
+        self.halign = kwargs.get('halign', 'center')
+        self.valign = kwargs.get('valign', 'center')
+        self.padding = kwargs.get('padding', 8)
+        self.line_spacing = kwargs.get('line_spacing', 2)
+
+        if kwargs.get('adjustBaseline'):
+            self.adjustBaseline = True
+        else:
+            self.adjustBaseline = False
+
+        self._result = self._lines()
+
+    def textsize(self, string):
+        u"""
+        Measure rendering size (width and height) of line.
+        Returned size will not be exactly as rendered text size,
+        Because this method does not use fonts to measure size.
+
+        >>> box = [0, 0, 100, 50]
+        >>> _font = FontInfo('serif', None, 11)
+        >>> TextFolder(box, "", _font).textsize(u"abc")
+        (19, 11)
+        >>> TextFolder(box, "", _font).textsize(u"あいう")
+        (33, 11)
+        >>> TextFolder(box, "", _font).textsize(u"あいc")
+        (29, 11)
+        >>> font = FontInfo('serif', None, 24)
+        >>> TextFolder(box, "", font).textsize(u"abc")
+        (40, 24)
+        >>> font = FontInfo('serif', None, 18)
+        >>> TextFolder(box, "", font).textsize(u"あいう")
+        (54, 18)
+        """
+        width = zenkaku_len(string) * self.font.size + \
+                hankaku_len(string) * self.font.size * 0.55
+        return (int(math.ceil(width)), self.font.size)
+
+    def height(self):
+        u"""
+        Measure rendering height of text.
+
+        If texts is heighter than bounding box,
+        jut out lines will be cut off.
+
+        >>> box = [0, 0, 100, 50]
+        >>> _font = FontInfo('serif', None, 11)
+        >>> TextFolder(box, u"abc", _font).height()
+        11
+        >>> TextFolder(box, u"abc\\ndef", _font).height()
+        24
+        >>> TextFolder(box, u"abc\\n\\ndef", _font).height()
+        37
+        >>> TextFolder(box, u"abc\\ndef\\nghi\\njkl", _font).height()
+        50
+        >>> TextFolder(box, u"abc\\ndef\\nghi\\njkl\\nmno", _font).height()
+        50
+        >>> font = FontInfo('serif', None, 24)
+        >>> TextFolder(box, u"abc", font).height()
+        24
+        >>> TextFolder(box, u"abc\\ndef", _font, line_spacing=8).height()
+        30
+        >>> font = FontInfo('serif', None, 15)
+        >>> TextFolder(box, u"abc\\ndef", font, line_spacing=8).height()
+        38
+        """
+        height = 0
+        for string in self._result:
+            height += self.textsize(string)[1]
+
+        if len(self._result) > 1:
+            height += (len(self._result) - 1) * self.line_spacing
+
+        return height
+
+    @property
+    def lines(self):
+        size = XY(self.box[2] - self.box[0], self.box[3] - self.box[1])
+
+        if self.valign == 'top':
+            height = self.line_spacing
+        elif self.valign == 'bottom':
+            height = size.y - self.height() - self.line_spacing
+        else:
+            height = int(math.ceil((size.y - self.height()) / 2.0))
+        base_xy = XY(self.box[0], self.box[1])
+
+        for string in self._result:
+            textsize = self.textsize(string)
+
+            halign = size.x - textsize[0] * self.scale
+            if self.halign == 'left':
+                x = self.padding
+            elif self.halign == 'right':
+                x = halign - self.padding
+            else:
+                x = int(math.ceil(halign / 2.0))
+
+            if self.adjustBaseline:
+                height += textsize[1]
+            draw_xy = XY(base_xy.x + x, base_xy.y + height)
+
+            yield string, draw_xy
+
+            if self.adjustBaseline:
+                height += self.line_spacing
+            else:
+                height += textsize[1] + self.line_spacing
+
+    @property
+    def outlinebox(self):
+        corners = []
+        for string, xy in self.lines:
+            textsize = self.textsize(string)
+            width = textsize[0] * self.scale
+            height = textsize[1] * self.scale
+
+            if self.adjustBaseline:
+                xy = XY(xy.x, xy.y - textsize[1])
+
+            corners.append(xy)
+            corners.append(XY(xy.x + width, xy.y + height))
+
+        if corners:
+            box = Box(min(p.x for p in corners) - self.padding,
+                      min(p.y for p in corners) - self.line_spacing,
+                      max(p.x for p in corners) + self.padding,
+                      max(p.y for p in corners) + self.line_spacing)
+        else:
+            box = [self.box[0], self.box[1], self.box[0], self.box[1]]
+
+        return box
+
+    def _splitlines(self):
+        u"""
+        Split text to lines as generator.
+        Every line will be stripped.
+        If text includes characters "\n", treat as line separator.
+
+        >>> box = [0, 0, 100, 50]
+        >>> ft = FontInfo('serif', None, 11)
+        >>> [l for l in TextFolder(box, u"abc", ft)._splitlines()]
+        [u'abc']
+        >>> [l for l in TextFolder(box, u"abc\\ndef", ft)._splitlines()]
+        [u'abc', u'def']
+        >>> [l for l in TextFolder(box, u"abc\\\\ndef", ft)._splitlines()]
+        [u'abc', u'def']
+        >>> [l for l in TextFolder(box, u" abc \\n def ", ft)._splitlines()]
+        [u'abc', u'def']
+        >>> [l for l in TextFolder(box, u" \\nabc\\\\ndef", ft)._splitlines()]
+        [u'abc', u'def']
+        >>> [l for l in TextFolder(box, u" \\\\nab \\\\ncd", ft)._splitlines()]
+        [u'', u'ab', u'cd']
+        >>> [l for l in TextFolder(box, u"abc\\\\\\\\ndef", ft)._splitlines()]
+        [u'abc\\\\ndef']
+        >>> [l for l in TextFolder(box, u"abc\xa5\\\\ndef", ft)._splitlines()]
+        [u'abc\\\\ndef']
+        """
+        string = re.sub('^\s*(.*?)\s*$', '\\1', self.string)
+        string = re.sub('(?:\xa5|\\\\){2}', '\x00', string)
+        string = re.sub('(?:\xa5|\\\\)n', '\n', string)
+        for line in string.splitlines():
+            yield re.sub('\x00', '\\\\', line).strip()
+
+    def _lines(self):
+        lines = []
+        size = (self.box[2] - self.box[0], self.box[3] - self.box[1])
+
+        height = 0
+        truncated = 0
+        for line in self._splitlines():
+            while True:
+                string = line.strip()
+                for i in range(0, len(string)):
+                    length = len(string) - i
+                    metrics = self.textsize(string[0:length])
+
+                    if metrics[0] <= size[0]:
+                        break
+                else:
+                    length = 0
+                    metrics = self.textsize(u" ")
+
+                if size[1] < height + metrics[1]:
+                    truncated = 1
+                    break
+
+                lines.append(string[0:length])
+                height += metrics[1] + self.line_spacing
+
+                line = string[length:]
+                if line == "":
+                    break
+
+        # truncate last line.
+        if len(lines) == 0:
+            pass
+        elif truncated:
+            string = lines.pop()
+            for i in range(0, len(string)):
+                if i == 0:
+                    truncated = string + ' ...'
+                else:
+                    truncated = string[0:-i] + ' ...'
+
+                metrics = self.textsize(truncated)
+                if metrics[0] <= size[0]:
+                    lines.append(truncated)
+                    break
+
+        return lines
diff --git a/src/blockdiag/utils/__init__.py b/src/blockdiag/utils/__init__.py
new file mode 100644
index 0000000..a50cdfe
--- /dev/null
+++ b/src/blockdiag/utils/__init__.py
@@ -0,0 +1,107 @@
+# -*- 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.
+
+import re
+from namedtuple import namedtuple
+
+
+Size = namedtuple('Size', 'width height')
+
+
+class XY(tuple):
+    mapper = dict(x=0, y=1)
+
+    def __new__(cls, x, y):
+        return super(XY, cls).__new__(cls, (x, y))
+
+    def __getattr__(self, name):
+        return self[self.mapper[name]]
+
+    def shift(self, x=0, y=0):
+        return self.__class__(self.x + x, self.y + y)
+
+
+class Box(list):
+    mapper = dict(x1=0, y1=1, x2=2, y2=3)
+
+    def __init__(self, x1, y1, x2, y2):
+        return super(Box, self).__init__((x1, y1, x2, y2))
+
+    def __getattr__(self, name):
+        return self[self.mapper[name]]
+
+    def __repr__(self):
+        class_name = self.__class__.__name__
+        x1 = self.x1
+        y1 = self.y1
+        width = self.width
+        height = self.height
+        addr = id(self)
+
+        format = "<%(class_name)s (%(x1)s, %(y1)s) " + \
+                 "%(width)dx%(height)d at 0x%(addr)08x>"
+        return format % locals()
+
+    def shift(self, x=0, y=0):
+        return self.__class__(self.x1 + x, self.y1 + y,
+                              self.x2 + x, self.y2 + y)
+
+    @property
+    def size(self):
+        return Size(self.width, self.height)
+
+    @property
+    def width(self):
+        return self.x2 - self.x1
+
+    @property
+    def height(self):
+        return self.y2 - self.y1
+
+    @property
+    def topleft(self):
+        return XY(self.x1, self.y1)
+
+    @property
+    def top(self):
+        return XY(self.x1 + self.width / 2, self.y1)
+
+    @property
+    def topright(self):
+        return XY(self.x2, self.y1)
+
+    @property
+    def bottomleft(self):
+        return XY(self.x1, self.y2)
+
+    @property
+    def bottom(self):
+        return XY(self.x1 + self.width / 2, self.y2)
+
+    @property
+    def bottomright(self):
+        return XY(self.x2, self.y2)
+
+    @property
+    def left(self):
+        return XY(self.x1, self.y1 + self.height / 2)
+
+    @property
+    def right(self):
+        return XY(self.x2, self.y1 + self.height / 2)
+
+    @property
+    def center(self):
+        return XY(self.x1 + self.width / 2, self.y1 + self.height / 2)
diff --git a/src/blockdiag/utils/bootstrap.py b/src/blockdiag/utils/bootstrap.py
new file mode 100644
index 0000000..2856308
--- /dev/null
+++ b/src/blockdiag/utils/bootstrap.py
@@ -0,0 +1,208 @@
+# -*- 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.
+
+import os
+import re
+import sys
+from optparse import OptionParser
+from blockdiag.utils.config import ConfigParser
+from blockdiag.utils.fontmap import parse_fontpath, FontMap
+
+
+class Application(object):
+    module = None
+
+    def run(self):
+        try:
+            self.parse_options()
+            self.create_fontmap()
+
+            parsed = self.parse_diagram()
+            return self.build_diagram(parsed)
+        except UnicodeEncodeError, e:
+            msg = "ERROR: UnicodeEncodeError caught " + \
+                  "(check your font settings)\n"
+            sys.stderr.write(msg)
+            return -1
+        except Exception, e:
+            sys.stderr.write("ERROR: %s\n" % e)
+            return -1
+
+    def parse_options(self):
+        self.options = Options(self.module).parse()
+
+    def create_fontmap(self):
+        self.fontmap = create_fontmap(self.options)
+
+    def parse_diagram(self):
+        if self.options.input == '-':
+            import codecs
+            stream = codecs.getreader('utf-8')(sys.stdin)
+            tree = self.module.parser.parse_string(stream.read())
+        else:
+            tree = self.module.parser.parse_file(self.options.input)
+
+        return tree
+
+    def build_diagram(self, tree):
+        DiagramDraw = self.module.drawer.DiagramDraw
+
+        diagram = self.module.builder.ScreenNodeBuilder.build(tree)
+
+        drawer = DiagramDraw(self.options.type, diagram,
+                             self.options.output, fontmap=self.fontmap,
+                             antialias=self.options.antialias,
+                             nodoctype=self.options.nodoctype)
+        drawer.draw()
+        drawer.save()
+
+        return 0
+
+
+class Options(object):
+    def __init__(self, module):
+        self.module = module
+        self.build_parser()
+
+    def parse(self):
+        self.options, self.args = self.parser.parse_args()
+        self.validate()
+        self.read_configfile()
+
+        return self.options
+
+    def build_parser(self):
+        version = "%%prog %s" % self.module.__version__
+        usage = "usage: %prog [options] infile"
+        self.parser = p = OptionParser(usage=usage, version=version)
+        p.add_option('-a', '--antialias', action='store_true',
+                     help='Pass diagram image to anti-alias filter')
+        p.add_option('-c', '--config',
+                     help='read configurations from FILE', metavar='FILE')
+        p.add_option('-o', dest='output',
+                     help='write diagram to FILE', metavar='FILE')
+        p.add_option('-f', '--font', default=[], action='append',
+                     help='use FONT to draw diagram', metavar='FONT')
+        p.add_option('--fontmap',
+                     help='use FONTMAP file to draw diagram', metavar='FONT')
+        p.add_option('-T', dest='type', default='PNG',
+                     help='Output diagram as TYPE format')
+        p.add_option('--nodoctype', action='store_true',
+                     help='Do not output doctype definition tags (SVG only)')
+
+        return p
+
+    def validate(self):
+        if len(self.args) == 0:
+            self.parser.print_help()
+            sys.exit(0)
+
+        self.options.input = self.args.pop(0)
+        if self.options.output:
+            pass
+        elif self.options.output == '-':
+            self.options.output = 'output.' + self.options.type.lower()
+        else:
+            ext = '.%s' % self.options.type.lower()
+            self.options.output = re.sub('\..*?$', ext, self.options.input)
+
+        self.options.type = self.options.type.upper()
+        if not self.options.type in ('SVG', 'PNG', 'PDF'):
+            msg = "unknown format: %s" % self.options.type
+            raise RuntimeError(msg)
+
+        if self.options.type == 'PDF':
+            try:
+                import reportlab.pdfgen.canvas
+            except ImportError:
+                msg = "could not output PDF format; Install reportlab."
+                raise RuntimeError(msg)
+
+        if self.options.nodoctype and self.options.type != 'SVG':
+            msg = "--nodoctype option work in SVG images."
+            raise RuntimeError(msg)
+
+        if self.options.config and not os.path.isfile(self.options.config):
+            msg = "config file is not found: %s" % self.options.config
+            raise RuntimeError(msg)
+
+        if self.options.fontmap and not os.path.isfile(self.options.fontmap):
+            msg = "fontmap file is not found: %s" % self.options.fontmap
+            raise RuntimeError(msg)
+
+    def read_configfile(self):
+        if self.options.config:
+            configpath = self.options.config
+        elif os.environ.get('HOME'):
+            configpath = '%s/.blockdiagrc' % os.environ.get('HOME')
+        elif os.environ.get('USERPROFILE'):
+            configpath = '%s/.blockdiagrc' % os.environ.get('USERPROFILE')
+        else:
+            configpath = ''
+
+        appname = self.module.__name__
+        if os.path.isfile(configpath):
+            config = ConfigParser()
+            config.read(configpath)
+
+            if config.has_option(appname, 'fontpath'):
+                fontpath = config.get(appname, 'fontpath')
+                self.options.font.append(fontpath)
+
+            if config.has_option(appname, 'fontmap'):
+                if self.options.fontmap is None:
+                    self.options.fontmap = config.get(appname, 'fontmap')
+
+            if self.options.fontmap is None:
+                self.options.fontmap = configpath
+
+
+def detectfont(options):
+    fonts = ['c:/windows/fonts/VL-Gothic-Regular.ttf',  # for Windows
+             'c:/windows/fonts/msgothic.ttf',  # for Windows
+             'c:/windows/fonts/msgoth04.ttc',  # for Windows
+             '/usr/share/fonts/truetype/ipafont/ipagp.ttf',  # for Debian
+             '/usr/local/share/font-ipa/ipagp.otf',  # for FreeBSD
+             '/Library/Fonts/Hiragino Sans GB W3.otf',  # for MacOS
+             '/System/Library/Fonts/AppleGothic.ttf']  # for MacOS
+
+    fontpath = None
+    if options.font:
+        for path in options.font:
+            _path, index = parse_fontpath(path)
+            if os.path.isfile(_path):
+                fontpath = path
+                break
+        else:
+            msg = 'fontfile is not found: %s' % options.font
+            raise RuntimeError(msg)
+
+    if fontpath is None:
+        for path in fonts:
+            _path, index = parse_fontpath(path)
+            if os.path.isfile(_path):
+                fontpath = path
+                break
+
+    return fontpath
+
+
+def create_fontmap(options):
+    fontmap = FontMap(options.fontmap)
+    if fontmap.find().path is None or options.font:
+        fontpath = detectfont(options)
+        fontmap.set_default_font(fontpath)
+
+    return fontmap
diff --git a/src/blockdiag/utils/collections.py b/src/blockdiag/utils/collections.py
new file mode 100644
index 0000000..89bfee9
--- /dev/null
+++ b/src/blockdiag/utils/collections.py
@@ -0,0 +1,39 @@
+# -*- 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.
+
+
+class defaultdict(dict):
+    def __init__(self, default_factory, *args, **kwargs):
+        self.default_factory = default_factory
+
+    def __getitem__(self, key):
+        try:
+            return super(defaultdict, self).__getitem__(key)
+        except:
+            return self.default_factory()
+
+
+def namedtuple(name, fields):
+    'Only space-delimited fields are supported.'
+    def prop(i, name):
+        return (name, property(lambda self: self[i]))
+    methods = dict(prop(i, f) for i, f in enumerate(fields.split(' ')))
+    methods.update({
+        '__new__': lambda cls, *args: tuple.__new__(cls, args),
+        '__repr__': lambda self: '%s(%s)' % (
+            name,
+            ', '.join('%s=%r' % (
+                f, getattr(self, f)) for f in fields.split(' ')))})
+    return type(name, (tuple,), methods)
diff --git a/src/blockdiag/utils/config.py b/src/blockdiag/utils/config.py
new file mode 100644
index 0000000..681f977
--- /dev/null
+++ b/src/blockdiag/utils/config.py
@@ -0,0 +1,40 @@
+# -*- 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.
+
+import re
+import sys
+import codecs
+from ConfigParser import SafeConfigParser
+
+
+class ConfigParser(SafeConfigParser):
+    def __init__(self):
+        if sys.version_info > (2, 6) and sys.version_info < (2, 7):
+            # only for Python2.6
+            # - dict_type argument is supported py2.6 or later
+            # - SafeConfigParser of py2.7 uses OrderedDict as default
+            from ordereddict import OrderedDict
+            SafeConfigParser.__init__(self, dict_type=OrderedDict)
+        else:
+            SafeConfigParser.__init__(self)
+
+    def read(self, path):
+        if sys.version_info > (2, 5):
+            fd = codecs.open(path, 'r', 'utf-8-sig')
+        else:
+            fd = codecs.open(path, 'r', 'utf-8')
+
+        self.readfp(fd)
+        fd.close()
diff --git a/src/blockdiag/utils/ellipse.py b/src/blockdiag/utils/ellipse.py
new file mode 100644
index 0000000..580a2f5
--- /dev/null
+++ b/src/blockdiag/utils/ellipse.py
@@ -0,0 +1,57 @@
+# -*- 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.
+
+import math
+from blockdiag.utils import XY
+
+DIVISION = 1000.0
+CYCLE = 10
+
+
+def angles(du, a, b, start, end):
+    phi = (start / 180.0) * math.pi
+    while phi <= (end / 180.0) * math.pi:
+        yield phi
+        phi += du / math.sqrt((a * math.sin(phi)) ** 2 + \
+                              (b * math.cos(phi)) ** 2)
+
+
+def coordinate(du, a, b, start, end):
+    for angle in angles(du, a, b, start, end):
+        yield (a * math.cos(angle), b * math.sin(angle))
+
+
+def dots(box, cycle, start=0, end=360):
+    width = box[2] - box[0]
+    height = box[3] - box[1]
+    center = XY(box[0] + width / 2, box[1] + height / 2)
+
+    # calcrate rendering pattern from cycle
+    base = 0
+    rendered = []
+    for index in range(0, len(cycle), 2):
+        i, j = cycle[index:index + 2]
+        for n in range(base * 2, (base + i) * 2):
+            rendered.append(n)
+        base += i + j
+
+    a = float(width) / 2
+    b = float(height) / 2
+    du = 1
+    _max = sum(cycle) * 2
+    for i, coord in enumerate(coordinate(du, a, b, start, end)):
+        if i % _max in rendered:
+            dot = XY(center.x + coord[0], center.y + coord[1])
+            yield dot
diff --git a/src/blockdiag/utils/fontmap.py b/src/blockdiag/utils/fontmap.py
new file mode 100644
index 0000000..ee28e1e
--- /dev/null
+++ b/src/blockdiag/utils/fontmap.py
@@ -0,0 +1,160 @@
+# -*- 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.
+
+import re
+import os
+import sys
+import copy
+import codecs
+from config import ConfigParser
+from blockdiag.utils.collections import namedtuple
+
+
+def parse_fontpath(path):
+    if path is None:
+        return (None, None)
+
+    match = re.search('^(.*):(\d)$', path)
+    if match:
+        return (match.group(1), int(match.group(2)))
+    else:
+        return (path, None)
+
+
+class FontInfo(object):
+    def __init__(self, family, path, size):
+        self.path = path
+        self.size = int(size)
+
+        family = self._parse(family)
+        self.name = family[0]
+        self.generic_family = family[1]
+        self.weight = family[2]
+        self.style = family[3]
+
+    @property
+    def familyname(self):
+        if self.name:
+            name = self.name + "-"
+        else:
+            name = ''
+
+        if self.weight == 'bold':
+            return "%s%s-%s" % (name, self.generic_family, self.weight)
+        else:
+            return "%s%s-%s" % (name, self.generic_family, self.style)
+
+    def _parse(self, familyname):
+        pattern = '^(?:(.*)-)?' + \
+                  '(serif|sansserif|monospace|fantasy|cursive)' + \
+                  '(?:-(normal|bold|italic|oblique))?$'
+
+        match = re.search(pattern, familyname or '')
+        if match is None:
+            msg = 'Unknown font family: %s' % familyname
+            raise AttributeError(msg)
+
+        name = match.group(1) or ''
+        generic_family = match.group(2)
+        style = match.group(3) or ''
+
+        if style == 'bold':
+            weight = 'bold'
+            style = 'normal'
+        elif style in ('italic', 'oblique'):
+            weight = 'normal'
+            style = style
+        else:
+            weight = 'normal'
+            style = 'normal'
+
+        return [name, generic_family, weight, style]
+
+    def duplicate(self):
+        return copy.copy(self)
+
+
+class FontMap(object):
+    fontsize = 11
+    default_fontfamily = 'sansserif'
+
+    def __init__(self, filename=None):
+        self.fonts = {}
+        self.aliases = {}
+
+        if filename:
+            self._parse_config(filename)
+        self.set_default_font(None)
+
+    def set_default_fontfamily(self, fontfamily):
+        self.default_fontfamily = fontfamily
+        self.set_default_font(None)
+
+    def _parse_config(self, conffile):
+        config = ConfigParser()
+
+        if hasattr(conffile, 'read'):
+            config.readfp(conffile)
+        elif os.path.isfile(conffile):
+            config.read(conffile)
+        else:
+            msg = "fontmap file is not found: %s" % conffile
+            raise RuntimeError(msg)
+
+        if config.has_section('fontmap'):
+            for name, path in config.items('fontmap'):
+                self.append_font(name, path)
+
+        if config.has_section('fontalias'):
+            for name, family in config.items('fontalias'):
+                self.aliases[name] = family
+
+    def set_default_font(self, path):
+        if path is None and self.find() is not None:
+            return
+
+        self.append_font(self.default_fontfamily, path)
+
+    def append_font(self, fontfamily, path):
+        _path, index = parse_fontpath(path)
+        if path is None or os.path.isfile(_path):
+            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)
+
+    def _regulate_familyname(self, name):
+        return FontInfo(name, None, 11).familyname
+
+    def find(self, element=None):
+        fontfamily = getattr(element, 'fontfamily', None) or \
+                       self.default_fontfamily
+        fontfamily = self.aliases.get(fontfamily, fontfamily)
+        fontsize = getattr(element, 'fontsize', None) or self.fontsize
+
+        name = self._regulate_familyname(fontfamily)
+        if name in self.fonts:
+            font = self.fonts[name].duplicate()
+            font.size = fontsize
+        elif element is not None:
+            msg = "Unknown fontfamily: %s" % fontfamily
+            sys.stderr.write("WARNING: %s\n" % msg)
+            elem = namedtuple('Font', 'fontsize')(fontsize)
+            font = self.find(elem)
+        else:
+            font = None
+
+        return font
diff --git a/src/blockdiag/utils/images.py b/src/blockdiag/utils/images.py
new file mode 100644
index 0000000..a82a3ee
--- /dev/null
+++ b/src/blockdiag/utils/images.py
@@ -0,0 +1,94 @@
+# -*- 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.
+
+import re
+import urlutil
+try:
+    from PIL import Image
+except ImportError:
+    try:
+        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):
+                import jpeg
+                import png
+
+                try:
+                    size = jpeg.JpegFile.get_size(self.filename)
+                except:
+                    try:
+                        if isinstance(self.filename, (str, unicode)):
+                            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 = {}
+
+
+def get_image_size(filename):
+    if filename not in _image_size_cache:
+        uri = filename
+        if urlutil.isurl(filename):
+            import cStringIO
+            import urllib
+            try:
+                uri = cStringIO.StringIO(urllib.urlopen(filename).read())
+            except:
+                return None
+
+        _image_size_cache[filename] = Image.open(uri).size
+
+    return _image_size_cache[filename]
+
+
+def calc_image_size(size, bounded):
+    if bounded[0] < size[0] or bounded[1] < size[1]:
+        if (size[0] * 1.0 / bounded[0]) < (size[1] * 1.0 / bounded[1]):
+            size = (size[0] * bounded[1] / size[1], bounded[1])
+        else:
+            size = (bounded[0], size[1] * bounded[0] / size[0])
+
+    return size
+
+
+def color_to_rgb(color):
+    import webcolors
+    if color == 'none' or isinstance(color, (list, tuple)):
+        rgb = color
+    elif re.match('#', color):
+        rgb = webcolors.hex_to_rgb(color)
+    else:
+        rgb = webcolors.name_to_rgb(color)
+
+    return rgb
diff --git a/src/blockdiag/utils/jpeg.py b/src/blockdiag/utils/jpeg.py
new file mode 100644
index 0000000..d2149e1
--- /dev/null
+++ b/src/blockdiag/utils/jpeg.py
@@ -0,0 +1,89 @@
+# -*- 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.
+
+
+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, (str, unicode)):
+            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/myitertools.py b/src/blockdiag/utils/myitertools.py
new file mode 100644
index 0000000..f49e64d
--- /dev/null
+++ b/src/blockdiag/utils/myitertools.py
@@ -0,0 +1,46 @@
+# -*- 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 itertools import cycle
+
+
+def istep(seq, step=2):
+    iterable = iter(seq)
+    while True:
+        yield [iterable.next() for i in range(step)]
+
+
+def stepslice(iterable, steps):
+    iterable = iter(iterable)
+    step = cycle(steps)
+
+    while True:
+        # skip (1)
+        n = step.next()
+        if n == 0:
+            pass
+        elif n == 1:
+            o = iterable.next()
+            yield o
+            yield o
+        else:
+            yield iterable.next()
+            for i in xrange(n - 2):
+                iterable.next()
+            yield iterable.next()
+
+        # skip (2)
+        for i in xrange(step.next()):
+            iterable.next()
diff --git a/src/blockdiag/utils/namedtuple.py b/src/blockdiag/utils/namedtuple.py
new file mode 100644
index 0000000..edd6e34
--- /dev/null
+++ b/src/blockdiag/utils/namedtuple.py
@@ -0,0 +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.
+
+
+from collections import namedtuple
diff --git a/src/blockdiag/utils/rst/__init__.py b/src/blockdiag/utils/rst/__init__.py
new file mode 100644
index 0000000..bd36e96
--- /dev/null
+++ b/src/blockdiag/utils/rst/__init__.py
@@ -0,0 +1,14 @@
+# -*- 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.
diff --git a/src/blockdiag/utils/rst/directives.py b/src/blockdiag/utils/rst/directives.py
new file mode 100644
index 0000000..707ca0a
--- /dev/null
+++ b/src/blockdiag/utils/rst/directives.py
@@ -0,0 +1,265 @@
+# -*- 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.
+
+import os
+import codecs
+from docutils import nodes
+from docutils.parsers import rst
+from docutils.statemachine import ViewList
+from blockdiag import parser
+from blockdiag.command import detectfont
+from blockdiag.builder import ScreenNodeBuilder
+from blockdiag.drawer import DiagramDraw
+from blockdiag.utils.collections import namedtuple
+
+
+format = 'PNG'
+antialias = False
+fontpath = None
+outputdir = None
+
+
+def relfn2path(env, filename):
+    if filename.startswith('/') or filename.startswith(os.sep):
+        relfn = filename[1:]
+    else:
+        path = env.doc2path(env.docname, base=None)
+        relfn = os.path.join(os.path.dirname(path), filename)
+
+    return relfn, os.path.join(env.srcdir, relfn)
+
+
+def cmp_node_number(a, b):
+    try:
+        n1 = int(a[0])
+    except (TypeError, ValueError):
+        n1 = 65535
+
+    try:
+        n2 = int(b[0])
+    except (TypeError, ValueError):
+        n2 = 65535
+
+    return cmp(n1, n2)
+
+
+class blockdiag(nodes.General, nodes.Element):
+    pass
+
+
+class BlockdiagDirectiveBase(rst.Directive):
+    """ Directive to insert arbitrary dot markup. """
+    name = "blockdiag"
+    node_class = blockdiag
+
+    has_content = True
+    required_arguments = 0
+    optional_arguments = 1
+    final_argument_whitespace = False
+    option_spec = {
+        'alt': rst.directives.unchanged,
+        'desctable': rst.directives.flag,
+        'maxwidth': rst.directives.nonnegative_int,
+    }
+
+    def run(self):
+        if self.arguments:
+            document = self.state.document
+            if self.content:
+                msg = ('%s directive cannot have both content and '
+                       'a filename argument' % self.name)
+                return [document.reporter.warning(msg, line=self.lineno)]
+
+            try:
+                filename = self.source_filename(self.arguments[0])
+                fp = codecs.open(filename, 'r', 'utf-8')
+                try:
+                    dotcode = fp.read()
+                finally:
+                    fp.close()
+            except (IOError, OSError):
+                msg = 'External %s file %r not found or reading it failed' % \
+                      (self.name, filename)
+                return [document.reporter.warning(msg, line=self.lineno)]
+        else:
+            dotcode = '\n'.join(self.content).strip()
+            if not dotcode:
+                msg = 'Ignoring "%s" directive without content.' % self.name
+                return [self.state_machine.reporter.warning(msg,
+                                                            line=self.lineno)]
+
+        node = self.node_class()
+        node['code'] = dotcode
+        node['alt'] = self.options.get('alt')
+        node['options'] = {}
+        if 'maxwidth' in self.options:
+            node['options']['maxwidth'] = self.options['maxwidth']
+        if 'desctable' in self.options:
+            node['options']['desctable'] = self.options['desctable']
+
+        return [node]
+
+    def source_filename(self, filename):
+        if hasattr(self.state.document.settings, 'env'):
+            env = self.state.document.settings.env
+            rel_filename, filename = relfn2path(env, self.arguments[0])
+            env.note_dependency(rel_filename)
+
+        return filename
+
+
+class BlockdiagDirective(BlockdiagDirectiveBase):
+    def run(self):
+        results = super(BlockdiagDirective, self).run()
+
+        node = results[0]
+        if not isinstance(node, self.node_class):
+            return results
+
+        diagram = self.node2diagram(node)
+
+        if 'desctable' in node['options']:
+            del node['options']['desctable']
+            results.append(self.description_table(diagram))
+
+        results[0] = self.node2image(node, diagram)
+
+        return results
+
+    def node2diagram(self, node):
+        tree = parser.parse_string(node['code'])
+        return ScreenNodeBuilder.build(tree)
+
+    def node2image(self, node, diagram):
+        filename = self.image_filename(node)
+        fontpath = self.detectfont()
+        drawer = DiagramDraw(format, diagram, filename,
+                             font=fontpath, antialias=antialias)
+
+        if not os.path.isfile(filename):
+            drawer.draw()
+            drawer.save()
+
+        size = drawer.pagesize()
+        options = node['options']
+        if 'maxwidth' in options and options['maxwidth'] < size[0]:
+            ratio = float(options['maxwidth']) / size[0]
+            thumb_size = (options['maxwidth'], size[1] * ratio)
+
+            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)
+        else:
+            image = nodes.image(uri=filename)
+
+        if node['alt']:
+            image['alt'] = node['alt']
+
+        return image
+
+    def detectfont(self):
+        Options = namedtuple('Options', 'font')
+        if isinstance(fontpath, (list, tuple)):
+            options = Options(fontpath)
+        elif isinstance(fontpath, (str, unicode)):
+            options = Options([fontpath])
+        else:
+            options = Options([])
+
+        return detectfont(options)
+
+    def image_filename(self, node, prefix='', ext='png'):
+        try:
+            from hashlib import sha1 as sha
+        except ImportError:
+            from sha import sha
+
+        options = dict(node['options'])
+        options.update(font=fontpath, antialias=antialias)
+        hashseed = node['code'].encode('utf-8') + str(options)
+        hashed = sha(hashseed).hexdigest()
+
+        filename = "%s%s-%s.%s" % (self.name, prefix, hashed, format.lower())
+        if outputdir:
+            filename = os.path.join(outputdir, filename)
+
+        return filename
+
+    def description_table(self, diagram):
+        nodes = diagram.traverse_nodes
+        klass = diagram._DiagramNode
+
+        widths = [25] + [50] * (len(klass.desctable) - 1)
+        headers = [klass.attrname[n] for n in klass.desctable]
+
+        descriptions = [n.to_desctable() for n in nodes()]
+        descriptions.sort(cmp_node_number)
+
+        for i in range(len(headers) - 2, -1, -1):
+            if [desc[i] for desc in descriptions  if desc[i]]:
+                pass
+            else:
+                widths.pop(i)
+                headers.pop(i)
+                for desc in descriptions:
+                    desc.pop(i)
+
+        return self._description_table(descriptions, widths, headers)
+
+    def _description_table(self, descriptions, widths, headers):
+        # generate table-root
+        tgroup = nodes.tgroup(cols=len(widths))
+        for width in widths:
+            tgroup += nodes.colspec(colwidth=width)
+        table = nodes.table()
+        table += tgroup
+
+        # generate table-header
+        thead = nodes.thead()
+        row = nodes.row()
+        for header in headers:
+            entry = nodes.entry()
+            entry += nodes.paragraph(text=header)
+            row += entry
+        thead += row
+        tgroup += thead
+
+        # generate table-body
+        tbody = nodes.tbody()
+        for desc in descriptions:
+            row = nodes.row()
+            for attr in desc:
+                entry = nodes.entry()
+                self.state.nested_parse(ViewList([attr], source=attr),
+                                        0, entry)
+                row += entry
+            tbody += row
+        tgroup += tbody
+
+        return table
+
+
+def setup(**kwargs):
+    global format, antialias, fontpath, outputdir
+    format = kwargs.get('format', 'PNG')
+    antialias = kwargs.get('antialias', False)
+    fontpath = kwargs.get('fontpath', None)
+    outputdir = kwargs.get('outputdir', None)
+
+    rst.directives.register_directive("blockdiag", BlockdiagDirective)
diff --git a/src/blockdiag/utils/urlutil.py b/src/blockdiag/utils/urlutil.py
new file mode 100644
index 0000000..a1a061e
--- /dev/null
+++ b/src/blockdiag/utils/urlutil.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+import urlparse
+
+
+def isurl(url):
+    o = urlparse.urlparse(url)
+    accpetable = ["http", "https"]
+    if o[0] in accpetable:
+        return True
+    else:
+        return False
diff --git a/src/blockdiag/utils/uuid.py b/src/blockdiag/utils/uuid.py
new file mode 100644
index 0000000..4815374
--- /dev/null
+++ b/src/blockdiag/utils/uuid.py
@@ -0,0 +1,23 @@
+# -*- 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:
+    from uuid import uuid1 as uuid
+except ImportError:
+    from random import random as uuid
+
+
+def generate():
+    return str(uuid())
diff --git a/src/blockdiag_sphinxhelper.py b/src/blockdiag_sphinxhelper.py
new file mode 100644
index 0000000..86dbd91
--- /dev/null
+++ b/src/blockdiag_sphinxhelper.py
@@ -0,0 +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 blockdiag import command, parser, builder, drawer
+from blockdiag import parser as diagparser
+from blockdiag import drawer as DiagramDraw
+from blockdiag.utils import collections
+from blockdiag.utils.fontmap import FontMap
+from blockdiag.utils.rst.directives import blockdiag, BlockdiagDirective

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