[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