clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

migrate BlockDiag family to Cloudflare Python

Dami committed b8c1a30
commit
b8c1a30e30b9f981ee0db3e965e43cf78c070975
parent
96433786c533fb7ef1d54ada06159afc4d924175
author
Dami <dami.etoile@gmail.com>
155 files changed, +16949 -62
Adiagramzip-python-units/blockdiag-family/.gitignore6+
Adiagramzip-python-units/blockdiag-family/README.md106+++
Adiagramzip-python-units/blockdiag-family/THIRD_PARTY_NOTICES.md52+
Adiagramzip-python-units/blockdiag-family/pylock.toml28+
Adiagramzip-python-units/blockdiag-family/pyproject.toml22+
Adiagramzip-python-units/blockdiag-family/src/actdiag/__init__.py16+
Adiagramzip-python-units/blockdiag-family/src/actdiag/builder.py380+++++++++++
Adiagramzip-python-units/blockdiag-family/src/actdiag/command.py31+
Adiagramzip-python-units/blockdiag-family/src/actdiag/drawer.py55++
Adiagramzip-python-units/blockdiag-family/src/actdiag/elements.py42+
Adiagramzip-python-units/blockdiag-family/src/actdiag/metrics.py97+++
Adiagramzip-python-units/blockdiag-family/src/actdiag/parser.py254+++++++
Adiagramzip-python-units/blockdiag-family/src/actdiag/plugins/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/actdiag/plugins/autolane.py36+
Adiagramzip-python-units/blockdiag-family/src/actdiag/utils/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/actdiag/utils/rst/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/actdiag/utils/rst/directives.py44+
Adiagramzip-python-units/blockdiag-family/src/actdiag/utils/rst/nodes.py25+
Adiagramzip-python-units/blockdiag-family/src/assets/DejaVuSerif.ttfbin
Adiagramzip-python-units/blockdiag-family/src/assets/__init__.py1+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/__init__.py16+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/__main__.py7+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/builder.py745+++++++++++++++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/command.py68++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/drawer.py188+++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/elements.py662+++++++++++++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/__init__.py54++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/base.py75++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/filters/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/filters/linejump.py183+++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/pdf.py245+++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/png.py516+++++++++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/simplesvg.py242+++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/svg.py321+++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/textfolder.py302++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/utils/__init__.py69++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/utils/ellipse.py61++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/metrics.py1067++++++++++++++++++++++++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/__init__.py50+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/actor.py120+++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/base.py136++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/beginpoint.py61++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/box.py46+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/circle.py60++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/cloud.py135++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/diamond.py63++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/dots.py53+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/ellipse.py53+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/endpoint.py68++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/database.py135++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/input.py63++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/loopin.py68++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/loopout.py68++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/terminator.py118+++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/mail.py63++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/minidiamond.py53+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/none.py35+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/note.py61++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/roundedbox.py132++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/square.py61++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/textbox.py49+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/parser.py263+++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/plugins/__init__.py98+++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/plugins/attributes.py36+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/plugins/autoclass.py37+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/__init__.py190+++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/bootstrap.py327+++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/compat.py46+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/config.py24+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/fontmap.py171+++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/images.py152++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/logging.py28+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/myitertools.py52+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/rst/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/rst/directives.py397+++++++++++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/rst/nodes.py56++
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/urlutil.py25+
Adiagramzip-python-units/blockdiag-family/src/blockdiag/utils/uuid.py25+
Adiagramzip-python-units/blockdiag-family/src/entry.py194+++++
Adiagramzip-python-units/blockdiag-family/src/nwdiag/__init__.py16+
Adiagramzip-python-units/blockdiag-family/src/nwdiag/builder.py305+++++++++
Adiagramzip-python-units/blockdiag-family/src/nwdiag/command.py31+
Adiagramzip-python-units/blockdiag-family/src/nwdiag/drawer.py178+++++
Adiagramzip-python-units/blockdiag-family/src/nwdiag/elements.py165+++++
Adiagramzip-python-units/blockdiag-family/src/nwdiag/metrics.py184+++++
Adiagramzip-python-units/blockdiag-family/src/nwdiag/parser.py299++++++++
Adiagramzip-python-units/blockdiag-family/src/nwdiag/utils/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/nwdiag/utils/rst/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/nwdiag/utils/rst/directives.py49+
Adiagramzip-python-units/blockdiag-family/src/nwdiag/utils/rst/nodes.py25+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/__init__.py18+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/builder.py120+++
Adiagramzip-python-units/blockdiag-family/src/packetdiag/command.py31+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/drawer.py47+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/elements.py79++
Adiagramzip-python-units/blockdiag-family/src/packetdiag/metrics.py63++
Adiagramzip-python-units/blockdiag-family/src/packetdiag/noderenderers.py53+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/parser.py219++++++
Adiagramzip-python-units/blockdiag-family/src/packetdiag/utils/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/utils/rst/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/packetdiag/utils/rst/directives.py66++
Adiagramzip-python-units/blockdiag-family/src/packetdiag/utils/rst/nodes.py25+
Adiagramzip-python-units/blockdiag-family/src/protocol.py93+++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/__init__.py18+
Adiagramzip-python-units/blockdiag-family/src/rackdiag/builder.py123+++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/command.py31+
Adiagramzip-python-units/blockdiag-family/src/rackdiag/drawer.py64++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/elements.py237+++++++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/metrics.py76++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/parser.py262+++++++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/utils/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/rackdiag/utils/math.py41+
Adiagramzip-python-units/blockdiag-family/src/rackdiag/utils/rst/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/rackdiag/utils/rst/directives.py73++
Adiagramzip-python-units/blockdiag-family/src/rackdiag/utils/rst/nodes.py25+
Adiagramzip-python-units/blockdiag-family/src/render.py112+++
Adiagramzip-python-units/blockdiag-family/src/seqdiag/__init__.py16+
Adiagramzip-python-units/blockdiag-family/src/seqdiag/builder.py233+++++++
Adiagramzip-python-units/blockdiag-family/src/seqdiag/command.py31+
Adiagramzip-python-units/blockdiag-family/src/seqdiag/drawer.py178+++++
Adiagramzip-python-units/blockdiag-family/src/seqdiag/elements.py325+++++++++
Adiagramzip-python-units/blockdiag-family/src/seqdiag/metrics.py536+++++++++++++++
Adiagramzip-python-units/blockdiag-family/src/seqdiag/parser.py286++++++++
Adiagramzip-python-units/blockdiag-family/src/seqdiag/utils/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/seqdiag/utils/rst/__init__.py14+
Adiagramzip-python-units/blockdiag-family/src/seqdiag/utils/rst/directives.py44+
Adiagramzip-python-units/blockdiag-family/src/seqdiag/utils/rst/nodes.py25+
Adiagramzip-python-units/blockdiag-family/src/svg.py171+++++
Adiagramzip-python-units/blockdiag-family/src/upstream_runtime.py113+++
Adiagramzip-python-units/blockdiag-family/test/test_protocol.py32+
Adiagramzip-python-units/blockdiag-family/test/test_render.py92+++
Adiagramzip-python-units/blockdiag-family/test/test_upstream_runtime.py83++
Adiagramzip-python-units/blockdiag-family/uv.lock381+++++++++++
Adiagramzip-python-units/blockdiag-family/vendor-licenses/actdiag-LICENSE202++++++
Adiagramzip-python-units/blockdiag-family/vendor-licenses/blockdiag-LICENSE202++++++
Adiagramzip-python-units/blockdiag-family/vendor-licenses/dejavu-COPYRIGHT78++
Adiagramzip-python-units/blockdiag-family/vendor-licenses/nwdiag-LICENSE202++++++
Adiagramzip-python-units/blockdiag-family/vendor-licenses/seqdiag-LICENSE202++++++
Rdiagramzip-render/wrangler.compatibility/blockdiag-family.jsonc → diagramzip-python-units/blockdiag-family/wrangler.jsonc21+-
Mdiagramzip-render/README.md10+-
Mdiagramzip-render/scripts/deploy-compatibility-units.mjs2+-
Mdiagramzip-render/scripts/deploy-worker-units.mjs1-
Mdiagramzip-render/scripts/smoke-units.mjs10+
Mdiagramzip-render/src/catalog.ts20+-
Mdiagramzip-render/src/index.ts13+-
Mdiagramzip-render/test/catalog.test.ts15+-
Adiagramzip-render/test/gateway.test.ts34+
Mdiagramzip/src/preview.js7+-
Mdiagramzip/src/renderer-units.js10+
Adiagramzip/test/preview-routing.test.js90+++
Mdiagramzip/test/renderer-units.test.js14+-
Mdocs/refactor-engines.md98++-
Rserver/src/main/resources/web/diagramzip/assets/index-C8MuTfFX.js → server/src/main/resources/web/diagramzip/assets/index-WmdMHtkV.js20+-
Mserver/src/main/resources/web/diagramzip/index.html2+-
A diagramzip-python-units/blockdiag-family/.gitignore +6 -0
A diagramzip-python-units/blockdiag-family/README.md +106 -0
@@ -0,0 +1,106 @@
1+# BlockDiag family renderer2+3+One Cloudflare Python Worker renders BlockDiag, SeqDiag, ActDiag, NwDiag,4+PacketDiag, and RackDiag directly from pinned upstream Python libraries. The5+request hostname selects the engine. Request bodies cannot select a different6+engine.7+8+The Worker imports renderer libraries directly and does not execute a command,9+spawn a process, call Java, or fetch the Fly compatibility origin. Remote and10+filesystem-backed images are rejected. SVG is sanitized before presentation11+metadata, padding, backgrounds, and framing are applied.12+13+The six engines share one Worker because they are one compatible upstream14+dependency family: the four source trees provide the same parser/builder,15+SVG drawer, font, and node-renderer model. They retain six hostnames so the16+hostname remains the only engine selector; sharing the deployment avoids six17+copies of Pillow, the font, and the registration table without allowing one18+request to select another engine.19+20+## Reproduce locally21+22+Run these commands from this directory:23+24+```sh25+uv sync26+uv run pytest -q27+uv run pywrangler sync28+uv run pywrangler dev --port 878829+```30+31+The Worker build is Pyodide-based. `pywrangler sync` is therefore the32+important packaging check; ordinary `uv sync` only checks the CPython test33+environment. Deploy only after both checks pass:34+35+```sh36+uv run pywrangler deploy37+```38+39+For a local request, route the hostname to the development port, for example:40+41+```sh42+curl --resolve blockdiag.render.diagram.zip:8788:127.0.0.1 \43+ -H 'Content-Type: application/json' \44+ --data '{"source":"blockdiag { a -> b; }"}' \45+ http://blockdiag.render.diagram.zip:8788/v1/svg46+```47+48+The request body must not contain an `engine` field. Wrangler's local custom49+domain emulation currently resolves every configured hostname as the first50+route, so use the CPython six-engine fixture suite locally and smoke all six51+real hostnames after deployment. Do not treat a local `Host` header as proof52+of per-host dispatch.53+54+## Parity and Java reference procedure55+56+The legacy Java/Kroki implementation is a reference only. Run it through57+Docker; do not install Java or invoke Maven on the host:58+59+```sh60+docker build -f ../../Dockerfile.fly -t diagramzip-kroki:local ../..61+docker run --rm -p 8000:8000 diagramzip-kroki:local62+```63+64+Send the same fixtures to the Docker reference and to the Worker development65+port, then compare structural SVG coverage and record engine-specific losses.66+Pixel equality is not a gate for this extraction. The repository's broader67+Docker smoke stack is available with:68+69+```sh70+docker compose -f ../../ci/tests/docker-compose.yaml up --abort-on-container-exit71+```72+73+Pinned upstream revisions and their licenses are recorded in74+`THIRD_PARTY_NOTICES.md` and `uv.lock`.75+76+## Boundaries and known losses77+78+The request protocol enforces a 1 MiB body limit, a 512 KiB source limit, at79+most 64 options (4 KiB per option), padding from 0 through 256, and a 4 MiB80+sanitized SVG limit. Only SVG output is exposed. Remote URLs and filesystem81+images are rejected before they can be fetched. Returned SVG is parsed with82+`defusedxml`; scripts, event handlers, active elements, external references,83+unsafe CSS, and unsafe embedded resources are removed.84+85+Supported renderer options currently include `size=WIDTHxHEIGHT`, `antialias`,86+`no-transparency`, and `no-doctype`. The two raster-only flags, `antialias` and87+`no-transparency`, intentionally have no effect on SVG. Other upstream88+command-line options are validated by the common protocol but are not silently89+passed to a CLI: they are documented loss until a direct API mapping is added.90+The Worker uses the same bundled DejaVu Serif font path as the Docker reference91+for deterministic text metrics; font fallback can still differ from a larger92+host installation.93+94+The vendored runtime replaces upstream `importlib.metadata` entry-point95+discovery with an explicit registration table. It installs the SVG drawer,96+all bundled node renderers (including PacketDiag), and only the built-in97+`attributes`, `autoclass`, and `autolane` plugins. This keeps the Worker98+reproducible and prevents arbitrary installed plugins from entering the99+rendering process.100+101+The upstream libraries expose process-wide mutable renderer and plugin102+registries. Registration is one-time startup work; each render and its cleanup103+are synchronous with no `await` boundary, so requests cannot interleave while104+those registries are mutated inside one isolate. Cloudflare may run multiple105+isolates, each with its own Python runtime and registries. Keep rendering106+synchronous unless the registries are first made request-local.
A diagramzip-python-units/blockdiag-family/THIRD_PARTY_NOTICES.md +52 -0
@@ -0,0 +1,52 @@
1+# Third-party renderer sources2+3+The Worker vendors source from these exact Apache-2.0-licensed upstream4+repositories. The commit is the source-of-truth revision used in `src/`;5+the package version is included where the upstream project publishes one.6+7+- [Kroki BlockDiag fork](https://github.com/yuzutech/blockdiag/tree/f0d23ca664462d09f19d7b8e706a5a024b7d5f4d)8+ v3.4.2, commit `f0d23ca664462d09f19d7b8e706a5a024b7d5f4d`9+- [SeqDiag](https://github.com/blockdiag/seqdiag/tree/9ffd7bf4e65f2508f3f7a2041f77564bc44deafa)10+ 3.0.0, commit `9ffd7bf4e65f2508f3f7a2041f77564bc44deafa`11+- [ActDiag](https://github.com/blockdiag/actdiag/tree/f74203528d6ae3ba43898f93505e01d153677062)12+ 3.0.0, commit `f74203528d6ae3ba43898f93505e01d153677062`13+- [NwDiag](https://github.com/blockdiag/nwdiag/tree/04bbb4581e90ea891fe826be6c82f4aaee522f26)14+ 3.0.0, commit `04bbb4581e90ea891fe826be6c82f4aaee522f26`; this source also15+ supplies PacketDiag and RackDiag.16+17+The complete Apache-2.0 texts are retained in `vendor-licenses/`:18+`blockdiag-LICENSE`, `seqdiag-LICENSE`, `actdiag-LICENSE`, and19+`nwdiag-LICENSE`. The static registry in `src/upstream_runtime.py` is a20+diagram.zip adapter: it replaces metadata entry-point discovery with the21+vendored SVG drawer, all vendored node renderers, PacketDiag's renderer, and22+the three built-in plugins (`attributes`, `autoclass`, `autolane`).23+24+## Runtime dependencies25+26+`uv.lock` pins the Pyodide-packaged runtime dependencies:27+28+- [Pillow](https://github.com/python-pillow/Pillow), 11.3.0 — HPND-style29+ Pillow license; used for text measurement and image primitives.30+- [funcparserlib](https://github.com/vlasov-volnov/funcparserlib), 1.0.1 —31+ BSD-3-Clause.32+- [webcolors](https://github.com/ubernostrum/webcolors), 25.10.0 —33+ BSD-3-Clause.34+- [defusedxml](https://github.com/tiran/defusedxml), 0.7.1 — Python35+ Software Foundation License; used for defensive SVG parsing.36+37+The lock file contains the resolved URLs and hashes. Development-only38+packages (`pytest`, `workers-py`, and the Workers runtime SDK) are not part of39+the renderer's application dependency list.40+41+## Bundled font42+43+`src/assets/DejaVuSerif.ttf` is from the [DejaVu Fonts project](https://dejavu-fonts.github.io/).44+Its Debian copyright/permission text is retained as45+`vendor-licenses/dejavu-COPYRIGHT`. The file is distributed under the46+Bitstream Vera terms for the original glyphs, with DejaVu changes in the47+public domain; the font is used only to make Worker text metrics deterministic.48+49+The Worker also bundles the resolved dependencies from `uv.lock`; package50+metadata and license files are carried into the generated `python_modules`51+deployment directory by Pywrangler. Re-run `uv lock` deliberately when52+updating dependencies and review the resulting hashes and license changes.
A diagramzip-python-units/blockdiag-family/pylock.toml +28 -0
@@ -0,0 +1,28 @@
1+lock-version = "1.0"2+created-by = "uv"3+requires-python = ">=3.13.2"4+5+[[packages]]6+name = "defusedxml"7+version = "0.7.1"8+wheels = [{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", upload-time = 2021-03-08T10:59:24Z, size = 25604, hashes = { sha256 = "a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" } }]9+10+[[packages]]11+name = "funcparserlib"12+version = "1.0.1"13+wheels = [{ url = "https://files.pythonhosted.org/packages/83/66/acd740d84f59c655935f586c113a863aa404dfe932052a68a1163d88ea63/funcparserlib-1.0.1-py2.py3-none-any.whl", upload-time = 2022-11-03T23:10:53Z, size = 17842, hashes = { sha256 = "95da15d3f0d00b9b6f4bf04005c708af3faa115f7b45692ace064ebe758c68e8" } }]14+15+[[packages]]16+name = "pillow"17+version = "11.3.0"18+wheels = [{ url = "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/pillow-11.3.0-cp313-cp313-pyodide_2025_0_wasm32.whl", hashes = { sha256 = "52984112ca668d60291a7868011a04d66baf526d27e5eaef0a8a16e7ef22ab6c" } }]19+20+[[packages]]21+name = "webcolors"22+version = "25.10.0"23+wheels = [{ url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", upload-time = 2025-10-31T07:51:01Z, size = 14905, hashes = { sha256 = "032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d" } }]24+25+[[packages]]26+name = "workers-runtime-sdk"27+version = "1.6.14"28+wheels = [{ url = "https://files.pythonhosted.org/packages/fa/95/960a9a940825c52eae52c316e8f23f91f9d92c5f0e3bb5400097c43c2747/workers_runtime_sdk-1.6.14-py3-none-any.whl", upload-time = 2026-08-19T14:54:24Z, size = 34805, hashes = { sha256 = "c66a7278a93198b36fa3f1d61e5ac071e9a0d5e57a3f4fadcab9cc5107e587b4" } }]
A diagramzip-python-units/blockdiag-family/pyproject.toml +22 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/__init__.py +16 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/builder.py +380 -0
@@ -0,0 +1,380 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from __future__ import print_function17+18+from blockdiag.utils import XY, unquote19+from blockdiag.utils.compat import cmp_to_key20+21+from actdiag import parser22+from actdiag.elements import Diagram, DiagramEdge, DiagramNode, NodeGroup23+24+25+class DiagramTreeBuilder(object):26+ def build(self, tree):27+ self.diagram = Diagram()28+ diagram = self.instantiate(self.diagram, tree)29+30+ self.bind_edges(diagram)31+32+ if len(self.diagram.lanes) == 0:33+ self.diagram.lanes.append(NodeGroup.get(None))34+35+ for node in self.diagram.nodes:36+ if node.lane is None:37+ edges = DiagramEdge.find(None, node)38+ parents = [e.node1 for e in edges if e.node1.lane]39+ parents.sort(key=lambda x: x.order)40+41+ if parents:42+ node.lane = parents[0].lane43+ node.lane.nodes.append(node)44+ else:45+ node.lane = self.diagram.lanes[0]46+ node.lane.nodes.append(node)47+48+ for lane in diagram.lanes:49+ if len(lane.nodes) == 0:50+ diagram.lanes.remove(lane)51+52+ return diagram53+54+ def belong_to(self, node, lane):55+ if lane and node.lane and node.lane != lane:56+ print(node, node.lane, lane)57+ msg = "DiagramNode could not belong to two lanes"58+ raise RuntimeError(msg)59+60+ node.group = self.diagram61+ if lane:62+ node.lane = lane63+ lane.nodes.append(node)64+65+ if node not in self.diagram.nodes:66+ self.diagram.nodes.append(node)67+68+ def instantiate(self, group, tree, lane=None):69+ for stmt in tree.stmts:70+ if isinstance(stmt, parser.Node):71+ node = DiagramNode.get(stmt.id)72+ node.set_attributes(stmt.attrs)73+ self.belong_to(node, lane)74+75+ elif isinstance(stmt, parser.Edge):76+ from_nodes = [DiagramNode.get(n) for n in stmt.from_nodes]77+ to_nodes = [DiagramNode.get(n) for n in stmt.to_nodes]78+79+ for node in from_nodes + to_nodes:80+ self.belong_to(node, lane)81+82+ for node1 in from_nodes:83+ for node2 in to_nodes:84+ edge = DiagramEdge.get(node1, node2)85+ edge.set_dir(stmt.edge_type)86+ edge.set_attributes(stmt.attrs)87+88+ elif isinstance(stmt, parser.Lane):89+ _lane = NodeGroup.get(stmt.id)90+ if _lane not in self.diagram.lanes:91+ self.diagram.lanes.append(_lane)92+93+ self.instantiate(group, stmt, _lane)94+95+ elif isinstance(stmt, parser.Attr):96+ if lane:97+ lane.set_attribute(stmt)98+ else:99+ self.diagram.set_attribute(stmt)100+101+ elif isinstance(stmt, parser.Extension):102+ if stmt.type == 'class':103+ name = unquote(stmt.name)104+ Diagram.classes[name] = stmt105+ if stmt.type == 'plugin':106+ self.diagram.set_plugin(stmt.name, stmt.attrs)107+108+ elif isinstance(stmt, parser.Statements):109+ self.instantiate(group, stmt, lane)110+111+ group.update_order()112+ return group113+114+ def bind_edges(self, group):115+ for node in group.nodes:116+ if isinstance(node, DiagramNode):117+ group.edges += DiagramEdge.find(node)118+ else:119+ self.bind_edges(node)120+121+122+class DiagramLayoutManager:123+ def __init__(self, diagram):124+ self.diagram = diagram125+126+ self.circulars = []127+ self.heightRefs = []128+129+ def run(self):130+ self.edges = [e for e in DiagramEdge.find_all()]131+ self.do_layout()132+ self.diagram.fixiate()133+ self.fixiate_lanes()134+135+ def fixiate_lanes(self):136+ height = 0137+ for lane in self.diagram.lanes:138+ if self.coordinates[lane]:139+ for node in self.diagram.nodes:140+ if node.lane == lane:141+ node.xy = XY(node.xy.x, node.xy.y + height)142+143+ height += max(xy.y for xy in self.coordinates[lane]) + 1144+145+ nodes = [n for n in self.diagram.nodes if n.lane == lane]146+ x = min(n.xy.x for n in nodes)147+ y = min(n.xy.y for n in nodes)148+ lane.xy = XY(x, y)149+ lane.colwidth = max(n.xy.x + n.colwidth for n in nodes) - x150+ lane.colheight = max(n.xy.y + n.colheight for n in nodes) - y151+152+ def do_layout(self):153+ self.detect_circulars()154+155+ self.set_node_width()156+ self.adjust_node_order()157+158+ height = 0159+ self.initialize_markers()160+ for node in self.diagram.traverse_nodes():161+ if node.xy.x == 0:162+ lane = node.lane163+ if self.coordinates[lane]:164+ height = max(xy.y for xy in self.coordinates[lane]) + 1165+ else:166+ height = 0167+ self.set_node_height(node, height)168+169+ def get_related_nodes(self, node, parent=False, child=False):170+ uniq = {}171+ for edge in self.edges:172+ if edge.folded:173+ continue174+175+ if parent and edge.node2 == node:176+ uniq[edge.node1] = 1177+ elif child and edge.node1 == node:178+ uniq[edge.node2] = 1179+180+ related = []181+ for uniq_node in uniq.keys():182+ if uniq_node == node:183+ pass184+ else:185+ related.append(uniq_node)186+187+ related.sort(key=lambda x: x.order)188+ return related189+190+ def get_parent_nodes(self, node):191+ return self.get_related_nodes(node, parent=True)192+193+ def get_child_nodes(self, node):194+ return self.get_related_nodes(node, child=True)195+196+ def detect_circulars(self):197+ for node in self.diagram.nodes:198+ if not [x for x in self.circulars if node in x]:199+ self.detect_circulars_sub(node, [node])200+201+ # remove part of other circular202+ for c1 in self.circulars:203+ for c2 in self.circulars:204+ intersect = set(c1) & set(c2)205+206+ if c1 != c2 and set(c1) == intersect:207+ self.circulars.remove(c1)208+ break209+210+ def detect_circulars_sub(self, node, parents):211+ for child in self.get_child_nodes(node):212+ if child in parents:213+ i = parents.index(child)214+ self.circulars.append(parents[i:])215+ else:216+ self.detect_circulars_sub(child, parents + [child])217+218+ def is_circular_ref(self, node1, node2):219+ for circular in self.circulars:220+ if node1 in circular and node2 in circular:221+ parents = []222+ for node in circular:223+ for parent in self.get_parent_nodes(node):224+ if parent not in circular:225+ parents.append(parent)226+227+ parents.sort(key=lambda x: x.order)228+229+ for parent in parents:230+ children = self.get_child_nodes(parent)231+ if node1 in children and node2 in children:232+ if circular.index(node1) > circular.index(node2):233+ return True234+ elif node2 in children:235+ return True236+ elif node1 in children:237+ return False238+ else:239+ if circular.index(node1) > circular.index(node2):240+ return True241+242+ return False243+244+ def set_node_width(self, depth=0):245+ for node in self.diagram.traverse_nodes():246+ if node.xy.x != depth:247+ continue248+249+ for child in self.get_child_nodes(node):250+ if self.is_circular_ref(node, child):251+ pass252+ elif node == child:253+ pass254+ elif child.xy.x > node.xy.x + node.colwidth:255+ pass256+ else:257+ child.xy = XY(node.xy.x + node.colwidth, 0)258+259+ nodes_iter = self.diagram.traverse_nodes()260+ depther_node = [x for x in nodes_iter if x.xy.x > depth]261+ if len(depther_node) > 0:262+ self.set_node_width(depth + 1)263+264+ def adjust_node_order(self):265+ for node in self.diagram.traverse_nodes():266+ parents = self.get_parent_nodes(node)267+ if len(set(parents)) > 1:268+ for i in range(1, len(parents)):269+ idx1 = self.diagram.nodes.index(parents[i - 1])270+ idx2 = self.diagram.nodes.index(parents[i])271+ if idx1 < idx2:272+ self.diagram.nodes.remove(parents[i])273+ self.diagram.nodes.insert(idx1 + 1, parents[i])274+ else:275+ self.diagram.nodes.remove(parents[i - 1])276+ self.diagram.nodes.insert(idx2 + 1, parents[i - 1])277+278+ if isinstance(node, NodeGroup):279+ nodes = [n for n in node.nodes if n in self.diagram.nodes]280+ if nodes:281+ idx = min(self.diagram.nodes.index(n) for n in nodes)282+ if idx < self.diagram.nodes.index(node):283+ self.diagram.nodes.remove(node)284+ self.diagram.nodes.insert(idx + 1, node)285+286+ self.diagram.update_order()287+288+ def initialize_markers(self):289+ self.coordinates = {}290+ for lane in self.diagram.lanes:291+ self.coordinates[lane] = []292+293+ def mark_xy(self, node):294+ xy = node.xy295+ for w in range(node.colwidth):296+ for h in range(node.colheight):297+ self.coordinates[node.lane].append(XY(xy.x + w, xy.y + h))298+299+ def is_marked(self, lane, xy):300+ return xy in self.coordinates[lane]301+302+ def set_node_height(self, node, height=0):303+ xy = XY(node.xy.x, height)304+ if self.is_marked(node.lane, xy):305+ return False306+ node.xy = xy307+ self.mark_xy(node)308+309+ def cmp(x, y):310+ if x.xy.x < y.xy.y:311+ return -1312+ elif x.xy.x == y.xy.y:313+ return 0314+ else:315+ return 1316+317+ count = 0318+ children = self.get_child_nodes(node)319+ children.sort(key=cmp_to_key(cmp))320+ for child in children:321+ if child.id in self.heightRefs:322+ pass323+ elif node is not None and node.xy.x >= child.xy.x:324+ pass325+ else:326+ if node.lane == child.lane:327+ h = height328+ else:329+ h = 0330+331+ while True:332+ if self.set_node_height(child, h):333+ child.xy = XY(child.xy.x, h)334+ self.mark_xy(child)335+ self.heightRefs.append(child.id)336+337+ count += 1338+ break339+ elif node.lane != child.lane:340+ h += 1341+ else:342+ if count == 0:343+ return False344+345+ h += 1346+347+ if node.lane == child.lane:348+ height = h + 1349+350+ return True351+352+353+class ScreenNodeBuilder(object):354+ @classmethod355+ def build(cls, tree, separate=False):356+ DiagramNode.clear()357+ DiagramEdge.clear()358+ NodeGroup.clear()359+360+ diagram = DiagramTreeBuilder().build(tree)361+ DiagramLayoutManager(diagram).run()362+ diagram.fixiate(True)363+364+ if diagram.orientation == 'portrait':365+ cls.rotate_diagram(diagram)366+367+ return diagram368+369+ @classmethod370+ def rotate_diagram(cls, diagram):371+ for node in diagram.traverse_nodes():372+ node.xy = XY(node.xy.y, node.xy.x)373+ node.colwidth, node.colheight = (node.colheight, node.colwidth)374+375+ for lane in diagram.lanes:376+ lane.xy = XY(lane.xy.y, lane.xy.x)377+ lane.colwidth, lane.colheight = (lane.colheight, lane.colwidth)378+379+ size = (diagram.colheight, diagram.colwidth)380+ diagram.colwidth, diagram.colheight = size
A diagramzip-python-units/blockdiag-family/src/actdiag/command.py +31 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/drawer.py +55 -0
@@ -0,0 +1,55 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import blockdiag.drawer17+18+from actdiag.metrics import DiagramMetrics19+20+21+class DiagramDraw(blockdiag.drawer.DiagramDraw):22+ def create_metrics(self, *args, **kwargs):23+ return DiagramMetrics(*args, **kwargs)24+25+ def _draw_elements(self, **kwargs):26+ m = self.metrics27+28+ # render label of lanes29+ for i, lane in enumerate(self.diagram.lanes):30+ if lane.label:31+ label = lane.label32+ elif isinstance(lane.id, str):33+ label = lane.id34+ else:35+ label = 'Lane %d' % (i + 1)36+37+ if lane.href and self.format == 'SVG':38+ drawer = self.drawer.anchor(lane.href)39+ else:40+ drawer = self.drawer41+42+ headerbox = m.lane_headerbox(lane)43+ drawer.rectangle(headerbox, fill=lane.color, outline=lane.color)44+45+ textbox = m.lane_textbox(lane)46+ drawer.textarea(textbox, label, fill=self.fill,47+ font=self.metrics.font_for(lane))48+49+ # render frame of activity lanes50+ frame = m.frame(self.diagram.lanes)51+ self.drawer.rectangle(frame.outline, outline='gray')52+ for xy in frame.separators:53+ self.drawer.line(xy, fill='gray')54+55+ super(DiagramDraw, self)._draw_elements(**kwargs)
A diagramzip-python-units/blockdiag-family/src/actdiag/elements.py +42 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/metrics.py +97 -0
@@ -0,0 +1,97 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from __future__ import division17+18+from collections import namedtuple19+20+import blockdiag.metrics21+from blockdiag.utils import XY, Box22+23+from actdiag import elements24+25+26+class DiagramMetrics(blockdiag.metrics.DiagramMetrics):27+ def __init__(self, diagram, **kwargs):28+ super(DiagramMetrics, self).__init__(diagram, **kwargs)29+30+ if diagram.page_padding is None:31+ if diagram.orientation == 'landscape':32+ padding = self.node_width + self.span_width33+ self.page_padding = [0, 0, 0, padding]34+ else:35+ padding = self.node_height + self.span_height36+ self.page_padding = [padding, 0, 0, 0]37+38+ def pagesize(self, width=None, height=None):39+ if width:40+ self.colwidth = width41+ else:42+ width = self.colwidth43+44+ if height:45+ self.colheight = height46+ else:47+ height = self.colheight48+49+ return super(DiagramMetrics, self).pagesize(width, height)50+51+ def frame(self, lanes):52+ dummy = elements.DiagramNode(None)53+ dummy.xy = XY(0, 0)54+ dummy.colwidth = self.colwidth55+ dummy.colheight = self.colheight56+ cell = self.cell(dummy, use_padding=False)57+58+ headerbox = Box(cell.topleft.x - self.span_width // 2,59+ (cell.topleft.y - self.node_height -60+ self.span_height - 2),61+ cell.topright.x + self.span_width // 2,62+ cell.topright.y - self.span_height // 2)63+64+ outline = Box(headerbox[0], headerbox[1], headerbox[2],65+ cell.bottom.y + self.span_height // 2)66+67+ separators = [(XY(headerbox[0], headerbox[3]),68+ XY(headerbox[2], headerbox[3]))]69+70+ for lane in lanes[:-1]:71+ x = lane.xy.x + lane.colwidth + 172+73+ m = self.cell(lane, use_padding=False)74+ span_width = self.spreadsheet.span_width[x] // 275+ x1 = m.right.x + span_width76+77+ xy = (XY(x1, outline[1]), XY(x1, outline[3]))78+ separators.append(xy)79+80+ Frame = namedtuple('Frame', 'headerbox outline separators')81+ return Frame(headerbox, outline, separators)82+83+ def lane_textbox(self, lane):84+ headerbox = self.frame([]).headerbox85+ m = self.cell(lane, use_padding=False)86+ x1 = m.left.x87+ x2 = m.right.x88+89+ return Box(x1, headerbox[1], x2, headerbox[3])90+91+ def lane_headerbox(self, lane):92+ headerbox = self.frame([]).headerbox93+ m = self.cell(lane)94+ x1 = m.left.x - self.spreadsheet.span_width[lane.xy.x] // 295+ x2 = m.right.x + self.spreadsheet.span_width[lane.xy.x + 1] // 296+97+ return Box(x1, headerbox[1], x2, headerbox[3])
A diagramzip-python-units/blockdiag-family/src/actdiag/parser.py +254 -0
@@ -0,0 +1,254 @@
1+# -*- coding: utf-8 -*-2+3+# Copyright (c) 2008/2009 Andrey Vlasovskikh4+#5+# Permission is hereby granted, free of charge, to any person obtaining6+# a copy of this software and associated documentation files (the7+# "Software"), to deal in the Software without restriction, including8+# without limitation the rights to use, copy, modify, merge, publish,9+# distribute, sublicense, and/or sell copies of the Software, and to10+# permit persons to whom the Software is furnished to do so, subject to11+# the following conditions:12+#13+# The above copyright notice and this permission notice shall be included14+# in all copies or substantial portions of the Software.15+#16+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,17+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF18+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.19+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY20+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,21+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE22+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.23+24+r'''A DOT language parser using funcparserlib.25+26+The parser is based on [the DOT grammar][1]. It is pretty complete with a few27+not supported things:28+29+* Ports and compass points30+* XML identifiers31+32+At the moment, the parser builds only a parse tree, not an abstract syntax tree33+(AST) or an API for dealing with DOT.34+35+ [1]: http://www.graphviz.org/doc/info/lang.html36+'''37+38+import io39+from collections import namedtuple40+from re import DOTALL, MULTILINE41+42+from blockdiag.parser import create_mapper, oneplus_to_list43+from funcparserlib.lexer import LexerError, Token, make_tokenizer44+from funcparserlib.parser import a, finished, many, maybe, skip, some45+46+Diagram = namedtuple('Diagram', 'id stmts')47+Lane = namedtuple('Lane', 'id stmts')48+Node = namedtuple('Node', 'id attrs')49+Attr = namedtuple('Attr', 'name value')50+Edge = namedtuple('Edge', 'from_nodes edge_type to_nodes attrs')51+Extension = namedtuple('Extension', 'type name attrs')52+Statements = namedtuple('Statements', 'stmts')53+54+55+class ParseException(Exception):56+ pass57+58+59+def tokenize(string):60+ """str -> Sequence(Token)"""61+ # flake8: NOQA62+ specs = [ # NOQA63+ ('Comment', (r'/\*(.|[\r\n])*?\*/', MULTILINE)), # NOQA64+ ('Comment', (r'(//|#).*',)), # NOQA65+ ('NL', (r'[\r\n]+',)), # NOQA66+ ('Space', (r'[ \t\r\n]+',)), # NOQA67+ ('Name', ('[A-Za-z_0-9\u0080-\uffff]' + # NOQA68+ '[A-Za-z_\\-.0-9\u0080-\uffff]*',)), # NOQA69+ ('Op', (r'[{};,=\[\]]|(<->)|(<-)|(--)|(->)',)), # NOQA70+ ('Number', (r'-?(\.[0-9]+)|([0-9]+(\.[0-9]*)?)',)), # NOQA71+ ('String', (r'(?P<quote>(""")|(\'\'\')|"|\').*?(?<!\\)(?P=quote)', DOTALL)), # NOQA72+ ]73+ useless = ['Comment', 'NL', 'Space']74+ t = make_tokenizer(specs)75+ return [x for x in t(string) if x.type not in useless]76+77+78+def parse(seq):79+ """Sequence(Token) -> object"""80+ tokval = lambda x: x.value81+ op = lambda s: a(Token('Op', s)) >> tokval82+ op_ = lambda s: skip(op(s))83+ _id = some(lambda t: t.type in ['Name', 'Number', 'String']) >> tokval84+ keyword = lambda s: a(Token('Name', s)) >> tokval85+86+ def make_node_list(node_list, attrs):87+ return Statements([Node(node, attrs) for node in node_list])88+89+ def make_edge(first, edge_type, second, followers, attrs):90+ edges = [Edge(first, edge_type, second, attrs)]91+92+ from_node = second93+ for edge_type, to_node in followers:94+ edges.append(Edge(from_node, edge_type, to_node, attrs))95+ from_node = to_node96+97+ return Statements(edges)98+99+ #100+ # parts of syntax101+ #102+ node_list = (103+ _id +104+ many(op_(',') + _id)105+ >> create_mapper(oneplus_to_list)106+ )107+ option_stmt = (108+ _id +109+ maybe(op_('=') + _id)110+ >> create_mapper(Attr)111+ )112+ option_list = (113+ maybe(op_('[') + option_stmt + many(op_(',') + option_stmt) + op_(']'))114+ >> create_mapper(oneplus_to_list, default_value=[])115+ )116+117+ # node (node list) statement::118+ # A;119+ # B [attr = value, attr = value];120+ # C, D [attr = value, attr = value];121+ #122+ node_stmt = (123+ node_list + option_list124+ >> create_mapper(make_node_list)125+ )126+127+ # edge statement::128+ # A -> B;129+ # A <- B;130+ #131+ edge_relation = (132+ op('->') | op('--') | op('<-') | op('<->')133+ )134+ edge_stmt = (135+ node_list +136+ edge_relation +137+ node_list +138+ many(edge_relation + node_list) +139+ option_list140+ >> create_mapper(make_edge)141+ )142+143+ # attributes statement::144+ # default_shape = box;145+ # default_fontsize = 16;146+ #147+ attribute_stmt = (148+ _id + op_('=') + _id149+ >> create_mapper(Attr)150+ )151+152+ # extension statement (class, plugin)::153+ # class red [color = red];154+ # plugin attributes [name = Name];155+ #156+ extension_stmt = (157+ (keyword('class') | keyword('plugin')) +158+ _id +159+ option_list160+ >> create_mapper(Extension)161+ )162+163+ # lane statement::164+ # lane A [color = red];165+ # lane {166+ # A;167+ # }168+ #169+ lane_declare_stmt = (170+ skip(keyword('lane')) +171+ _id +172+ option_list173+ >> create_mapper(Lane)174+ )175+ lane_inline_stmt = (176+ edge_stmt |177+ attribute_stmt |178+ node_stmt179+ )180+ lane_inline_stmt_list = (181+ many(lane_inline_stmt + skip(maybe(op(';'))))182+ )183+ lane_stmt = (184+ skip(keyword('lane')) +185+ maybe(_id) +186+ op_('{') +187+ lane_inline_stmt_list +188+ op_('}')189+ >> create_mapper(Lane)190+ )191+192+ #193+ # diagram statement::194+ # actdiag {195+ # A;196+ # }197+ #198+ diagram_id = (199+ (keyword('diagram') | keyword('actdiag')) +200+ maybe(_id)201+ >> list202+ )203+ diagram_inline_stmt = (204+ extension_stmt |205+ edge_stmt |206+ lane_stmt |207+ lane_declare_stmt |208+ attribute_stmt |209+ node_stmt210+ )211+ diagram_inline_stmt_list = (212+ many(diagram_inline_stmt + skip(maybe(op(';'))))213+ )214+ diagram = (215+ maybe(diagram_id) +216+ op_('{') +217+ diagram_inline_stmt_list +218+ op_('}')219+ >> create_mapper(Diagram)220+ )221+ dotfile = diagram + skip(finished)222+223+ return dotfile.parse(seq)224+225+226+def sort_tree(tree):227+ def weight(node):228+ if isinstance(node, (Attr, Extension)):229+ return 1230+ else:231+ return 2232+233+ if hasattr(tree, 'stmts'):234+ tree.stmts.sort(key=lambda x: weight(x))235+ for stmt in tree.stmts:236+ sort_tree(stmt)237+238+ return tree239+240+241+def parse_string(string):242+ try:243+ tree = parse(tokenize(string))244+ return sort_tree(tree)245+ except LexerError as e:246+ message = "Got unexpected token at line %d column %d" % e.place247+ raise ParseException(message)248+ except Exception as e:249+ raise ParseException(str(e))250+251+252+def parse_file(path):253+ input = io.open(path, 'r', encoding='utf-8-sig').read()254+ return parse_string(input)
A diagramzip-python-units/blockdiag-family/src/actdiag/plugins/__init__.py +14 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/plugins/autolane.py +36 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/utils/__init__.py +14 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/utils/rst/__init__.py +14 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/utils/rst/directives.py +44 -0
A diagramzip-python-units/blockdiag-family/src/actdiag/utils/rst/nodes.py +25 -0
A diagramzip-python-units/blockdiag-family/src/assets/DejaVuSerif.ttf +0 -0

Binary file.

A diagramzip-python-units/blockdiag-family/src/assets/__init__.py +1 -0
A diagramzip-python-units/blockdiag-family/src/blockdiag/__init__.py +16 -0
A diagramzip-python-units/blockdiag-family/src/blockdiag/__main__.py +7 -0
A diagramzip-python-units/blockdiag-family/src/blockdiag/builder.py +745 -0
@@ -0,0 +1,745 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from blockdiag import parser17+from blockdiag.elements import Diagram, DiagramEdge, DiagramNode, NodeGroup18+from blockdiag.plugins import fire_node_event19+from blockdiag.utils import XY, unquote20+from blockdiag.utils.compat import cmp_to_key21+22+23+class DiagramTreeBuilder:24+ def build(self, tree, config):25+ self.config = config26+ self.diagram = Diagram()27+ self.instantiate(self.diagram, tree)28+ for subgroup in self.diagram.traverse_groups():29+ if len(subgroup.nodes) == 0:30+ subgroup.group.nodes.remove(subgroup)31+32+ self.bind_edges(self.diagram)33+ self.fire_node_event('build_finished')34+ return self.diagram35+36+ def fire_node_event(self, event_type):37+ for node in self.diagram.nodes:38+ if node.drawable:39+ fire_node_event(node, event_type)40+41+ def is_related_group(self, group1, group2):42+ if group1.is_parent(group2) or group2.is_parent(group1):43+ return True44+ else:45+ return False46+47+ def belong_to(self, node, group):48+ if node.group and node.group.level > group.level:49+ override = False50+ else:51+ override = True52+53+ if node.group and node.group != group and override:54+ if not self.is_related_group(node.group, group):55+ msg = "could not belong to two groups: %s" % node.id56+ raise RuntimeError(msg)57+58+ old_group = node.group59+60+ parent = group.parent(old_group.level + 1)61+ if parent:62+ if parent in old_group.nodes:63+ old_group.nodes.remove(parent)64+65+ index = old_group.nodes.index(node)66+ old_group.nodes.insert(index + 1, parent)67+68+ old_group.nodes.remove(node)69+ node.group = None70+71+ if node.group is None:72+ node.group = group73+74+ if node not in group.nodes:75+ group.nodes.append(node)76+77+ def instantiate(self, group, tree):78+ for stmt in tree.stmts:79+ # Translate Node having group attribute to Group80+ if isinstance(stmt, parser.Node):81+ group_attr = [a for a in stmt.attrs if a.name == 'group']82+ if group_attr:83+ group_id = group_attr[-1]84+ stmt.attrs.remove(group_id)85+86+ if group_id.value != group.id:87+ stmt = parser.Group(group_id.value, [stmt])88+89+ # Instantiate statements90+ if isinstance(stmt, parser.Node):91+ node = DiagramNode.get(stmt.id)92+ node.set_attributes(stmt.attrs)93+ self.belong_to(node, group)94+95+ elif isinstance(stmt, parser.Edge):96+ from_nodes = [DiagramNode.get(n) for n in stmt.from_nodes]97+ to_nodes = [DiagramNode.get(n) for n in stmt.to_nodes]98+99+ for node in from_nodes + to_nodes:100+ self.belong_to(node, group)101+102+ for node1 in from_nodes:103+ for node2 in to_nodes:104+ edge = DiagramEdge.get(node1, node2)105+ edge.set_dir(stmt.edge_type)106+ edge.set_attributes(stmt.attrs)107+108+ elif isinstance(stmt, parser.Group):109+ subgroup = NodeGroup.get(stmt.id)110+ subgroup.level = group.level + 1111+ self.belong_to(subgroup, group)112+ self.instantiate(subgroup, stmt)113+114+ elif isinstance(stmt, parser.Attr):115+ group.set_attribute(stmt)116+117+ elif isinstance(stmt, parser.Extension):118+ if stmt.type == 'class':119+ name = unquote(stmt.name)120+ Diagram.classes[name] = stmt121+ elif stmt.type == 'plugin':122+ self.diagram.set_plugin(stmt.name, stmt.attrs,123+ config=self.config)124+125+ elif isinstance(stmt, parser.Statements):126+ self.instantiate(group, stmt)127+128+ group.update_order()129+ return group130+131+ def bind_edges(self, group):132+ for node in group.nodes:133+ if isinstance(node, DiagramNode):134+ group.edges += DiagramEdge.find(node)135+ else:136+ self.bind_edges(node)137+138+139+class DiagramLayoutManager:140+ def __init__(self, diagram):141+ self.diagram = diagram142+143+ self.circulars = []144+ self.heightRefs = []145+ self.coordinates = []146+147+ def run(self):148+ if isinstance(self.diagram, Diagram):149+ for group in self.diagram.traverse_groups():150+ self.__class__(group).run()151+152+ self.edges = DiagramEdge.find_by_level(self.diagram.level)153+ self.do_layout()154+ self.diagram.fixiate()155+156+ if self.diagram.orientation == 'portrait':157+ self.rotate_diagram()158+159+ def rotate_diagram(self):160+ for node in self.diagram.traverse_nodes():161+ node.xy = XY(node.xy.y, node.xy.x)162+ node.colwidth, node.colheight = (node.colheight, node.colwidth)163+164+ if isinstance(node, NodeGroup):165+ if node.orientation == 'portrait':166+ node.orientation = 'landscape'167+ else:168+ node.orientation = 'portrait'169+170+ xy = (self.diagram.colheight, self.diagram.colwidth)171+ self.diagram.colwidth, self.diagram.colheight = xy172+173+ def do_layout(self):174+ self.detect_circulars()175+176+ self.set_node_xpos()177+ self.adjust_node_order()178+179+ height = 0180+ for node in self.diagram.nodes:181+ if node.xy.x == 0:182+ self.set_node_ypos(node, height)183+ height = max(xy.y for xy in self.coordinates) + 1184+185+ def get_related_nodes(self, node, parent=False, child=False):186+ uniq = {}187+ for edge in self.edges:188+ if edge.folded:189+ continue190+191+ if parent and edge.node2 == node:192+ uniq[edge.node1] = 1193+ elif child and edge.node1 == node:194+ uniq[edge.node2] = 1195+196+ related = []197+ for uniq_node in uniq.keys():198+ if uniq_node == node:199+ pass200+ elif uniq_node.group != node.group:201+ pass202+ else:203+ related.append(uniq_node)204+205+ related.sort(key=lambda x: x.order)206+ return related207+208+ def get_parent_nodes(self, node):209+ return self.get_related_nodes(node, parent=True)210+211+ def get_child_nodes(self, node):212+ return self.get_related_nodes(node, child=True)213+214+ def detect_circulars(self):215+ for node in self.diagram.nodes:216+ if not [x for x in self.circulars if node in x]:217+ self.detect_circulars_sub(node, [node])218+219+ # remove part of other circular220+ for c1 in self.circulars[:]:221+ for c2 in self.circulars:222+ intersect = set(c1) & set(c2)223+224+ if c1 != c2 and set(c1) == intersect:225+ if c1 in self.circulars:226+ self.circulars.remove(c1)227+ break228+229+ if c1 != c2 and intersect:230+ if c1 in self.circulars:231+ self.circulars.remove(c1)232+ self.circulars.remove(c2)233+ self.circulars.append(c1 + c2)234+ break235+236+ def detect_circulars_sub(self, node, parents):237+ for child in self.get_child_nodes(node):238+ if child in parents:239+ i = parents.index(child)240+ if parents[i:] not in self.circulars:241+ self.circulars.append(parents[i:])242+ else:243+ self.detect_circulars_sub(child, parents + [child])244+245+ def is_circular_ref(self, node1, node2):246+ for circular in self.circulars:247+ if node1 in circular and node2 in circular:248+ parents = []249+ for node in circular:250+ for parent in self.get_parent_nodes(node):251+ if parent not in circular:252+ parents.append(parent)253+254+ for parent in sorted(parents, key=lambda x: x.order):255+ children = self.get_child_nodes(parent)256+ if node1 in children and node2 in children:257+ if circular.index(node1) > circular.index(node2):258+ return True259+ elif node2 in children:260+ return True261+ elif node1 in children:262+ return False263+ else:264+ if circular.index(node1) > circular.index(node2):265+ return True266+267+ return False268+269+ def set_node_xpos(self, depth=0):270+ for node in self.diagram.nodes:271+ if node.xy.x != depth:272+ continue273+274+ for child in self.get_child_nodes(node):275+ if self.is_circular_ref(node, child):276+ pass277+ elif node == child:278+ pass279+ elif child.xy.x > node.xy.x + node.colwidth:280+ pass281+ else:282+ child.xy = XY(node.xy.x + node.colwidth, 0)283+284+ depther_node = [x for x in self.diagram.nodes if x.xy.x > depth]285+ if len(depther_node) > 0:286+ self.set_node_xpos(depth + 1)287+288+ def adjust_node_order(self):289+ for node in list(self.diagram.nodes):290+ parents = self.get_parent_nodes(node)291+ if len(set(parents)) > 1:292+ for i in range(1, len(parents)):293+ node1 = parents[i - 1]294+ node2 = parents[i]295+296+ if node1.xy.x == node2.xy.x:297+ idx1 = self.diagram.nodes.index(node1)298+ idx2 = self.diagram.nodes.index(node2)299+300+ if idx1 < idx2:301+ self.diagram.nodes.remove(node2)302+ self.diagram.nodes.insert(idx1 + 1, node2)303+ else:304+ self.diagram.nodes.remove(node1)305+ self.diagram.nodes.insert(idx2 + 1, node1)306+307+ children = self.get_child_nodes(node)308+ if len(set(children)) > 1:309+ for i in range(1, len(children)):310+ node1 = children[i - 1]311+ node2 = children[i]312+313+ idx1 = self.diagram.nodes.index(node1)314+ idx2 = self.diagram.nodes.index(node2)315+316+ if node1.xy.x == node2.xy.x:317+ if idx1 < idx2:318+ self.diagram.nodes.remove(node2)319+ self.diagram.nodes.insert(idx1 + 1, node2)320+ else:321+ self.diagram.nodes.remove(node1)322+ self.diagram.nodes.insert(idx2 + 1, node1)323+ elif self.is_circular_ref(node1, node2):324+ pass325+ else:326+ if node1.xy.x < node2.xy.x:327+ self.diagram.nodes.remove(node2)328+ self.diagram.nodes.insert(idx1 + 1, node2)329+ else:330+ self.diagram.nodes.remove(node1)331+ self.diagram.nodes.insert(idx2 + 1, node1)332+333+ if isinstance(node, NodeGroup):334+ children = self.get_child_nodes(node)335+ if len(set(children)) > 1:336+ while True:337+ exchange = 0338+339+ for i in range(1, len(children)):340+ node1 = children[i - 1]341+ node2 = children[i]342+343+ idx1 = self.diagram.nodes.index(node1)344+ idx2 = self.diagram.nodes.index(node2)345+ ret = self.compare_child_node_order(node,346+ node1, node2)347+348+ if ret > 0 and idx1 < idx2:349+ self.diagram.nodes.remove(node1)350+ self.diagram.nodes.insert(idx2 + 1, node1)351+ exchange += 1352+353+ if exchange == 0:354+ break355+356+ self.diagram.update_order()357+358+ def compare_child_node_order(self, parent, node1, node2):359+ def compare(x, y):360+ x = x.duplicate()361+ y = y.duplicate()362+ while x.node1 == y.node1 and x.node1.group is not None:363+ x.node1 = x.node1.group364+ y.node1 = y.node1.group365+366+ # cmp x.node1.order and y.node1.order367+ if x.node1.order < y.node1.order:368+ return -1369+ elif x.node1.order == y.node1.order:370+ return 0371+ else:372+ return 1373+374+ edges = (DiagramEdge.find(parent, node1) +375+ DiagramEdge.find(parent, node2))376+ edges.sort(key=cmp_to_key(compare))377+ if len(edges) == 0:378+ return 0379+ elif edges[0].node2 == node2:380+ return 1381+ else:382+ return -1383+384+ def mark_xy(self, xy, width, height):385+ for w in range(width):386+ for h in range(height):387+ self.coordinates.append(XY(xy.x + w, xy.y + h))388+389+ def set_node_ypos(self, node, height=0):390+ for x in range(node.colwidth):391+ for y in range(node.colheight):392+ xy = XY(node.xy.x + x, height + y)393+ if xy in self.coordinates:394+ return False395+ node.xy = XY(node.xy.x, height)396+ self.mark_xy(node.xy, node.colwidth, node.colheight)397+398+ def cmp(x, y):399+ if x.xy.x < y.xy.y:400+ return -1401+ elif x.xy.x == y.xy.y:402+ return 0403+ else:404+ return 1405+406+ count = 0407+ children = self.get_child_nodes(node)408+ children.sort(key=cmp_to_key(cmp))409+410+ grandchild = 0411+ for child in children:412+ if self.get_child_nodes(child):413+ grandchild += 1414+415+ prev_child = None416+ for child in children:417+ if child.id in self.heightRefs:418+ pass419+ elif node.xy.x >= child.xy.x:420+ pass421+ else:422+ if isinstance(node, NodeGroup):423+ parent_height = self.get_parent_node_ypos(node, child)424+ if parent_height and parent_height > height:425+ height = parent_height426+427+ if (prev_child and grandchild > 1 and428+ (not self.is_rhombus(prev_child, child))):429+ coord = [p.y for p in self.coordinates if p.x > child.xy.x]430+ if coord and max(coord) >= node.xy.y:431+ height = max(coord) + 1432+433+ while True:434+ if self.set_node_ypos(child, height):435+ child.xy = XY(child.xy.x, height)436+ self.mark_xy(child.xy, child.colwidth, child.colheight)437+ self.heightRefs.append(child.id)438+439+ count += 1440+ break441+ else:442+ if count == 0:443+ return False444+445+ height += 1446+447+ height += 1448+ prev_child = child449+450+ return True451+452+ def is_rhombus(self, node1, node2):453+ ret = False454+ while True:455+ if node1 == node2:456+ ret = True457+ break458+459+ child1 = self.get_child_nodes(node1)460+ child2 = self.get_child_nodes(node2)461+462+ if len(child1) != 1 or len(child2) != 1:463+ break464+ elif node1.xy.x > child1[0].xy.x or node2.xy.x > child2[0].xy.x:465+ break466+ else:467+ node1 = child1[0]468+ node2 = child2[0]469+470+ return ret471+472+ def get_parent_node_ypos(self, parent, child):473+ heights = []474+ for e in DiagramEdge.find(parent, child):475+ y = parent.xy.y476+477+ node = e.node1478+ while node != parent:479+ y += node.xy.y480+ node = node.group481+482+ heights.append(y)483+484+ if heights:485+ return min(heights)486+ else:487+ return None488+489+490+class EdgeLayoutManager(object):491+ def __init__(self, diagram):492+ self.diagram = diagram493+494+ @property495+ def groups(self):496+ if self.diagram.separated:497+ seq = self.diagram.nodes498+ else:499+ seq = self.diagram.traverse_groups(preorder=True)500+501+ for group in seq:502+ if not group.drawable:503+ yield group504+505+ @property506+ def nodes(self):507+ if self.diagram.separated:508+ seq = self.diagram.nodes509+ else:510+ seq = self.diagram.traverse_nodes()511+512+ for node in seq:513+ if node.drawable:514+ yield node515+516+ @property517+ def edges(self):518+ for edge in (e for e in self.diagram.edges if e.style != 'none'):519+ yield edge520+521+ for group in self.groups:522+ for edge in (e for e in group.edges if e.style != 'none'):523+ yield edge524+525+ def run(self):526+ for edge in self.edges:527+ _dir = edge.direction528+529+ if edge.node1.group.orientation == 'landscape':530+ if _dir == 'right':531+ r = range(edge.node1.xy.x + 1, edge.node2.xy.x)532+ for x in r:533+ xy = (x, edge.node1.xy.y)534+ nodes = [x for x in self.nodes if x.xy == xy]535+ if len(nodes) > 0:536+ edge.skipped = 1537+ elif _dir == 'right-up':538+ r = range(edge.node1.xy.x + 1, edge.node2.xy.x)539+ for x in r:540+ xy = (x, edge.node1.xy.y)541+ nodes = [x for x in self.nodes if x.xy == xy]542+ if len(nodes) > 0:543+ edge.skipped = 1544+ elif _dir == 'right-down':545+ if self.diagram.edge_layout == 'flowchart':546+ r = range(edge.node1.xy.y, edge.node2.xy.y)547+ for y in r:548+ xy = (edge.node1.xy.x, y + 1)549+ nodes = [x for x in self.nodes if x.xy == xy]550+ if len(nodes) > 0:551+ edge.skipped = 1552+553+ r = range(edge.node1.xy.x + 1, edge.node2.xy.x)554+ for x in r:555+ xy = (x, edge.node2.xy.y)556+ nodes = [x for x in self.nodes if x.xy == xy]557+ if len(nodes) > 0:558+ edge.skipped = 1559+ elif _dir in ('left-down', 'down'):560+ r = range(edge.node1.xy.y + 1, edge.node2.xy.y)561+ for y in r:562+ xy = (edge.node1.xy.x, y)563+ nodes = [x for x in self.nodes if x.xy == xy]564+ if len(nodes) > 0:565+ edge.skipped = 1566+ elif _dir == 'up':567+ r = range(edge.node2.xy.y + 1, edge.node1.xy.y)568+ for y in r:569+ xy = (edge.node1.xy.x, y)570+ nodes = [x for x in self.nodes if x.xy == xy]571+ if len(nodes) > 0:572+ edge.skipped = 1573+ else:574+ if _dir == 'right':575+ r = range(edge.node1.xy.x + 1, edge.node2.xy.x)576+ for x in r:577+ xy = (x, edge.node1.xy.y)578+ nodes = [x for x in self.nodes if x.xy == xy]579+ if len(nodes) > 0:580+ edge.skipped = 1581+ elif _dir in ('left-down', 'down'):582+ r = range(edge.node1.xy.y + 1, edge.node2.xy.y)583+ for y in r:584+ xy = (edge.node1.xy.x, y)585+ nodes = [x for x in self.nodes if x.xy == xy]586+ if len(nodes) > 0:587+ edge.skipped = 1588+ elif _dir == 'right-down':589+ if self.diagram.edge_layout == 'flowchart':590+ r = range(edge.node1.xy.x, edge.node2.xy.x)591+ for x in r:592+ xy = (x + 1, edge.node1.xy.y)593+ nodes = [x for x in self.nodes if x.xy == xy]594+ if len(nodes) > 0:595+ edge.skipped = 1596+597+ r = range(edge.node1.xy.y + 1, edge.node2.xy.y)598+ for y in r:599+ xy = (edge.node2.xy.x, y)600+ nodes = [x for x in self.nodes if x.xy == xy]601+ if len(nodes) > 0:602+ edge.skipped = 1603+604+605+class ScreenNodeBuilder:606+ @classmethod607+ def build(cls, tree, config=None, layout=True):608+ DiagramNode.clear()609+ DiagramEdge.clear()610+ NodeGroup.clear()611+ Diagram.clear()612+613+ return cls(tree, config, layout).run()614+615+ def __init__(self, tree, config, layout):616+ self.diagram = DiagramTreeBuilder().build(tree, config)617+ self.config = config618+ self.layout = layout619+620+ def run(self):621+ if self.layout:622+ DiagramLayoutManager(self.diagram).run()623+ self.diagram.fixiate(True)624+625+ EdgeLayoutManager(self.diagram).run()626+627+ return self.diagram628+629+630+class SeparateDiagramBuilder(ScreenNodeBuilder):631+ @property632+ def _groups(self):633+ # Store nodes and edges of subgroups634+ nodes = {self.diagram: self.diagram.nodes}635+ edges = {self.diagram: self.diagram.edges}636+ levels = {self.diagram: self.diagram.level}637+ for group in self.diagram.traverse_groups():638+ nodes[group] = group.nodes639+ edges[group] = group.edges640+ levels[group] = group.level641+642+ groups = {}643+ orders = {}644+ for node in self.diagram.traverse_nodes():645+ groups[node] = node.group646+ orders[node] = node.order647+648+ for group in self.diagram.traverse_groups():649+ yield group650+651+ # Restore nodes, groups and edges652+ for g in nodes:653+ g.nodes = nodes[g]654+ g.edges = edges[g]655+ g.level = levels[g]656+657+ for n in groups:658+ n.group = groups[n]659+ n.order = orders[n]660+ n.xy = XY(0, 0)661+ n.colwidth = 1662+ n.colheight = 1663+ n.separated = False664+665+ for edge in DiagramEdge.find_all():666+ edge.skipped = False667+ edge.crosspoints = []668+669+ yield self.diagram670+671+ def _filter_edges(self, edges, parent, level):672+ filtered = {}673+ for e in edges:674+ if e.node1.group.is_parent(parent):675+ if e.node1.group.level > level:676+ e = e.duplicate()677+ if isinstance(e.node1, NodeGroup):678+ e.node1 = e.node1.parent(level + 1)679+ else:680+ e.node1 = e.node1.group.parent(level + 1)681+ else:682+ continue683+684+ if e.node2.group.is_parent(parent):685+ if e.node2.group.level > level:686+ e = e.duplicate()687+ if isinstance(e.node2, NodeGroup):688+ e.node2 = e.node2.parent(level + 1)689+ else:690+ e.node2 = e.node2.group.parent(level + 1)691+ else:692+ continue693+694+ filtered[(e.node1, e.node2)] = e695+696+ return filtered.values()697+698+ def run(self):699+ for i, group in enumerate(self._groups):700+ base = self.diagram.duplicate()701+ base.level = group.level - 1702+703+ # bind edges on base diagram (outer the group)704+ edges = (DiagramEdge.find(None, group) +705+ DiagramEdge.find(group, None))706+ base.edges = self._filter_edges(edges, self.diagram, group.level)707+708+ # bind edges on target group (inner the group)709+ subgroups = group.traverse_groups()710+ edges = sum([g.edges for g in subgroups], group.edges)711+ group.edges = []712+ for e in self._filter_edges(edges, group, group.level):713+ if isinstance(e.node1, NodeGroup) and e.node1 == e.node2:714+ pass715+ else:716+ group.edges.append(e)717+718+ # clear subgroups in the group719+ for g in group.nodes:720+ if isinstance(g, NodeGroup):721+ g.nodes = []722+ g.edges = []723+ g.separated = True724+725+ # pick up nodes to base diagram726+ nodes1 = [e.node1 for e in DiagramEdge.find(None, group)]727+ nodes1.sort(key=lambda x: x.order)728+ nodes2 = [e.node2 for e in DiagramEdge.find(group, None)]729+ nodes2.sort(key=lambda x: x.order)730+731+ nodes = nodes1 + [group] + nodes2732+ for i, n in enumerate(nodes):733+ n.order = i734+ if n not in base.nodes:735+ base.nodes.append(n)736+ n.group = base737+738+ if isinstance(group, Diagram):739+ base = group740+741+ DiagramLayoutManager(base).run()742+ base.fixiate(True)743+ EdgeLayoutManager(base).run()744+745+ yield base
A diagramzip-python-units/blockdiag-family/src/blockdiag/command.py +68 -0
@@ -0,0 +1,68 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import re17+import sys18+19+import blockdiag20+import blockdiag.builder21+import blockdiag.drawer22+import blockdiag.parser23+from blockdiag.utils.bootstrap import Application, Options24+25+26+class BlockdiagOptions(Options):27+ def build_parser(self):28+ super(BlockdiagOptions, self).build_parser()29+ self.parser.add_option(30+ '-s', '--separate', action='store_true',31+ help='Separate diagram images for each group (SVG only)'32+ )33+34+35+class BlockdiagApp(Application):36+ module = blockdiag37+38+ def parse_options(self, args):39+ self.options = BlockdiagOptions(self.module).parse(args)40+ if self.options.module:41+ self.module = self.options.module42+43+ def build_diagram(self, tree):44+ if not self.options.separate:45+ return super(BlockdiagApp, self).build_diagram(tree)46+ else:47+ DiagramBuilder = self.module.builder.SeparateDiagramBuilder48+ DiagramDraw = self.module.drawer.DiagramDraw49+50+ basename = re.sub('.svg$', '', self.options.output)51+ for i, group in enumerate(DiagramBuilder.build(tree)):52+ outfile = '%s_%d.svg' % (basename, i + 1)53+ draw = DiagramDraw(self.options.type, group, outfile,54+ fontmap=self.fontmap,55+ antialias=self.options.antialias,56+ nodoctype=self.options.nodoctype,57+ transparency=self.options.transparency)58+ draw.draw()59+ draw.save()60+61+ return 062+63+64+def main(args=None):65+ if args is None:66+ args = sys.argv[1:]67+68+ return BlockdiagApp().run(args)
A diagramzip-python-units/blockdiag-family/src/blockdiag/drawer.py +188 -0
@@ -0,0 +1,188 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from collections import defaultdict17+18+from blockdiag import imagedraw, noderenderer19+from blockdiag.metrics import AutoScaler, DiagramMetrics20+21+22+class DiagramDraw(object):23+ shadow_colors = defaultdict(lambda: (0, 0, 0))24+ shadow_colors['PNG'] = (64, 64, 64)25+ shadow_colors['PDF'] = (144, 144, 144)26+27+ def __init__(self, _format, diagram, filename=None, **kwargs):28+ self.format = _format.upper()29+ self.diagram = diagram30+ self.fill = kwargs.get('fill', (0, 0, 0))31+ self.badgeFill = kwargs.get('badgeFill', 'pink')32+ self.filename = filename33+ self.shadow = self.shadow_colors[self.format.upper()]34+35+ if self.format == 'PNG' and kwargs.get('antialias'):36+ self.scale_ratio = 237+ else:38+ self.scale_ratio = 139+40+ self.drawer = imagedraw.create(self.format, self.filename,41+ filters=['linejump'],42+ scale_ratio=self.scale_ratio,43+ **kwargs)44+45+ self.metrics = self.create_metrics(kwargs.get('basediagram', diagram),46+ drawer=self.drawer,47+ fontmap=kwargs.get('fontmap'))48+ if self.scale_ratio == 2:49+ self.metrics = AutoScaler(self.metrics,50+ scale_ratio=self.scale_ratio)51+52+ self.drawer.set_canvas_size(self.pagesize())53+ self.drawer.set_options(jump_radius=self.metrics.cellsize / 2)54+55+ def create_metrics(self, *args, **kwargs):56+ return DiagramMetrics(*args, **kwargs)57+58+ @property59+ def nodes(self):60+ for node in self.diagram.traverse_nodes():61+ if node.drawable:62+ yield node63+64+ @property65+ def groups(self):66+ for group in self.diagram.traverse_groups(preorder=True):67+ if not group.drawable:68+ yield group69+70+ @property71+ def edges(self):72+ edges = self.diagram.traverse_edges(preorder=True)73+ for edge in (e for e in edges if e.style != 'none'):74+ yield edge75+76+ def pagesize(self, scaled=False):77+ if scaled:78+ metrics = self.metrics79+ else:80+ metrics = self.metrics.original_metrics81+82+ width = self.diagram.colwidth83+ height = self.diagram.colheight84+ return metrics.pagesize(width, height)85+86+ def draw(self, **kwargs):87+ # switch metrics object during draw backgrounds88+ temp, self.metrics = self.metrics, self.metrics.original_metrics89+ self._draw_background()90+ self.metrics = temp91+92+ if self.scale_ratio > 1:93+ pagesize = self.pagesize(scaled=True)94+ self.drawer.resizeCanvas(pagesize)95+96+ self._draw_elements(**kwargs)97+98+ def _draw_background(self):99+ # Draw node groups.100+ for group in self.groups:101+ if group.shape == 'box':102+ box = self.metrics.group(group).marginbox103+ if group.href and self.format == 'SVG':104+ drawer = self.drawer.anchor(group.href)105+ else:106+ drawer = self.drawer107+108+ drawer.rectangle(box, fill=group.color, filter='blur')109+110+ # Drop node shadows.111+ for node in self.nodes:112+ if node.color != 'none' and self.diagram.shadow_style != 'none':113+ r = noderenderer.get(node.shape)114+115+ shape = r(node, self.metrics)116+ if node.href and self.format == 'SVG':117+ drawer = self.drawer.anchor(node.href)118+ else:119+ drawer = self.drawer120+121+ shape.render(drawer, self.format,122+ fill=self.shadow, shadow=True,123+ style=self.diagram.shadow_style)124+125+ def _draw_elements(self, **kwargs):126+ for node in self.nodes:127+ self.node(node, **kwargs)128+129+ for edge in self.edges:130+ self.edge(edge)131+132+ for edge in self.edges:133+ self.edge_label(edge)134+135+ for group in self.groups:136+ if group.shape == 'line':137+ box = self.metrics.group(group).marginbox138+ self.drawer.rectangle(box, fill='none', outline=group.color,139+ style=group.style, thick=group.thick)140+141+ for node in self.groups:142+ self.group_label(node, **kwargs)143+144+ def node(self, node, **kwargs):145+ r = noderenderer.get(node.shape)146+ shape = r(node, self.metrics)147+ if node.href and self.format == 'SVG':148+ drawer = self.drawer.anchor(node.href)149+ else:150+ drawer = self.drawer151+152+ shape.render(drawer, self.format, fill=self.fill,153+ badgeFill=self.badgeFill)154+155+ def group_label(self, group):156+ m = self.metrics.group(group)157+ font = self.metrics.font_for(group)158+159+ if group.label and not group.separated:160+ self.drawer.textarea(m.grouplabelbox, group.label, font=font,161+ fill=group.textcolor)162+ elif group.label:163+ self.drawer.textarea(m.corebox, group.label, font=font,164+ fill=group.textcolor)165+166+ def edge(self, edge):167+ metrics = self.metrics.edge(edge)168+169+ for line in metrics.shaft.polylines:170+ self.drawer.line(line, fill=edge.color, thick=edge.thick,171+ style=edge.style, jump=True)172+173+ for head in metrics.heads:174+ if edge.hstyle in ('generalization', 'aggregation'):175+ self.drawer.polygon(head, outline=edge.color, fill='white')176+ else:177+ self.drawer.polygon(head, outline=edge.color, fill=edge.color)178+179+ def edge_label(self, edge):180+ if edge.label:181+ metrics = self.metrics.edge(edge)182+183+ font = self.metrics.font_for(edge)184+ self.drawer.textarea(metrics.labelbox, edge.label, font=font,185+ fill=edge.textcolor, outline=self.fill)186+187+ def save(self, size=None):188+ return self.drawer.save(self.filename, size, self.format)
A diagramzip-python-units/blockdiag-family/src/blockdiag/elements.py +662 -0
@@ -0,0 +1,662 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import copy17+import os18+import re19+20+from blockdiag import noderenderer, plugins21+from blockdiag.utils import XY, images, unquote, urlutil, uuid22+from blockdiag.utils.logging import warning23+24+25+class Base(object):26+ basecolor = (255, 255, 255)27+ textcolor = (0, 0, 0)28+ fontfamily = None29+ fontsize = None30+ style = None31+ int_attrs = ['colwidth', 'colheight', 'fontsize']32+33+ @classmethod34+ def set_default_color(cls, color):35+ cls.basecolor = images.color_to_rgb(color)36+37+ @classmethod38+ def set_default_style(cls, style):39+ cls.style = style40+41+ @classmethod42+ def set_default_text_color(cls, color):43+ cls.textcolor = images.color_to_rgb(color)44+45+ @classmethod46+ def set_default_fontfamily(cls, fontfamily):47+ cls.fontfamily = fontfamily48+49+ @classmethod50+ def set_default_fontsize(cls, fontsize):51+ cls.fontsize = int(fontsize)52+53+ @classmethod54+ def clear(cls):55+ cls.basecolor = (255, 255, 255)56+ cls.textcolor = (0, 0, 0)57+ cls.fontfamily = None58+ cls.fontsize = None59+ cls.style = None60+61+ def duplicate(self):62+ return copy.copy(self)63+64+ def set_attribute(self, attr):65+ name = attr.name66+ value = unquote(attr.value)67+68+ if name == 'class':69+ if value in Diagram.classes:70+ klass = Diagram.classes[value]71+ self.set_attributes(klass.attrs)72+ else:73+ msg = "Unknown class: %s" % value74+ raise AttributeError(msg)75+ elif hasattr(self, "set_%s" % name):76+ getattr(self, "set_%s" % name)(value)77+ elif name in self.int_attrs:78+ setattr(self, name, int(value))79+ elif hasattr(self, name) and not callable(getattr(self, name)):80+ setattr(self, name, value)81+ else:82+ class_name = self.__class__.__name__83+ msg = "Unknown attribute: %s.%s" % (class_name, attr.name)84+ raise AttributeError(msg)85+86+ def set_attributes(self, attrs):87+ for attr in attrs:88+ self.set_attribute(attr)89+90+ def set_style(self, value):91+ if re.search(r'^(?:none|solid|dotted|dashed|\d+(,\d+)*)$', value, re.I):92+ self.style = value.lower()93+ else:94+ class_name = self.__class__.__name__95+ msg = "unknown %s style: %s" % (class_name, value)96+ raise AttributeError(msg)97+98+99+class Element(Base):100+ namespace = {}101+ int_attrs = Base.int_attrs + ['width', 'height']102+103+ @classmethod104+ def get(cls, elemid):105+ if not elemid:106+ elemid = uuid.generate()107+108+ unquote_id = unquote(elemid)109+ if unquote_id not in cls.namespace:110+ obj = cls(elemid)111+ cls.namespace[unquote_id] = obj112+113+ return cls.namespace[unquote_id]114+115+ @classmethod116+ def clear(cls):117+ super(Element, cls).clear()118+ cls.namespace = {}119+ cls.basecolor = (255, 255, 255)120+ cls.textcolor = (0, 0, 0)121+122+ def __init__(self, elemid):123+ self.id = unquote(elemid)124+ self.label = ''125+ self.xy = XY(0, 0)126+ self.group = None127+ self.drawable = False128+ self.order = 0129+ self.color = self.basecolor130+ self.width = None131+ self.height = None132+ self.colwidth = 1133+ self.colheight = 1134+ self.stacked = False135+136+ def __repr__(self):137+ _format = "<%s '%s' %s %dx%d at 0x%08x>"138+ params = (self.__class__.__name__, self.id, str(self.xy),139+ self.colwidth, self.colheight, id(self))140+ return _format % params141+142+ def set_color(self, color):143+ self.color = images.color_to_rgb(color)144+145+ def set_textcolor(self, color):146+ self.textcolor = images.color_to_rgb(color)147+148+149+class DiagramNode(Element):150+ shape = 'box'151+ int_attrs = Element.int_attrs + ['rotate']152+ linecolor = (0, 0, 0)153+ label_orientation = 'horizontal'154+ desctable = []155+ attrname = {}156+157+ @classmethod158+ def set_default_shape(cls, shape):159+ cls.shape = shape160+161+ @classmethod162+ def set_default_linecolor(cls, color):163+ cls.linecolor = images.color_to_rgb(color)164+165+ @classmethod166+ def clear(cls):167+ super(DiagramNode, cls).clear()168+ cls.shape = 'box'169+ cls.linecolor = (0, 0, 0)170+ cls.label_orientation = 'horizontal'171+ cls.desctable = ['numbered', 'label', 'description']172+ cls.attrname = dict(numbered='No', label='Name',173+ description='Description')174+175+ def __init__(self, elemid):176+ super(DiagramNode, self).__init__(elemid)177+178+ self.label = unquote(elemid) or ''179+ self.numbered = None180+ self.icon = None181+ self.background = None182+ self.description = None183+ self.rotate = 0184+ self.drawable = True185+ self.href = None186+187+ plugins.fire_node_event(self, 'created')188+189+ def set_attribute(self, attr):190+ if plugins.fire_node_event(self, 'attr_changing', attr):191+ super(DiagramNode, self).set_attribute(attr)192+ plugins.fire_node_event(self, 'attr_changed', attr)193+194+ def set_linecolor(self, color):195+ self.linecolor = images.color_to_rgb(color)196+197+ def set_shape(self, value):198+ if noderenderer.get(value):199+ self.shape = value200+ else:201+ msg = "unknown node shape: %s" % value202+ raise AttributeError(msg)203+204+ def set_icon(self, value):205+ if urlutil.isurl(value) or os.path.isfile(value):206+ self.icon = value207+ else:208+ warning("icon image not found: %s", value)209+210+ def set_background(self, value):211+ if urlutil.isurl(value) or os.path.isfile(value):212+ self.background = value213+ else:214+ warning("background image not found: %s", value)215+216+ def set_stacked(self, _):217+ self.stacked = True218+219+ def set_label_orientation(self, value):220+ value = value.lower()221+ if value in ('horizontal', 'vertical'):222+ self.label_orientation = value223+ else:224+ msg = "unknown label orientation: %s" % value225+ raise AttributeError(msg)226+227+ def to_desctable(self):228+ attrs = []229+ for name in self.desctable:230+ value = getattr(self, name)231+ if value is None:232+ attrs.append("")233+ else:234+ attrs.append(value)235+236+ return attrs237+238+239+class NodeGroup(Element):240+ basecolor = (243, 152, 0)241+242+ @classmethod243+ def clear(cls):244+ super(NodeGroup, cls).clear()245+ cls.basecolor = (243, 152, 0)246+247+ def __init__(self, elemid):248+ super(NodeGroup, self).__init__(elemid)249+250+ self.level = 0251+ self.separated = False252+ self.shape = 'box'253+ self.thick = 3254+ self.nodes = []255+ self.edges = []256+ self.icon = None257+ self.orientation = 'landscape'258+ self.href = None259+260+ def duplicate(self):261+ copied = super(NodeGroup, self).duplicate()262+ copied.nodes = []263+ copied.edges = []264+265+ return copied266+267+ def parent(self, level):268+ if self.level < level:269+ return None270+271+ group = self272+ while group.level != level:273+ group = group.group274+275+ return group276+277+ def is_parent(self, other):278+ parent = self.parent(other.level)279+ return parent == other280+281+ def traverse_nodes(self, preorder=False):282+ for node in self.nodes:283+ if isinstance(node, NodeGroup):284+ if preorder:285+ yield node286+287+ for subnode in node.traverse_nodes(preorder=preorder):288+ yield subnode289+290+ if not preorder:291+ yield node292+ else:293+ yield node294+295+ def traverse_edges(self, preorder=False):296+ if preorder:297+ for edge in self.edges:298+ yield edge299+300+ for group in self.traverse_groups(preorder):301+ for edge in group.traverse_edges(preorder):302+ yield edge303+304+ if not preorder:305+ for edge in self.edges:306+ yield edge307+308+ def traverse_groups(self, preorder=False):309+ for node in self.traverse_nodes(preorder=preorder):310+ if isinstance(node, NodeGroup):311+ yield node312+313+ def fixiate(self, fixiate_nodes=False):314+ if self.separated:315+ self.colwidth = 1316+ self.colheight = 1317+318+ return319+ elif len(self.nodes) > 0:320+ self.colwidth = max(x.xy.x + x.colwidth for x in self.nodes)321+ self.colheight = max(x.xy.y + x.colheight for x in self.nodes)322+323+ for node in self.nodes:324+ if fixiate_nodes:325+ node.xy = XY(self.xy.x + node.xy.x,326+ self.xy.y + node.xy.y)327+328+ if isinstance(node, NodeGroup):329+ node.fixiate(fixiate_nodes)330+331+ def update_order(self):332+ for i, node in enumerate(self.nodes):333+ node.order = i334+335+ def set_orientation(self, value):336+ value = value.lower()337+ if value in ('landscape', 'portrait'):338+ self.orientation = value339+ else:340+ msg = "unknown diagram orientation: %s" % value341+ raise AttributeError(msg)342+343+ def set_shape(self, value):344+ value = value.lower()345+ if value in ('box', 'line'):346+ self.shape = value347+ else:348+ msg = "unknown group shape: %s" % value349+ raise AttributeError(msg)350+351+352+class DiagramEdge(Base):353+ basecolor = (0, 0, 0)354+ namespace = {}355+356+ @classmethod357+ def get(cls, node1, node2):358+ if node1 not in cls.namespace:359+ cls.namespace[node1] = {}360+361+ if node2 not in cls.namespace[node1]:362+ obj = cls(node1, node2)363+ cls.namespace[node1][node2] = obj364+365+ return cls.namespace[node1][node2]366+367+ @classmethod368+ def find(cls, node1, node2=None):369+ if node1 is None and node2 is None:370+ return cls.find_all()371+ elif isinstance(node1, NodeGroup):372+ edges = cls.find(None, node2)373+ edges = (e for e in edges if e.node1.group.is_parent(node1))374+ return [e for e in edges if not e.node2.group.is_parent(node1)]375+ elif isinstance(node2, NodeGroup):376+ edges = cls.find(node1, None)377+ edges = (e for e in edges if e.node2.group.is_parent(node2))378+ return [e for e in edges if not e.node1.group.is_parent(node2)]379+ elif node1 is None:380+ return [e for e in cls.find_all() if e.node2 == node2]381+ else:382+ if node1 not in cls.namespace:383+ return []384+385+ if node2 is None:386+ return cls.namespace[node1].values()387+388+ if node2 not in cls.namespace[node1]:389+ return []390+391+ return cls.namespace[node1][node2]392+393+ @classmethod394+ def find_all(cls):395+ for v1 in cls.namespace.values():396+ for v2 in v1.values():397+ yield v2398+399+ @classmethod400+ def find_by_level(cls, level):401+ edges = []402+ for e in cls.find_all():403+ edge = e.duplicate()404+ skips = 0405+406+ if edge.node1.group.level < level:407+ skips += 1408+ else:409+ while edge.node1.group.level != level:410+ edge.node1 = edge.node1.group411+412+ if edge.node2.group.level < level:413+ skips += 1414+ else:415+ while edge.node2.group.level != level:416+ edge.node2 = edge.node2.group417+418+ if skips == 2:419+ continue420+421+ edges.append(edge)422+423+ return edges424+425+ @classmethod426+ def clear(cls):427+ super(DiagramEdge, cls).clear()428+ cls.namespace = {}429+ cls.basecolor = (0, 0, 0)430+431+ def __init__(self, node1, node2):432+ self.node1 = node1433+ self.node2 = node2434+ self.crosspoints = []435+ self.skipped = 0436+437+ self.label = None438+ self.description = None439+ self.dir = 'forward'440+ self.color = self.basecolor441+ self.hstyle = None442+ self.folded = None443+ self.thick = None444+445+ def __repr__(self):446+ _format = "<%s '%s' %s - '%s' %s at 0x%08x>"447+ params = (self.__class__.__name__, self.node1.id, self.node1.xy,448+ self.node2.id, self.node2.xy, id(self))449+ return _format % params450+451+ def set_dir(self, value):452+ value = value.lower()453+ if value in ('back', 'both', 'none', 'forward'):454+ self.dir = value455+ elif value == '-<':456+ self.dir = 'forward'457+ self.hstyle = 'onemany'458+ elif value == '>-':459+ self.dir = 'back'460+ self.hstyle = 'manyone'461+ elif value == '>-<':462+ self.dir = 'both'463+ self.hstyle = 'manymany'464+ elif value == '->':465+ self.dir = 'forward'466+ elif value == '<-':467+ self.dir = 'back'468+ elif value == '<->':469+ self.dir = 'both'470+ elif value == '--':471+ self.dir = 'none'472+ else:473+ msg = "unknown edge dir: %s" % value474+ raise AttributeError(msg)475+476+ def set_color(self, color):477+ self.color = images.color_to_rgb(color)478+479+ def set_hstyle(self, value):480+ value = value.lower()481+ if value in ('generalization', 'composition', 'aggregation'):482+ self.hstyle = value483+ elif value == 'oneone':484+ self.dir = 'none'485+ self.hstyle = value486+ elif value == 'onemany':487+ self.dir = 'forward'488+ self.hstyle = value489+ elif value == 'manyone':490+ self.dir = 'back'491+ self.hstyle = value492+ elif value == 'manymany':493+ self.dir = 'both'494+ self.hstyle = value495+ else:496+ msg = "unknown edge hstyle: %s" % value497+ raise AttributeError(msg)498+499+ def set_folded(self, _):500+ self.folded = True501+502+ def set_nofolded(self, _):503+ self.folded = False504+505+ def set_thick(self, _):506+ self.thick = 3507+508+ @property509+ def direction(self):510+ node1 = self.node1.xy511+ node2 = self.node2.xy512+513+ if node1.x > node2.x:514+ if node1.y > node2.y:515+ _dir = 'left-up'516+ elif node1.y == node2.y:517+ _dir = 'left'518+ else:519+ _dir = 'left-down'520+ elif node1.x == node2.x:521+ if node1.y > node2.y:522+ _dir = 'up'523+ elif node1.y == node2.y:524+ _dir = 'same'525+ else:526+ _dir = 'down'527+ else:528+ if node1.y > node2.y:529+ _dir = 'right-up'530+ elif node1.y == node2.y:531+ _dir = 'right'532+ else:533+ _dir = 'right-down'534+535+ return _dir536+537+ def to_desctable(self):538+ label = "%s -> %s" % (self.node1.label, self.node2.label)539+ return [label, self.description]540+541+542+class Diagram(NodeGroup):543+ _DiagramNode = DiagramNode544+ _DiagramEdge = DiagramEdge545+ _NodeGroup = NodeGroup546+547+ classes = {}548+ shadow_style = 'blur'549+ linecolor = (0, 0, 0)550+ int_attrs = (NodeGroup.int_attrs +551+ ['node_width', 'node_height', 'span_width', 'span_height'])552+553+ @classmethod554+ def clear(cls):555+ super(Diagram, cls).clear()556+ cls.shadow_style = 'blur'557+ cls.linecolor = (0, 0, 0)558+ cls.classes = {}559+560+ def __init__(self):561+ super(Diagram, self).__init__(None)562+563+ self.config = None564+ self.node_width = None565+ self.node_height = None566+ self.span_width = None567+ self.span_height = None568+ self.page_padding = None569+ self.edge_layout = None570+571+ def set_plugin(self, name, attrs, **kwargs):572+ kwargs.update(dict([str(unquote(attr.name)), unquote(attr.value)]573+ for attr in attrs))574+ plugins.load([name], diagram=self, **kwargs)575+576+ def set_plugins(self, value, **kwargs):577+ modules = [name.strip() for name in value.split(',')]578+ plugins.load(modules, diagram=self, **kwargs)579+580+ def set_default_shape(self, value):581+ if noderenderer.get(value):582+ DiagramNode.set_default_shape(value)583+ else:584+ msg = "unknown node shape: %s" % value585+ raise AttributeError(msg)586+587+ def set_default_label_orientation(self, value):588+ value = value.lower()589+ if value in ('horizontal', 'vertical'):590+ DiagramNode.label_orientation = value591+ else:592+ msg = "unknown label orientation: %s" % value593+ raise AttributeError(msg)594+595+ def set_default_text_color(self, color):596+ warning("default_text_color is obsoleted; use default_textcolor")597+ self.set_default_textcolor(color)598+599+ def set_default_textcolor(self, color):600+ self.textcolor = images.color_to_rgb(color)601+ self._DiagramNode.set_default_text_color(self.textcolor)602+ self._NodeGroup.set_default_text_color(self.textcolor)603+ self._DiagramEdge.set_default_text_color(self.textcolor)604+605+ def set_default_node_color(self, color):606+ color = images.color_to_rgb(color)607+ self._DiagramNode.set_default_color(color)608+609+ def set_default_node_style(self, value):610+ if re.search(r'^(?:none|solid|dotted|dashed|\d+(,\d+)*)$', value, re.I):611+ self._DiagramNode.set_default_style(value)612+ else:613+ msg = "unknown node style: %s" % value614+ raise AttributeError(msg)615+616+ def set_default_line_color(self, color):617+ warning("default_line_color is obsoleted; use default_linecolor")618+ self.set_default_linecolor(color)619+620+ def set_default_linecolor(self, color):621+ self.linecolor = images.color_to_rgb(color)622+ self._DiagramNode.set_default_linecolor(self.linecolor)623+ self._DiagramEdge.set_default_color(self.linecolor)624+625+ def set_default_group_color(self, color):626+ color = images.color_to_rgb(color)627+ self._NodeGroup.set_default_color(color)628+629+ def set_shape_namespace(self, value):630+ noderenderer.set_default_namespace(value)631+632+ def set_default_fontfamily(self, fontfamily):633+ self._DiagramNode.set_default_fontfamily(fontfamily)634+ self._NodeGroup.set_default_fontfamily(fontfamily)635+ self._DiagramEdge.set_default_fontfamily(fontfamily)636+637+ def set_default_fontsize(self, fontsize):638+ self._DiagramNode.set_default_fontsize(fontsize)639+ self._NodeGroup.set_default_fontsize(fontsize)640+ self._DiagramEdge.set_default_fontsize(fontsize)641+642+ def set_shadow_style(self, value):643+ value = value.lower()644+ if value in ('solid', 'blur', 'none'):645+ self.shadow_style = value646+ else:647+ msg = "unknown shadow style: %s" % value648+ raise AttributeError(msg)649+650+ def set_edge_layout(self, value):651+ value = value.lower()652+ if value in ('normal', 'flowchart'):653+ warning("edge_layout is very experimental feature!")654+655+ self.edge_layout = value656+ else:657+ msg = "unknown edge layout: %s" % value658+ raise AttributeError(msg)659+660+ def set_fontsize(self, value):661+ warning("fontsize is obsoleted; use default_fontsize")662+ self.set_default_fontsize(int(value))
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/__init__.py +54 -0
@@ -0,0 +1,54 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from importlib import metadata17+18+from blockdiag.utils.logging import warning19+20+drawers = {}21+22+23+def init_imagedrawers(debug=False):24+ for drawer in metadata.entry_points(group='blockdiag_imagedrawers'):25+ try:26+ module = drawer.load()27+ if hasattr(module, 'setup'):28+ module.setup(module)29+ except Exception as exc:30+ if debug:31+ warning('Failed to load %s: %r' % (drawer.name, exc))32+33+34+def install_imagedrawer(ext, drawer):35+ drawers[ext] = drawer36+37+38+def create(_format, filename, **kwargs):39+ if len(drawers) == 0:40+ init_imagedrawers(debug=kwargs.get('debug'))41+42+ _format = _format.lower()43+ if _format in drawers:44+ drawer = drawers[_format](filename, **kwargs)45+ else:46+ msg = 'failed to load %s image driver' % _format47+ raise RuntimeError(msg)48+49+ if 'linejump' in kwargs.get('filters', []):50+ from blockdiag.imagedraw.filters.linejump import LineJumpDrawFilter51+ jumpsize = kwargs.get('jumpsize', 0)52+ drawer = LineJumpDrawFilter(drawer, jumpsize)53+54+ return drawer
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/base.py +75 -0
@@ -0,0 +1,75 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from functools import partial17+18+from blockdiag.imagedraw import textfolder19+from blockdiag.utils import Box20+21+22+class ImageDraw(object):23+ self_generative_methods = []24+ nosideeffect_methods = ['set_canvas_size', 'textsize', 'textlinesize']25+ supported_path = False26+ baseline_text_rendering = False27+28+ def set_canvas_size(self, size):29+ pass30+31+ def set_options(self, **kwargs):32+ pass33+34+ def line(self, xy, **kwargs):35+ pass36+37+ def rectangle(self, box, **kwargs):38+ pass39+40+ def polygon(self, xy, **kwargs):41+ pass42+43+ def arc(self, xy, start, end, **kwargs):44+ pass45+46+ def ellipse(self, xy, **kwargs):47+ pass48+49+ def textsize(self, string, font, maxwidth=None, **kwargs):50+ if maxwidth is None:51+ maxwidth = 6553552+53+ box = Box(0, 0, maxwidth, 65535)54+ textbox = self.textfolder(box, string, font, **kwargs)55+ return textbox.outlinebox.size56+57+ @property58+ def textfolder(self):59+ return partial(textfolder.get, self,60+ adjustBaseline=self.baseline_text_rendering)61+62+ def textlinesize(self, string, font, **kwargs):63+ pass64+65+ def text(self, xy, string, font, **kwargs):66+ pass67+68+ def textarea(self, box, string, font, **kwargs):69+ pass70+71+ def image(self, box, url):72+ pass73+74+ def save(self, filename, size, _format):75+ pass
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/filters/__init__.py +14 -0
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/filters/linejump.py +183 -0
@@ -0,0 +1,183 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import functools17+18+from blockdiag.utils import XY, Box19+20+21+class LazyReceiver(object):22+ def __init__(self, target):23+ self.target = target24+ self.calls = []25+26+ def __getattr__(self, name):27+ return self.get_lazy_method(name)28+29+ def get_lazy_method(self, name):30+ if name in self.target.nosideeffect_methods:31+ method = self._find_method(name)32+ return functools.partial(method, self.target)33+ elif name in self.target.self_generative_methods:34+ def _(*args, **kwargs):35+ receiver = LazySubReceiver(name, self.target, *args, **kwargs)36+ self.calls.append((receiver, args, kwargs))37+ return receiver38+39+ return _40+ else:41+ def _(*args, **kwargs):42+ self.calls.append((name, args, kwargs))43+ return self44+45+ return _46+47+ def _find_method(self, name):48+ if isinstance(name, LazyReceiver):49+ return name50+51+ for p in self.target.__class__.__mro__:52+ if name in p.__dict__:53+ return p.__dict__[name]54+55+ raise AttributeError("%s instance has no attribute '%s'" %56+ (self.target.__class__.__name__, name))57+58+ def _run(self):59+ for name, args, kwargs in self.calls:60+ method = self._find_method(name)61+ method(self.target, *args, **kwargs)62+63+64+class LazySubReceiver(LazyReceiver):65+ def __init__(self, name, target, *args, **kwargs):66+ self.name = name67+ self.args = args68+ self.kwargs = kwargs69+ super(LazySubReceiver, self).__init__(target)70+71+ def __call__(self, target, *args, **kwargs):72+ method = self._find_method(self.name)73+ self.target = method(self.target, *self.args, **self.kwargs)74+ self._run()75+76+77+class LineJumpDrawFilter(LazyReceiver):78+ def __init__(self, target, jump_radius):79+ super(LineJumpDrawFilter, self).__init__(target)80+ self.ytree = []81+ self.x_cross = {}82+ self.y_cross = {}83+ self.forward = 'holizonal'84+ self.jump_radius = jump_radius85+ self.jump_shift = 086+87+ def set_options(self, **kwargs):88+ if 'jump_forward' in kwargs:89+ self.forward = kwargs['jump_forward']90+91+ if 'jump_radius' in kwargs:92+ self.jump_radius = kwargs['jump_radius']93+94+ if 'jump_shift' in kwargs:95+ self.jump_shift = kwargs['jump_shift']96+97+ def _run(self):98+ for name, args, kwargs in self.calls:99+ if name == 'line' and kwargs.get('jump'):100+ ((x1, y1), (x2, y2)) = args[0]101+ if self.forward == 'holizonal' and y1 == y2:102+ self._holizonal_jumpline(x1, y1, x2, y2, **kwargs)103+ continue104+ elif self.forward == 'vertical' and x1 == x2:105+ self._vertical_jumpline(x1, y1, x2, y2, **kwargs)106+ continue107+108+ method = self._find_method(name)109+ method(self.target, *args, **kwargs)110+111+ def _holizonal_jumpline(self, x1, y1, x2, y2, **kwargs):112+ y = y1113+ if x2 < x1:114+ x1, x2 = x2, x1115+116+ for x in sorted(self.x_cross.get(y, [])):117+ if x1 < x and x < x2:118+ arckwargs = dict(kwargs)119+ del arckwargs['jump']120+121+ x += self.jump_shift122+ r = self.jump_radius123+ line = (XY(x1, y), XY(x - r, y))124+ self.target.line(line, **kwargs)125+ box = Box(x - r, y - r, x + r, y + r)126+ self.target.arc(box, 180, 0, **arckwargs)127+ x1 = x + r128+129+ self.target.line((XY(x1, y), XY(x2, y)), **kwargs)130+131+ def _vertical_jumpline(self, x1, y1, x2, y2, **kwargs):132+ x = x1133+ if y2 < y1:134+ y1, y2 = y2, y1135+136+ for y in sorted(self.y_cross.get(x, [])):137+ if y1 < y and y < y2:138+ arckwargs = dict(kwargs)139+ del arckwargs['jump']140+141+ y += self.jump_shift142+ r = self.jump_radius143+ line = (XY(x, y1), XY(x, y - r))144+ self.target.line(line, **kwargs)145+ box = Box(x - r, y - r, x + r, y + r)146+ self.target.arc(box, 270, 90, **arckwargs)147+ y1 = y + r148+149+ self.target.line((XY(x, y1), XY(x, y2)), **kwargs)150+151+ def line(self, xy, **kwargs):152+ from bisect import insort153+ for st, ed in zip(xy[:-1], xy[1:]):154+ self.get_lazy_method("line")((st, ed), **kwargs)155+156+ if 'jump' in kwargs and kwargs['jump'] is True:157+ if st.y == ed.y: # horizonal158+ insort(self.ytree, (st.y, 0, (st, ed)))159+ elif st.x == ed.x: # vertical160+ insort(self.ytree, (max(st.y, ed.y), -1, (st, ed)))161+ insort(self.ytree, (min(st.y, ed.y), +1, (st, ed)))162+163+ def save(self, *args, **kwargs):164+ # Search crosspoints165+ from bisect import bisect_left, bisect_right, insort166+ xtree = []167+ for y, _, ((x1, y1), (x2, y2)) in self.ytree:168+ if x2 < x1:169+ x1, x2 = x2, x1170+ if y2 < y1:171+ y1, y2 = y2, y1172+173+ if y == y1:174+ insort(xtree, x1)175+176+ if y == y2:177+ del xtree[bisect_left(xtree, x1)]178+ for x in xtree[bisect_right(xtree, x1):bisect_left(xtree, x2)]:179+ self.x_cross.setdefault(y, set()).add(x)180+ self.y_cross.setdefault(x, set()).add(y)181+182+ self._run()183+ return self.target.save(*args, **kwargs)
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/pdf.py +245 -0
@@ -0,0 +1,245 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import math17+import re18+import sys19+20+from reportlab.lib.utils import ImageReader21+from reportlab.pdfbase import pdfmetrics22+from reportlab.pdfbase.ttfonts import TTFont23+from reportlab.pdfgen import canvas24+25+from blockdiag.imagedraw import base26+from blockdiag.imagedraw.utils import memoize27+from blockdiag.utils import Box, Size, images28+from blockdiag.utils.fontmap import parse_fontpath29+30+31+class PDFImageDraw(base.ImageDraw):32+ baseline_text_rendering = True33+34+ def __init__(self, filename, **kwargs):35+ self.size = None36+ self.filename = filename37+ self.canvas = None38+ self.fonts = {}39+40+ self.set_canvas_size(Size(1, 1)) # This line make textsize() workable41+42+ def set_canvas_size(self, size):43+ if self.filename is None:44+ self.canvas = canvas.Canvas(sys.stdout.buffer, pagesize=size)45+ else:46+ self.canvas = canvas.Canvas(self.filename, pagesize=size)47+48+ self.size = size49+50+ def set_font(self, font):51+ if font.path is None:52+ msg = "Could not detect fonts, use --font option\n"53+ raise RuntimeError(msg)54+55+ if font.path not in self.fonts:56+ path, index = parse_fontpath(font.path)57+ if index:58+ ttfont = TTFont(font.path, path, subfontIndex=index)59+ else:60+ ttfont = TTFont(font.path, path)61+ pdfmetrics.registerFont(ttfont)62+63+ self.fonts[font.path] = ttfont64+65+ self.canvas.setFont(font.path, font.size)66+67+ def set_render_params(self, **kwargs):68+ self.set_stroke_color(kwargs.get('outline'))69+ self.set_fill_color(kwargs.get('fill', 'none'))70+ self.set_style(kwargs.get('style'), kwargs.get('thick'))71+72+ params = {}73+ if kwargs.get('fill', 'none') == 'none':74+ params['fill'] = 075+ else:76+ params['fill'] = 177+78+ if kwargs.get('outline') is None:79+ params['stroke'] = 080+ else:81+ params['stroke'] = 182+83+ return params84+85+ def set_style(self, style, thick):86+ if thick is None:87+ thick = 188+89+ if style == 'dotted':90+ self.canvas.setDash([2 * thick, 2 * thick])91+ elif style == 'dashed':92+ self.canvas.setDash([4 * thick, 4 * thick])93+ elif style == 'none':94+ self.canvas.setDash([0, 65535 * thick])95+ elif re.search(r'^\d+(,\d+)*$', style or ""):96+ self.canvas.setDash([int(n) * thick for n in style.split(',')])97+ else:98+ self.canvas.setDash()99+100+ def set_stroke_color(self, color="black"):101+ if isinstance(color, str):102+ self.canvas.setStrokeColor(color)103+ elif color:104+ rgb = (color[0] / 256.0, color[1] / 256.0, color[2] / 256.0)105+ self.canvas.setStrokeColorRGB(*rgb)106+ else:107+ self.set_stroke_color()108+109+ def set_fill_color(self, color="white"):110+ if isinstance(color, str):111+ if color != 'none':112+ self.canvas.setFillColor(color)113+ elif color:114+ rgb = (color[0] / 256.0, color[1] / 256.0, color[2] / 256.0)115+ self.canvas.setFillColorRGB(*rgb)116+ else:117+ self.set_fill_color()118+119+ def path(self, pd, **kwargs):120+ params = self.set_render_params(**kwargs)121+ self.canvas.drawPath(pd, **params)122+123+ def rectangle(self, box, **kwargs):124+ x = box[0]125+ y = self.size[1] - box[3]126+ width = box[2] - box[0]127+ height = box[3] - box[1]128+129+ if 'thick' in kwargs and kwargs['thick'] is not None:130+ self.canvas.setLineWidth(kwargs['thick'])131+132+ params = self.set_render_params(**kwargs)133+ self.canvas.rect(x, y, width, height, **params)134+135+ if 'thick' in kwargs:136+ self.canvas.setLineWidth(1)137+138+ @memoize139+ def textlinesize(self, string, font):140+ self.set_font(font)141+ width = self.canvas.stringWidth(string, font.path, font.size)142+ return Size(int(math.ceil(width)), font.size)143+144+ def text(self, xy, string, font, **kwargs):145+ self.set_font(font)146+ self.set_fill_color(kwargs.get('fill'))147+ self.canvas.drawString(xy[0], self.size[1] - xy[1], string)148+149+ def textarea(self, box, string, font, **kwargs):150+ self.canvas.saveState()151+152+ if 'rotate' in kwargs and kwargs['rotate'] != 0:153+ angle = 360 - int(kwargs['rotate']) % 360154+ self.canvas.rotate(angle)155+156+ if angle == 90:157+ box = Box(-box.y2, box.x1, -box.y1, box.x1 + box.width)158+ box = box.shift(x=self.size.height, y=self.size.height)159+ elif angle == 180:160+ box = Box(-box.x2, -box.y2, -box.x1, -box.y2 + box.height)161+ box = box.shift(y=self.size.height * 2)162+ elif angle == 270:163+ box = Box(box.y1, -box.x2, box.y2, -box.x1)164+ box = box.shift(x=-self.size.height, y=self.size.height)165+166+ self.set_font(font)167+ lines = self.textfolder(box, string, font, **kwargs)168+169+ if kwargs.get('outline'):170+ outline = kwargs.get('outline')171+ self.rectangle(lines.outlinebox, fill='white', outline=outline)172+173+ rendered = False174+ for string, xy in lines.lines:175+ self.text(xy, string, font, **kwargs)176+ rendered = True177+ self.canvas.restoreState()178+179+ if not rendered and font.size > 0:180+ font.size = int(font.size * 0.8)181+ self.textarea(box, string, font, **kwargs)182+183+ def line(self, xy, **kwargs):184+ self.set_stroke_color(kwargs.get('fill', 'none'))185+ self.set_style(kwargs.get('style'), kwargs.get('thick'))186+187+ if 'thick' in kwargs and kwargs['thick'] is not None:188+ self.canvas.setLineWidth(kwargs['thick'])189+190+ p1 = xy[0]191+ y = self.size[1]192+ for p2 in xy[1:]:193+ self.canvas.line(p1.x, y - p1.y, p2.x, y - p2.y)194+ p1 = p2195+196+ if 'thick' in kwargs:197+ self.canvas.setLineWidth(1)198+199+ def arc(self, xy, start, end, **kwargs):200+ start, end = 360 - end, 360 - start201+ r = (360 + end - start) % 360202+203+ self.set_render_params(**kwargs)204+ y = self.size[1]205+ self.canvas.arc(xy[0], y - xy[3], xy[2], y - xy[1], start, r)206+207+ def ellipse(self, xy, **kwargs):208+ params = self.set_render_params(**kwargs)209+ y = self.size[1]210+ self.canvas.ellipse(xy[0], y - xy[3], xy[2], y - xy[1], **params)211+212+ def polygon(self, xy, **kwargs):213+ pd = self.canvas.beginPath()214+ y = self.size[1]215+ pd.moveTo(xy[0][0], y - xy[0][1])216+ for p in xy[1:]:217+ pd.lineTo(p[0], y - p[1])218+219+ params = self.set_render_params(**kwargs)220+ self.canvas.drawPath(pd, **params)221+222+ def image(self, box, url):223+ try:224+ image = images.open(url, mode='pillow')225+ if image.mode not in ('RGBA', 'L', 'RGB', 'CYMYK'):226+ # convert to format that reportlab can recognize227+ image = image.convert('RGBA')228+229+ y = self.size[1] - box[3]230+ data = ImageReader(image)231+ self.canvas.drawImage(data, box.x1, y, box.width, box.height,232+ mask='auto', preserveAspectRatio=True)233+ except IOError:234+ pass235+236+ def save(self, filename, size, _format):237+ # Ignore size and format parameter; compatibility for ImageDrawEx.238+239+ self.canvas.showPage()240+ self.canvas.save()241+242+243+def setup(self):244+ from blockdiag.imagedraw import install_imagedrawer245+ install_imagedrawer('pdf', PDFImageDraw)
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/png.py +516 -0
@@ -0,0 +1,516 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from __future__ import division17+18+import math19+import re20+from functools import partial, wraps21+from itertools import tee22+23+from PIL import Image, ImageDraw, ImageFilter, ImageFont24+25+from blockdiag.imagedraw import base26+from blockdiag.imagedraw.utils import memoize27+from blockdiag.imagedraw.utils.ellipse import dots as ellipse_dots28+from blockdiag.utils import XY, Box, Size, images29+from blockdiag.utils.fontmap import FontMap, parse_fontpath30+from blockdiag.utils.myitertools import istep, stepslice31+32+# to support pillow < 9.1.033+if not hasattr(Image, 'Resampling'):34+ from enum import IntEnum35+36+ class Resampling(IntEnum):37+ NEAREST = 038+ BOX = 439+ BILINEAR = 240+ HAMMING = 541+ BICUBIC = 342+ LANCZOS = 143+44+ Image.Resampling = Resampling45+46+47+def point_pairs(xylist):48+ iterable = iter(xylist)49+ for pt in iterable:50+ if isinstance(pt, int):51+ yield (pt, next(iterable))52+ else:53+ yield pt54+55+56+def line_segments(xylist):57+ p1, p2 = tee(point_pairs(xylist))58+ next(p2)59+ return zip(p1, p2)60+61+62+def dashize_line(line, length):63+ pt1, pt2 = line64+ if pt1[0] == pt2[0]: # holizonal65+ if pt1[1] > pt2[1]:66+ pt2, pt1 = line67+68+ r = stepslice(range(round(pt1[1]), round(pt2[1])), length)69+ for y1, y2 in istep(n for n in r):70+ yield [(pt1[0], y1), (pt1[0], y2)]71+72+ elif pt1[1] == pt2[1]: # vertical73+ if pt1[0] > pt2[0]:74+ pt2, pt1 = line75+76+ r = stepslice(range(round(pt1[0]), round(pt2[0])), length)77+ for x1, x2 in istep(n for n in r):78+ yield [(x1, pt1[1]), (x2, pt1[1])]79+ else: # diagonal80+ if pt1[0] > pt2[0]:81+ pt2, pt1 = line82+83+ # DDA (Digital Differential Analyzer) Algorithm84+ locus = []85+ m = float(pt2[1] - pt1[1]) / float(pt2[0] - pt1[0])86+ x = pt1[0]87+ y = pt1[1]88+89+ while x <= pt2[0]:90+ locus.append((int(x), int(round(y))))91+ x += 192+ y += m93+94+ for p1, p2 in istep(stepslice(locus, length)):95+ yield (p1, p2)96+97+98+def style2cycle(style, thick):99+ if thick is None:100+ thick = 1101+102+ if style == 'dotted':103+ length = [2 * thick, 2 * thick]104+ elif style == 'dashed':105+ length = [4 * thick, 4 * thick]106+ elif style == 'none':107+ length = [0, 65535 * thick]108+ elif re.search(r'^\d+(,\d+)*$', style or ""):109+ length = [int(n) * thick for n in style.split(',')]110+ else:111+ length = None112+113+ return length114+115+116+def ttfont_for(font):117+ if font.size <= 0:118+ return None119+ if font.path:120+ path, index = parse_fontpath(font.path)121+ if index:122+ ttfont = ImageFont.truetype(path, font.size, index=index)123+ else:124+ ttfont = ImageFont.truetype(path, font.size)125+ else:126+ ttfont = None127+128+ return ttfont129+130+131+class ImageDrawExBase(base.ImageDraw):132+ def __init__(self, filename, **kwargs):133+ self.filename = filename134+ self.transparency = kwargs.get('transparency')135+ self.bgcolor = kwargs.get('color', (256, 256, 256))136+ self._image = None137+ self.draw = None138+139+ if kwargs.get('parent'):140+ self.scale_ratio = kwargs.get('parent').scale_ratio141+ else:142+ self.scale_ratio = kwargs.get('scale_ratio', 1)143+144+ self.set_canvas_size(Size(1, 1)) # This line make textsize() workable145+146+ def paste(self, image, pt, mask=None):147+ self._image.paste(image, pt, mask)148+ self.draw = ImageDraw.Draw(self._image)149+150+ def set_canvas_size(self, size):151+ if self.transparency:152+ mode = 'RGBA'153+ else:154+ mode = 'RGB'155+156+ self._image = Image.new(mode, size, self.bgcolor)157+158+ # set transparency to background159+ if self.transparency:160+ alpha = Image.new('L', size, 1)161+ self._image.putalpha(alpha)162+163+ self.draw = ImageDraw.Draw(self._image)164+165+ def resizeCanvas(self, size):166+ self._image = self._image.resize(size, Image.Resampling.LANCZOS)167+ self.draw = ImageDraw.Draw(self._image)168+169+ def arc(self, box, start, end, **kwargs):170+ style = kwargs.get('style')171+ if 'style' in kwargs:172+ del kwargs['style']173+ if 'thick' in kwargs:174+ del kwargs['thick']175+176+ if style:177+ while start > end:178+ end += 360179+180+ cycle = style2cycle(style, kwargs.get('width'))181+ for pt in ellipse_dots(box, cycle, start, end):182+ self.draw.line([pt, pt], fill=kwargs['fill'])183+ else:184+ self.draw.arc(box.to_integer_point(), start, end, **kwargs)185+186+ def ellipse(self, box, **kwargs):187+ if 'filter' in kwargs:188+ del kwargs['filter']189+190+ style = kwargs.get('style')191+ if 'style' in kwargs:192+ del kwargs['style']193+194+ if style:195+ if kwargs.get('fill') != 'none':196+ kwargs2 = dict(kwargs)197+ if 'outline' in kwargs2:198+ del kwargs2['outline']199+ self.draw.ellipse(box, **kwargs2)200+201+ if 'outline' in kwargs:202+ kwargs['fill'] = kwargs['outline']203+ del kwargs['outline']204+205+ cycle = style2cycle(style, kwargs.get('width'))206+ for pt in ellipse_dots(box, cycle):207+ self.draw.line([pt, pt], fill=kwargs['fill'])208+ else:209+ if kwargs.get('fill') == 'none':210+ del kwargs['fill']211+212+ self.draw.ellipse(box.to_integer_point(), **kwargs)213+214+ def line(self, xy, **kwargs):215+ if 'jump' in kwargs:216+ del kwargs['jump']217+ if 'thick' in kwargs:218+ if kwargs['thick'] is not None:219+ kwargs['width'] = kwargs['thick']220+ del kwargs['thick']221+222+ style = kwargs.get('style')223+ if kwargs.get('fill') == 'none':224+ pass225+ elif (style in ('dotted', 'dashed', 'none') or226+ re.search(r'^\d+(,\d+)*$', style or "")):227+ self.dashed_line(xy, **kwargs)228+ else:229+ if 'style' in kwargs:230+ del kwargs['style']231+232+ self.draw.line(xy, **kwargs)233+234+ def dashed_line(self, xy, **kwargs):235+ style = kwargs.get('style')236+ del kwargs['style']237+238+ cycle = style2cycle(style, kwargs.get('width'))239+ for line in line_segments(xy):240+ for subline in dashize_line(line, cycle):241+ self.line(subline, **kwargs)242+243+ def rectangle(self, box, **kwargs):244+ thick = kwargs.get('thick', self.scale_ratio)245+ fill = kwargs.get('fill')246+ outline = kwargs.get('outline')247+ style = kwargs.get('style')248+249+ if thick == 1:250+ d = 0251+ else:252+ d = int(math.ceil(thick / 2.0))253+254+ if fill and fill != 'none':255+ self.draw.rectangle(box, fill=fill)256+257+ x1, y1, x2, y2 = box258+ lines = (((x1, y1), (x2, y1)), ((x1, y2), (x2, y2)), # horizonal259+ ((x1, y1 - d), (x1, y2 + d)), # vettical (left)260+ ((x2, y1 - d), (x2, y2 + d))) # vertical (right)261+262+ for line in lines:263+ self.line(line, fill=outline, width=thick, style=style)264+265+ def polygon(self, xy, **kwargs):266+ if 'filter' in kwargs:267+ del kwargs['filter']268+269+ if kwargs.get('fill') != 'none':270+ kwargs2 = dict(kwargs)271+272+ if 'style' in kwargs2:273+ del kwargs2['style']274+ if 'outline' in kwargs2:275+ del kwargs2['outline']276+ self.draw.polygon(xy, **kwargs2)277+278+ if kwargs.get('outline'):279+ kwargs['fill'] = kwargs['outline']280+ del kwargs['outline']281+ self.line(xy, **kwargs)282+283+ @property284+ def textfolder(self):285+ textfolder = super(ImageDrawExBase, self).textfolder286+ return partial(textfolder, scale=self.scale_ratio)287+288+ @memoize289+ def textlinesize(self, string, font):290+ ttfont = ttfont_for(font)291+ if ttfont is None:292+ if hasattr(self.draw, 'textbbox'):293+ left, top, right, bottom = self.draw.textbbox((0, 0), string)294+ size = (right - left, bottom - top)295+ else:296+ size = self.draw.textsize(string, font=None)297+298+ font_ratio = font.size * 1.0 / FontMap.BASE_FONTSIZE299+ size = Size(int(size[0] * font_ratio),300+ int(size[1] * font_ratio))301+ else:302+ if hasattr(ttfont, 'getbbox'):303+ left, top, right, bottom = ttfont.getbbox(string)304+ size = Size(right - left, bottom - top)305+ else:306+ size = Size(*ttfont.getsize(string))307+308+ return size309+310+ def text(self, xy, string, font, **kwargs):311+ fill = kwargs.get('fill')312+ ttfont = ttfont_for(font)313+314+ if ttfont is None:315+ if self.scale_ratio == 1 and font.size == FontMap.BASE_FONTSIZE:316+ self.draw.text(xy, string, fill=fill)317+ else:318+ if hasattr(self.draw, 'textbbox'):319+ left, top, right, bottom = self.draw.textbbox((0, 0), string)320+ size = (right - left, bottom - top)321+ else:322+ size = self.draw.textsize(string)323+ image = Image.new('RGBA', size)324+ draw = ImageDraw.Draw(image)325+ draw.text((0, 0), string, fill=fill)326+ del draw327+328+ basesize = (size[0] * self.scale_ratio,329+ size[1] * self.scale_ratio)330+ text_image = image.resize(basesize, Image.Resampling.LANCZOS)331+ self.paste(text_image, xy, text_image)332+ else:333+ if hasattr(ttfont, 'getbbox'):334+ left, top, right, bottom = ttfont.getbbox(string)335+ size = (right - left, bottom - top)336+ else:337+ size = ttfont.getsize(string)338+339+ # Generate mask to support BDF(bitmap font)340+ mask = Image.new('1', size)341+ draw = ImageDraw.Draw(mask)342+ draw.text((0, 0), string, fill='white', font=ttfont)343+344+ # Rendering text345+ filler = Image.new('RGB', size, fill)346+ self.paste(filler, xy, mask)347+348+ def textarea(self, box, string, font, **kwargs):349+ if 'rotate' in kwargs and kwargs['rotate'] != 0:350+ angle = 360 - int(kwargs['rotate']) % 360351+ del kwargs['rotate']352+353+ if angle in (90, 270):354+ _box = Box(0, 0, box.height, box.width)355+ else:356+ _box = box357+358+ text = ImageDrawEx(None, parent=self, transparency=True)359+ text.set_canvas_size(_box.size)360+ textbox = Box(0, 0, _box.width, _box.height)361+ text.textarea(textbox, string, font, **kwargs)362+363+ filler = Image.new('RGB', box.size, kwargs.get('fill'))364+365+ mask = text._image.rotate(angle, expand=True)366+ if mask.size != filler.size:367+ # Image.rotate(expand=True) of Pillow earlier than368+ # 3.3.0 (including 2.x) returns image object with369+ # unexpected size: for example, rotating 10x20 by 270370+ # causes not 20x10 but 21x11.371+ # Therefore, crop rotated image in order to make it372+ # match against size of "filler".373+ mask = mask.crop((0, 0, box.width, box.height))374+375+ self.paste(filler, box.topleft, mask)376+ return377+378+ lines = self.textfolder(box, string, font, **kwargs)379+380+ if kwargs.get('outline'):381+ outline = kwargs.get('outline')382+ self.rectangle(lines.outlinebox, fill='white', outline=outline)383+384+ rendered = False385+ for string, xy in lines.lines:386+ self.text(xy, string, font, **kwargs)387+ rendered = True388+389+ if not rendered and font.size > 0:390+ _font = font.duplicate()391+ _font.size = int(font.size * 0.8)392+ self.textarea(box, string, _font, **kwargs)393+394+ def image(self, box, url):395+ if not box.width or not box.height:396+ # resizing image into "0 width and/or height" is meaningless397+ return398+399+ try:400+ image = images.open(url, mode='pillow')401+402+ # resize image.403+ w = min([box.width, image.size[0] * self.scale_ratio])404+ h = min([box.height, image.size[1] * self.scale_ratio])405+ image.thumbnail((w, h), Image.Resampling.LANCZOS)406+407+ # centering image.408+ w, h = image.size409+ if box.width > w:410+ x = box[0] + (box.width - w) // 2411+ else:412+ x = box[0]413+414+ if box.height > h:415+ y = box[1] + (box.height - h) // 2416+ else:417+ y = box[1]418+419+ if image.mode == 'P':420+ # convert P to RGBA to masking transparent pixels421+ image = image.convert('RGBA')422+423+ if image.mode == 'RGBA':424+ self.paste(image, (round(x), round(y)), mask=image)425+ else:426+ self.paste(image, (round(x), round(y)))427+ except IOError:428+ pass429+430+ def save(self, filename, size, _format):431+ if filename:432+ self.filename = filename433+434+ if size is None:435+ x = int(self._image.size[0] / self.scale_ratio)436+ y = int(self._image.size[1] / self.scale_ratio)437+ size = (x, y)438+439+ self._image.thumbnail(size, Image.Resampling.LANCZOS)440+441+ if self.filename:442+ self._image.save(self.filename, _format)443+ image = None444+ else:445+ from io import BytesIO446+ tmp = BytesIO()447+ self._image.save(tmp, _format)448+ image = tmp.getvalue()449+450+ return image451+452+453+def blurred(fn):454+ PADDING = 16455+456+ def get_shape_box(*args):457+ if fn.__name__ == 'polygon':458+ xlist = [pt.x for pt in args[0]]459+ ylist = [pt.y for pt in args[0]]460+ return Box(min(xlist), min(ylist), max(xlist), max(ylist))461+ else:462+ return args[0]463+464+ def get_abs_coordinate(box, *args):465+ dx = box.x1 - PADDING466+ dy = box.y1 - PADDING467+ if fn.__name__ == 'polygon':468+ return [pt.shift(-dx, -dy) for pt in args[0]]469+ else:470+ return box.shift(-dx, -dy)471+472+ def create_shadow(self, size, *args, **kwargs):473+ drawer = ImageDrawExBase(self.filename, transparency=True)474+ drawer.set_canvas_size(size)475+ getattr(drawer, fn.__name__)(*args, **kwargs)476+477+ for _ in range(15):478+ drawer._image = drawer._image.filter(ImageFilter.SMOOTH_MORE)479+480+ return drawer._image481+482+ @wraps(fn)483+ def func(self, *args, **kwargs):484+ args = list(args)485+486+ if kwargs.get('filter') not in ('blur', 'transp-blur'):487+ return fn(self, *args, **kwargs)488+ else:489+ box = get_shape_box(*args)490+ args[0] = get_abs_coordinate(box, *args)491+492+ size = Size(box.width + PADDING * 2, box.height + PADDING * 2)493+ shadow = create_shadow(self, size, *args, **kwargs)494+ xy = XY(box.x1 - PADDING, box.y1 - PADDING)495+ self.paste(shadow, xy, shadow)496+497+ return func498+499+500+class ImageDrawEx(ImageDrawExBase):501+ @blurred502+ def ellipse(self, box, **kwargs):503+ super(ImageDrawEx, self).ellipse(box, **kwargs)504+505+ @blurred506+ def rectangle(self, box, **kwargs):507+ super(ImageDrawEx, self).rectangle(box, **kwargs)508+509+ @blurred510+ def polygon(self, xy, **kwargs):511+ super(ImageDrawEx, self).polygon(xy, **kwargs)512+513+514+def setup(self):515+ from blockdiag.imagedraw import install_imagedrawer516+ install_imagedrawer('png', ImageDrawEx)
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/simplesvg.py +242 -0
@@ -0,0 +1,242 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import re17+from io import StringIO18+19+20+def _escape(s):21+ if not isinstance(s, str):22+ s = str(s)23+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")24+25+26+def _quote(s):27+ return '"%s"' % _escape(s).replace('"', "&quot;")28+29+30+class base(object):31+ def __init__(self, *args, **kwargs):32+ self.text = None33+ self.elements = []34+ self.attributes = {}35+ for key, value in kwargs.items():36+ self.add_attribute(key, value)37+38+ def add_attribute(self, key, value):39+ setter = 'set_%s' % key40+ if hasattr(self, setter):41+ getattr(self, setter)(value)42+ else:43+ key = re.sub('_', '-', key)44+ self.attributes[key] = value45+46+ def addElement(self, element):47+ self.elements.append(element)48+49+ def set_text(self, text):50+ self.text = text51+52+ def to_xml(self, io, level=0):53+ clsname = self.__class__.__name__54+ indent = ' ' * level55+56+ io.write('%s<%s' % (indent, clsname))57+ for key in sorted(self.attributes):58+ value = self.attributes[key]59+ if value is not None:60+ io.write(' %s=%s' % (_escape(key), _quote(value)))61+62+ if self.elements == []:63+ if self.text is not None:64+ io.write(">%s</%s>\n" % (_escape(self.text), clsname))65+ else:66+ io.write(" />\n")67+ elif self.elements:68+ if self.text is not None:69+ io.write(">%s\n" % (_escape(self.text),))70+ else:71+ io.write(">\n")72+73+ for e in self.elements:74+ e.to_xml(io, level + 1)75+ io.write('%s</%s>\n' % (indent, clsname))76+77+78+class element(base):79+ def __init__(self, x, y, width=None, height=None, *args, **kwargs):80+ super(element, self).__init__(*args, **kwargs)81+ self.attributes['x'] = x82+ self.attributes['y'] = y83+ if width is not None:84+ self.attributes['width'] = width85+ if height is not None:86+ self.attributes['height'] = height87+88+89+class svg(base):90+ def __init__(self, x, y, width, height, **kwargs):91+ if kwargs.get('noviewbox'):92+ super(svg, self).__init__(width=(width - x), height=(height - y))93+ else:94+ viewbox = "%d %d %d %d" % (x, y, width, height)95+ super(svg, self).__init__(viewBox=viewbox)96+97+ self.nodoctype = kwargs.get('nodoctype', False)98+ self.add_attribute('xmlns', 'http://www.w3.org/2000/svg')99+100+ def to_xml(self):101+ io = StringIO()102+103+ if not self.nodoctype:104+ url = "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"105+ io.write("<?xml version='1.0' encoding='UTF-8'?>\n")106+ io.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "%s">\n' % url)107+108+ super(svg, self).to_xml(io)109+110+ return io.getvalue()111+112+113+class title(base):114+ def __init__(self, _title):115+ super(title, self).__init__(text=_title)116+117+118+class desc(base):119+ def __init__(self, _title):120+ super(desc, self).__init__(text=_title)121+122+123+class text(element):124+ def __init__(self, x, y, _text, **kwargs):125+ super(text, self).__init__(x, y, text=_text, **kwargs)126+127+128+class rect(element):129+ pass130+131+132+class ellipse(base):133+ def __init__(self, cx, cy, rx, ry, **kwargs):134+ super(ellipse, self).__init__(cx=cx, cy=cy, rx=rx, ry=ry, **kwargs)135+136+137+class image(element):138+ def __init__(self, uri, x, y, width, height, **kwargs):139+ super(image, self).__init__(x, y, width, height, **kwargs)140+ self.add_attribute('xlink:href', uri)141+142+143+class polygon(base):144+ def __init__(self, points, **kwargs):145+ xylist = " ".join('%d,%d' % pt for pt in points)146+ super(polygon, self).__init__(points=xylist, **kwargs)147+148+149+class path(base):150+ def __init__(self, data, **kwargs):151+ super(path, self).__init__(d=data, **kwargs)152+153+154+class pathdata:155+ def __init__(self, x=None, y=None):156+ self.path = []157+ if x is not None and y is not None:158+ self.move(x, y)159+160+ def closepath(self):161+ self.path.append('z')162+163+ def move(self, x, y):164+ self.path.append('M %s %s' % (x, y))165+166+ def relmove(self, x, y):167+ self.path.append('m %s %s' % (x, y))168+169+ def line(self, x, y):170+ self.path.append('L %s %s' % (x, y))171+172+ def relline(self, x, y):173+ self.path.append('l %s %s' % (x, y))174+175+ def hline(self, x):176+ self.path.append('H%s' % (x,))177+178+ def relhline(self, x):179+ self.path.append('h%s' % (x,))180+181+ def vline(self, y):182+ self.path.append('V%s' % (y,))183+184+ def relvline(self, y):185+ self.path.append('v%s' % (y,))186+187+ def bezier(self, x1, y1, x2, y2, x, y):188+ self.path.append('C%s,%s %s,%s %s,%s' % (x1, y1, x2, y2, x, y))189+190+ def relbezier(self, x1, y1, x2, y2, x, y):191+ self.path.append('c%s,%s %s,%s %s,%s' % (x1, y1, x2, y2, x, y))192+193+ def smbezier(self, x2, y2, x, y):194+ self.path.append('S%s,%s %s,%s' % (x2, y2, x, y))195+196+ def relsmbezier(self, x2, y2, x, y):197+ self.path.append('s%s,%s %s,%s' % (x2, y2, x, y))198+199+ def qbezier(self, x1, y1, x, y):200+ self.path.append('Q%s,%s %s,%s' % (x1, y1, x, y))201+202+ def qrelbezier(self, x1, y1, x, y):203+ self.path.append('q%s,%s %s,%s' % (x1, y1, x, y))204+205+ def smqbezier(self, x, y):206+ self.path.append('T%s %s' % (x, y))207+208+ def relsmqbezier(self, x, y):209+ self.path.append('t%s %s' % (x, y))210+211+ def ellarc(self, rx, ry, xrot, laf, sf, x, y):212+ self.path.append('A%s,%s %s %s %s %s %s' %213+ (rx, ry, xrot, laf, sf, x, y))214+215+ def relellarc(self, rx, ry, xrot, laf, sf, x, y):216+ self.path.append('a%s,%s %s %s %s %s %s' %217+ (rx, ry, xrot, laf, sf, x, y))218+219+ def __repr__(self):220+ return ' '.join(self.path)221+222+223+class defs(base):224+ pass225+226+227+class g(base):228+ pass229+230+231+class a(base):232+ pass233+234+235+class filter(element):236+ def __init__(self, x, y, width, height, **kwargs):237+ super(filter, self).__init__(x, y, width, height, **kwargs)238+239+240+def svgclass(name):241+ """ svg class generating function """242+ return type(name, (base,), {})
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/svg.py +321 -0
@@ -0,0 +1,321 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import os17+import re18+from base64 import b64encode19+20+from PIL.Image import Image21+22+from blockdiag.imagedraw import base as _base23+from blockdiag.imagedraw.simplesvg import (a, defs, desc, ellipse, filter, g,24+ image, path, pathdata, polygon,25+ rect, svg, svgclass, text, title)26+from blockdiag.imagedraw.utils import memoize27+from blockdiag.imagedraw.utils.ellipse import endpoints as ellipse_endpoints28+from blockdiag.utils import XY, Box, images, is_Pillow_available29+30+feGaussianBlur = svgclass('feGaussianBlur')31+32+33+def rgb(color):34+ if isinstance(color, tuple):35+ color = 'rgb(%d,%d,%d)' % color36+37+ return color38+39+40+def style(name):41+ if name == 'blur':42+ value = "filter:url(#filter_blur)"43+ elif name == 'transp-blur':44+ value = "filter:url(#filter_blur);opacity:0.7;fill-opacity:1"45+ else:46+ value = None47+48+ return value49+50+51+def dasharray(pattern, thick):52+ if thick is None:53+ thick = 154+55+ if pattern == 'dotted':56+ value = 2 * thick57+ elif pattern == 'dashed':58+ value = 4 * thick59+ elif pattern == 'none':60+ value = "%d %d" % (0, 65535 * thick)61+ elif re.search(r'^\d+(,\d+)*$', pattern or ""):62+ lengths = [int(n) * thick for n in pattern.split(",")]63+ value = " ".join(str(n) for n in lengths)64+ else:65+ value = None66+67+ return value68+69+70+def drawing_params(kwargs):71+ params = {}72+73+ if 'style' in kwargs:74+ params['stroke_dasharray'] = dasharray(kwargs.get('style'),75+ kwargs.get('thick'))76+77+ if 'filter' in kwargs:78+ params['style'] = style(kwargs.get('filter'))79+80+ return params81+82+83+class SVGImageDrawElement(_base.ImageDraw):84+ self_generative_methods = ['group', 'anchor']85+ supported_path = True86+ baseline_text_rendering = True87+88+ def __init__(self, svg, parent=None):89+ self.svg = svg90+91+ def path(self, pd, **kwargs):92+ fill = kwargs.get('fill')93+ outline = kwargs.get('outline')94+95+ p = path(pd, fill=rgb(fill), stroke=rgb(outline),96+ **drawing_params(kwargs))97+ self.svg.addElement(p)98+99+ def rectangle(self, box, **kwargs):100+ thick = kwargs.get('thick')101+ fill = kwargs.get('fill', 'none')102+ outline = kwargs.get('outline')103+104+ r = rect(box.x, box.y, box.width, box.height,105+ fill=rgb(fill), stroke=rgb(outline),106+ stroke_width=thick, **drawing_params(kwargs))107+ self.svg.addElement(r)108+109+ @memoize110+ def textlinesize(self, string, font, **kwargs):111+ if is_Pillow_available():112+ if not hasattr(self, '_pil_drawer'):113+ from blockdiag.imagedraw import png114+ self._pil_drawer = png.ImageDrawEx(None)115+116+ return self._pil_drawer.textlinesize(string, font)117+ else:118+ from blockdiag.imagedraw.utils import textsize119+ return textsize(string, font)120+121+ def text(self, point, string, font, **kwargs):122+ fill = kwargs.get('fill')123+124+ size = self.textlinesize(string, font)125+ point = point.shift(size.width / 2)126+ t = text(point.x, point.y, string, fill=rgb(fill),127+ font_family=font.generic_family, font_size=font.size,128+ font_weight=font.weight, font_style=font.style,129+ text_anchor='middle', textLength=size.width)130+ self.svg.addElement(t)131+132+ def textarea(self, box, string, font, **kwargs):133+ if 'rotate' in kwargs and kwargs['rotate'] != 0:134+ self.rotated_textarea(box, string, font, **kwargs)135+ else:136+ lines = self.textfolder(box, string, font, **kwargs)137+138+ if kwargs.get('outline'):139+ outline = kwargs.get('outline')140+ self.rectangle(lines.outlinebox, fill='white', outline=outline)141+142+ rendered = False143+ for string, point in lines.lines:144+ self.text(point, string, font, **kwargs)145+ rendered = True146+147+ if not rendered and font.size > 0:148+ _font = font.duplicate()149+ _font.size = int(font.size * 0.8)150+ self.textarea(box, string, _font, **kwargs)151+152+ def rotated_textarea(self, box, string, font, **kwargs):153+ angle = int(kwargs['rotate']) % 360154+ del kwargs['rotate']155+156+ if angle in (90, 270):157+ _box = Box(box[0], box[1],158+ box[0] + box.height, box[1] + box.width)159+ if angle == 90:160+ _box = _box.shift(x=box.width)161+ elif angle == 270:162+ _box = _box.shift(y=box.height)163+ elif angle == 180:164+ _box = Box(box[2], box[3],165+ box[2] + box.width, box[3] + box.height)166+ else:167+ _box = Box(box[0], box[1],168+ box[0] + box.width, box[1] + box.height)169+170+ rotate = "rotate(%d,%d,%d)" % (angle, _box[0], _box[1])171+ group = g(transform="%s" % rotate)172+ self.svg.addElement(group)173+174+ elem = SVGImageDrawElement(group, self)175+ elem.textarea(_box, string, font, **kwargs)176+177+ def line(self, points, **kwargs):178+ fill = kwargs.get('fill')179+ thick = kwargs.get('thick')180+181+ pd = pathdata(points[0].x, points[0].y)182+ for pt in points[1:]:183+ pd.line(pt.x, pt.y)184+185+ p = path(pd, fill="none", stroke=rgb(fill),186+ stroke_width=thick, **drawing_params(kwargs))187+ self.svg.addElement(p)188+189+ def arc(self, box, start, end, **kwargs):190+ fill = kwargs.get('fill')191+192+ w = box.width / 2193+ h = box.height / 2194+195+ if start > end:196+ end += 360197+198+ endpoints = ellipse_endpoints(1, w, h, start, end)199+ pt1 = XY(box.x + w + round(endpoints[0].x, 0),200+ box.y + h + round(endpoints[0].y, 0))201+ pt2 = XY(box.x + w + round(endpoints[1].x, 0),202+ box.y + h + round(endpoints[1].y, 0))203+204+ if end - start > 180:205+ largearc = 1206+ else:207+ largearc = 0208+209+ pd = pathdata(pt1[0], pt1[1])210+ pd.ellarc(w, h, 0, largearc, 1, pt2[0], pt2[1])211+ p = path(pd, fill="none", stroke=rgb(fill),212+ **drawing_params(kwargs))213+ self.svg.addElement(p)214+215+ def ellipse(self, box, **kwargs):216+ fill = kwargs.get('fill')217+ outline = kwargs.get('outline')218+219+ w = box.width / 2220+ h = box.height / 2221+ pt = box.center222+223+ e = ellipse(pt.x, pt.y, w, h, fill=rgb(fill),224+ stroke=rgb(outline), **drawing_params(kwargs))225+ self.svg.addElement(e)226+227+ def polygon(self, points, **kwargs):228+ fill = kwargs.get('fill')229+ outline = kwargs.get('outline')230+231+ pg = polygon(points, fill=rgb(fill), stroke=rgb(outline),232+ **drawing_params(kwargs))233+ self.svg.addElement(pg)234+235+ def image(self, box, url):236+ if hasattr(url, 'read'):237+ url = "data:;base64," + b64encode(url.read()).decode('utf-8')238+ else:239+ if isinstance(url, Image):240+ ext = None241+ else:242+ ext = os.path.splitext(url)[1].lower()243+244+ if ext not in ('.jpg', '.png', '.gif'):245+ stream = None246+ try:247+ stream = images.open(url, mode='png')248+ url = "data:;base64," + b64encode(stream.read()).decode('utf-8')249+ except IOError:250+ url = None251+ finally:252+ if stream:253+ stream.close()254+255+ im = image(url, box.x1, box.y1, box.width, box.height)256+ self.svg.addElement(im)257+258+ def anchor(self, url):259+ a_node = a(url)260+ a_node.add_attribute('xlink:href', url)261+ self.svg.addElement(a_node)262+263+ return SVGImageDrawElement(a_node, self)264+265+ def group(self):266+ group = g()267+ self.svg.addElement(group)268+269+ return SVGImageDrawElement(group, self)270+271+272+class SVGImageDraw(SVGImageDrawElement):273+ def __init__(self, filename, **kwargs):274+ super(SVGImageDraw, self).__init__(None)275+276+ self.filename = filename277+ self.options = kwargs278+ self.set_canvas_size((0, 0))279+280+ def set_canvas_size(self, size):281+ self.svg = svg(0, 0, size[0], size[1], **self.options)282+ uri = 'http://www.inkscape.org/namespaces/inkscape'283+ self.svg.add_attribute('xmlns:inkspace', uri)284+ uri = 'http://www.w3.org/1999/xlink'285+ self.svg.add_attribute('xmlns:xlink', uri)286+287+ # inkspace's Gaussian filter288+ if self.options.get('style') != 'blur':289+ fgb = feGaussianBlur(id='feGaussianBlur3780', stdDeviation=4.2)290+ fgb.add_attribute('inkspace:collect', 'always')291+ f = filter(-0.07875, -0.252, 1.1575, 1.504, id='filter_blur')292+ f.add_attribute('inkspace:collect', 'always')293+ f.addElement(fgb)294+ d = defs(id='defs_block')295+ d.addElement(f)296+ self.svg.addElement(d)297+298+ self.svg.addElement(title('blockdiag'))299+ self.svg.addElement(desc(self.options.get('code')))300+301+ def save(self, filename, size, _format):302+ # Ignore format parameter; compatibility for ImageDrawEx.303+304+ if filename:305+ self.filename = filename306+307+ if size:308+ self.svg.attributes['width'] = size[0]309+ self.svg.attributes['height'] = size[1]310+311+ image = self.svg.to_xml()312+313+ if self.filename:314+ open(self.filename, 'wb').write(image.encode('utf-8'))315+316+ return image317+318+319+def setup(self):320+ from blockdiag.imagedraw import install_imagedrawer321+ install_imagedrawer('svg', SVGImageDraw)
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/textfolder.py +302 -0
@@ -0,0 +1,302 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import re17+18+from blockdiag.utils import XY, Box, Size19+20+21+def splitlabel(string):22+ """Split text to lines as generator.23+ Every line will be stripped.24+ If text includes characters "\n", treat as line separator.25+ """26+ string = re.sub(r'^\s*', '', string)27+ string = re.sub(r'\s*$', '', string)28+ string = re.sub(r'\xa5', '\\\\', string)29+ string = re.sub('(\\\\){2}', '\x00', string)30+ string = re.sub('\\\\n', '\n', string)31+ for line in string.splitlines():32+ yield re.sub('\x00', '\\\\', line).strip()33+34+35+def splittext(metrics, text, bound, measure='width'):36+ folded = []37+ if text == '':38+ folded.append(' ')39+40+ for i in range(len(text), 0, -1):41+ textsize = metrics.textsize(text[0:i])42+43+ if getattr(textsize, measure) <= bound:44+ folded.append(text[0:i])45+ if text[i:]:46+ folded += splittext(metrics, text[i:], bound, measure)47+ break48+49+ return folded50+51+52+def truncate_text(metrics, text, bound, measure='width'):53+ for i in range(len(text), 0, -1):54+ textsize = metrics.textsize(text[0:i] + ' ...')55+56+ if getattr(textsize, measure) <= bound:57+ return text[0:i] + ' ...'58+59+ return text60+61+62+def get(*args, **kwargs):63+ if kwargs.get('orientation') == 'vertical':64+ return VerticalTextFolder(*args, **kwargs)65+ else:66+ return HorizontalTextFolder(*args, **kwargs)67+68+69+class VerticalTextFolder(object):70+ def __init__(self, drawer, box, string, font, **kwargs):71+ self.drawer = drawer72+ self.box = box73+ self.string = string74+ self.font = font75+ self.scale = 176+ self.halign = kwargs.get('halign', 'center')77+ self.valign = kwargs.get('valign', 'center')78+ self.padding = kwargs.get('padding', 8)79+ self.line_spacing = kwargs.get('line_spacing', 2)80+81+ if kwargs.get('adjustBaseline'):82+ self.adjustBaseline = True83+ else:84+ self.adjustBaseline = False85+86+ self._result = self._lines()87+88+ def textsize(self, text, scaled=False):89+ if isinstance(text, str):90+ size = [self.drawer.textlinesize(c, self.font) for c in text]91+ width = max(s.width for s in size)92+ height = (sum(s.height for s in size) +93+ self.line_spacing * (len(text) - 1))94+95+ textsize = Size(width, height)96+ else:97+ if text:98+ size = [self.textsize(s) for s in text]99+ height = max(s.height for s in size)100+ width = (sum(s.width for s in size) +101+ self.line_spacing * (len(text) - 1))102+103+ textsize = Size(width, height)104+ else:105+ textsize = Size(0, 0)106+107+ if scaled:108+ textsize = Size(textsize.width * self.scale,109+ textsize.height * self.scale)110+111+ return textsize112+113+ @property114+ def lines(self):115+ textsize = self.textsize(self._result, scaled=True)116+117+ dx, _ = self.box.get_padding_for(textsize, halign=self.halign,118+ padding=self.padding)119+120+ width = self.box.width - dx + self.line_spacing121+ base_xy = XY(self.box.x1, self.box.y1)122+ for string in self._result:123+ textsize = self.textsize(string, scaled=True)124+ _, dy = self.box.get_padding_for(textsize, valign=self.valign,125+ padding=self.line_spacing)126+127+ height = dy128+ width -= textsize.width + self.line_spacing129+ for char in string:130+ charsize = self.textsize(char, scaled=True)131+132+ if self.adjustBaseline:133+ draw_xy = base_xy.shift(width, height + charsize.height)134+ else:135+ draw_xy = base_xy.shift(width, height)136+137+ yield char, draw_xy138+139+ height += charsize.height + self.line_spacing140+141+ @property142+ def outlinebox(self):143+ corners = []144+ for string, xy in self.lines:145+ textsize = self.textsize(string)146+ width = textsize[0] * self.scale147+ height = textsize[1] * self.scale148+149+ if self.adjustBaseline:150+ xy = XY(xy.x, xy.y - textsize[1])151+152+ corners.append(xy)153+ corners.append(XY(xy.x + width, xy.y + height))154+155+ if corners:156+ box = Box(min(p.x for p in corners) - self.padding,157+ min(p.y for p in corners) - self.line_spacing,158+ max(p.x for p in corners) + self.padding,159+ max(p.y for p in corners) + self.line_spacing)160+ else:161+ box = Box(self.box[0], self.box[1], self.box[0], self.box[1])162+163+ return box164+165+ def _lines(self):166+ lines = []167+ measure = 'height'168+ maxwidth, maxheight = self.box.size169+170+ width = 0171+ finished = False172+ for line in splitlabel(self.string):173+ for folded in splittext(self, line, maxheight, measure):174+ textsize = self.textsize(folded)175+176+ if width + textsize.width + self.line_spacing < maxwidth:177+ lines.append(folded)178+ width += textsize.width + self.line_spacing179+ elif len(lines) > 0:180+ lines[-1] = truncate_text(self, lines[-1],181+ maxheight, measure)182+ finished = True183+ break184+185+ if finished:186+ break187+188+ return lines189+190+191+class HorizontalTextFolder(object):192+ def __init__(self, drawer, box, string, font, **kwargs):193+ self.drawer = drawer194+ self.box = box195+ self.string = string196+ self.font = font197+ self.scale = 1198+ self.halign = kwargs.get('halign', 'center')199+ self.valign = kwargs.get('valign', 'center')200+ self.padding = kwargs.get('padding', 8)201+ self.line_spacing = kwargs.get('line_spacing', 2)202+203+ if kwargs.get('adjustBaseline'):204+ self.adjustBaseline = True205+ else:206+ self.adjustBaseline = False207+208+ self._result = self._lines()209+210+ def textsize(self, text, scaled=False):211+ if isinstance(text, str):212+ textsize = self.drawer.textlinesize(text, self.font)213+ else:214+ if text:215+ size = [self.textsize(s) for s in text]216+ width = max(s.width for s in size)217+ height = (sum(s.height for s in size) +218+ self.line_spacing * (len(text) - 1))219+220+ textsize = Size(width, height)221+ else:222+ textsize = Size(0, 0)223+224+ if scaled:225+ textsize = Size(textsize.width * self.scale,226+ textsize.height * self.scale)227+228+ return textsize229+230+ @property231+ def lines(self):232+ textsize = self.textsize(self._result, scaled=True)233+234+ _, dy = self.box.get_padding_for(textsize, valign=self.valign,235+ padding=self.line_spacing)236+237+ height = dy238+ base_xy = XY(self.box.x1, self.box.y1)239+ for string in self._result:240+ textsize = self.textsize(string, scaled=True)241+ dx, _ = self.box.get_padding_for(textsize, halign=self.halign,242+ padding=self.padding)243+244+ if self.adjustBaseline:245+ draw_xy = base_xy.shift(dx, height + textsize.height)246+ else:247+ draw_xy = base_xy.shift(dx, height)248+249+ yield string, draw_xy250+251+ height += textsize.height + self.line_spacing252+253+ @property254+ def outlinebox(self):255+ corners = []256+ for string, xy in self.lines:257+ textsize = self.textsize(string)258+ width = textsize[0] * self.scale259+ height = textsize[1] * self.scale260+261+ if self.adjustBaseline:262+ xy = XY(xy.x, xy.y - textsize[1])263+264+ corners.append(xy)265+ corners.append(XY(xy.x + width, xy.y + height))266+267+ if corners:268+ box = Box(min(p.x for p in corners) - self.padding,269+ min(p.y for p in corners) - self.line_spacing,270+ max(p.x for p in corners) + self.padding,271+ max(p.y for p in corners) + self.line_spacing)272+ else:273+ box = Box(self.box[0], self.box[1], self.box[0], self.box[1])274+275+ return box276+277+ def _lines(self):278+ lines = []279+ measure = 'width'280+ maxwidth, maxheight = self.box.size281+282+ height = 0283+ finished = False284+ for line in splitlabel(self.string):285+ for folded in splittext(self, line, maxwidth, measure):286+ textsize = self.textsize(folded)287+288+ if height + textsize.height + self.line_spacing < maxheight:289+ lines.append(folded)290+ height += textsize.height + self.line_spacing291+ else:292+ if len(lines) > 0:293+ lines[-1] = truncate_text(self, lines[-1],294+ maxwidth, measure)295+296+ finished = True297+ break298+299+ if finished:300+ break301+302+ return lines
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/utils/__init__.py +69 -0
@@ -0,0 +1,69 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import math17+import unicodedata18+from functools import wraps19+20+from blockdiag.utils import Size21+22+23+def is_zenkaku(char):24+ """Detect given character is Japanese ZENKAKU character"""25+ char_width = unicodedata.east_asian_width(char)26+ return char_width in "WFA"27+28+29+def zenkaku_len(string):30+ """Count Japanese ZENKAKU characters from string"""31+ return len([x for x in string if is_zenkaku(x)])32+33+34+def hankaku_len(string):35+ """Count non Japanese ZENKAKU characters from string"""36+ return len([x for x in string if not is_zenkaku(x)])37+38+39+def string_width(string):40+ """Measure rendering width of string.41+ Count ZENKAKU-character as 2-point and non ZENKAKU-character as 1-point42+ """43+ widthmap = {'Na': 1, 'N': 1, 'H': 1, 'W': 2, 'F': 2, 'A': 2}44+ return sum(widthmap[unicodedata.east_asian_width(c)] for c in string)45+46+47+def textsize(string, font):48+ """Measure rendering size (width and height) of line.49+ Returned size will not be exactly as rendered text size,50+ Because this method does not use fonts to measure size.51+ """52+ width = (zenkaku_len(string) * font.size +53+ hankaku_len(string) * font.size * 0.55)54+55+ return Size(int(math.ceil(width)), font.size)56+57+58+def memoize(fn):59+ fn.cache = {}60+61+ @wraps(fn)62+ def func(*args, **kwargs):63+ key = str(args) + str(kwargs)64+ if key not in fn.cache:65+ fn.cache[key] = fn(*args, **kwargs)66+67+ return fn.cache[key]68+69+ return func
A diagramzip-python-units/blockdiag-family/src/blockdiag/imagedraw/utils/ellipse.py +61 -0
@@ -0,0 +1,61 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+import math17+18+from blockdiag.utils import XY19+20+DIVISION = 1000.021+CYCLE = 1022+23+24+def _angles(du, a, b, start, end):25+ phi = (start / 180.0) * math.pi26+ while phi <= (end / 180.0) * math.pi:27+ yield phi28+ phi += du / math.sqrt((a * math.sin(phi)) ** 2 +29+ (b * math.cos(phi)) ** 2)30+31+32+def _coordinates(du, a, b, start, end):33+ for angle in _angles(du, a, b, start, end):34+ yield (a * math.cos(angle), b * math.sin(angle))35+36+37+def endpoints(du, a, b, start, end):38+ pt1 = next(iter(_coordinates(du, a, b, start, start + 1)))39+ pt2 = next(iter(_coordinates(du, a, b, end, end + 1)))40+41+ return [XY(*pt1), XY(*pt2)]42+43+44+def dots(box, cycle, start=0, end=360):45+ # calcrate rendering pattern from cycle46+ base = 047+ rendered = []48+ for index in range(0, len(cycle), 2):49+ i, j = cycle[index:index + 2]50+ for n in range(base * 2, (base + i) * 2):51+ rendered.append(n)52+ base += i + j53+54+ a = float(box.width) / 255+ b = float(box.height) / 256+ du = 157+ _max = sum(cycle) * 258+ center = box.center59+ for i, coord in enumerate(_coordinates(du, a, b, start, end)):60+ if i % _max in rendered:61+ yield XY(center.x + coord[0], center.y + coord[1])
A diagramzip-python-units/blockdiag-family/src/blockdiag/metrics.py +1067 -0
@@ -0,0 +1,1067 @@
1+# -*- coding: utf-8 -*-2+# Copyright 2011 Takeshi KOMIYA3+#4+# Licensed under the Apache License, Version 2.0 (the "License");5+# you may not use this file except in compliance with the License.6+# You may obtain a copy of the License at7+#8+# http://www.apache.org/licenses/LICENSE-2.09+#10+# Unless required by applicable law or agreed to in writing, software11+# distributed under the License is distributed on an "AS IS" BASIS,12+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13+# See the License for the specific language governing permissions and14+# limitations under the License.15+16+from __future__ import division17+18+import copy19+from collections import defaultdict20+21+from blockdiag import noderenderer22+from blockdiag.elements import DiagramNode23+from blockdiag.utils import XY, Box, Size24+from blockdiag.utils.fontmap import FontInfo, FontMap25+26+cellsize = 827+28+29+class EdgeLines(object):30+ def __init__(self):31+ self.xy = None32+ self.stroking = False33+ self.polylines = []34+35+ def moveTo(self, x, y=None):36+ self.stroking = False37+ if y is None:38+ self.xy = x39+ else:40+ self.xy = XY(x, y)41+42+ def lineTo(self, x, y=None):43+ if y is None:44+ elem = x45+ else:46+ elem = XY(x, y)47+48+ if self.stroking is False:49+ self.stroking = True50+ polyline = []51+ if self.xy:52+ polyline.append(self.xy)53+ self.polylines.append(polyline)54+55+ if len(self.polylines[-1]) > 0:56+ if self.polylines[-1][-1] == elem:57+ return58+59+ self.polylines[-1].append(elem)60+61+ def lines(self):62+ lines = []63+ for line in self.polylines:64+ start = line[0]65+ for elem in list(line[1:]):66+ lines.append((start, elem))67+ start = elem68+69+ return lines70+71+72+class AutoScaler(object):73+ def __init__(self, subject, scale_ratio):74+ self.subject = subject75+ self.scale_ratio = scale_ratio76+77+ def __getattr__(self, name):78+ ratio = self.scale_ratio79+ return self.scale(getattr(self.subject, name), ratio)80+81+ def __getitem__(self, name):82+ ratio = self.scale_ratio83+ return self.scale(self.subject[name], ratio)84+85+ @classmethod86+ def scale(cls, value, ratio):87+ if not callable(value):88+ return cls._scale(value, ratio)89+ else:90+ def _(*args, **kwargs):91+ ret = value(*args, **kwargs)92+ return cls._scale(ret, ratio)93+94+ return _95+96+ @classmethod97+ def _scale(cls, value, ratio):98+ if ratio == 1:99+ return value100+101+ klass = value.__class__102+ if klass == XY:103+ ret = XY(value.x * ratio, value.y * ratio)104+ elif klass == Size:105+ ret = Size(value.width * ratio, value.height * ratio)106+ elif klass == Box:107+ ret = Box(value[0] * ratio, value[1] * ratio,108+ value[2] * ratio, value[3] * ratio)109+ elif klass == tuple:110+ ret = tuple([cls.scale(x, ratio) for x in value])111+ elif klass == list:112+ ret = [cls.scale(x, ratio) for x in value]113+ elif klass == EdgeLines:114+ ret = EdgeLines()115+ ret.polylines = cls.scale(value.polylines, ratio)116+ elif klass == FontInfo:117+ ret = FontInfo(value.familyname, value.path, value.size * ratio)118+ elif klass == int:119+ ret = value * ratio120+ elif klass == str:121+ ret = value122+ else:123+ ret = cls(value, ratio)124+125+ return ret126+127+ @property128+ def original_metrics(self):129+ return self.subject130+131+132+class DiagramMetrics(object):133+ cellsize = cellsize134+ edge_layout = 'normal'135+ node_padding = 4136+ line_spacing = 2137+ shadow_offset = XY(3, 6)138+ page_margin = XY(0, 0)139+ page_padding = [0, 0, 0, 0]140+ node_width = cellsize * 16141+ node_height = cellsize * 5142+ span_width = cellsize * 8143+ span_height = cellsize * 5144+145+ def __init__(self, diagram, drawer=None, fontmap=None):146+ self.drawer = drawer147+148+ if diagram.node_width is not None:149+ self.node_width = diagram.node_width150+151+ if diagram.node_height is not None:152+ self.node_height = diagram.node_height153+154+ if diagram.span_width is not None:155+ self.span_width = diagram.span_width156+157+ if diagram.span_height is not None:158+ self.span_height = diagram.span_height159+160+ if fontmap is not None:161+ self.fontmap = fontmap162+ else:163+ self.fontmap = FontMap()164+165+ if diagram.page_padding is not None:166+ self.page_padding = diagram.page_padding167+168+ if diagram.edge_layout is not None:169+ self.edge_layout = diagram.edge_layout170+171+ # setup spreadsheet172+ sheet = self.spreadsheet = SpreadSheetMetrics(self)173+ nodes = [n for n in diagram.traverse_nodes() if n.drawable]174+175+ node_width = self.node_width176+ for x in range(diagram.colwidth):177+ widths = [n.width for n in nodes if n.xy.x == x]178+ if widths:179+ width = max(n or node_width for n in widths)180+ sheet.set_node_width(x, width)181+182+ node_height = self.node_height183+ for y in range(diagram.colheight):184+ heights = [n.height for n in nodes if n.xy.y == y]185+ if heights:186+ height = max(n or node_height for n in heights)187+ sheet.set_node_height(y, height)188+189+ @property190+ def original_metrics(self):191+ return self192+193+ def shift(self, x, y):194+ metrics = copy.copy(self)195+ metrics.spreadsheet = copy.copy(self.spreadsheet)196+ metrics.spreadsheet.metrics = metrics197+ metrics.page_margin = XY(x, y)198+199+ return metrics200+201+ def textsize(self, string, font=None, width=65535):202+ return self.drawer.textsize(string, font, maxwidth=width)203+204+ def node(self, node):205+ renderer = noderenderer.get(node.shape)206+207+ if hasattr(renderer, 'render'):208+ return renderer(node, self)209+ else:210+ return self.cell(node)211+212+ def cell(self, node, use_padding=True):213+ return self.spreadsheet.node(node, use_padding)214+215+ def group(self, group):216+ return self.spreadsheet.node(group, use_padding=False)217+218+ def edge(self, edge):219+ if self.edge_layout == 'flowchart':220+ if edge.node1.group.orientation == 'landscape':221+ return FlowchartLandscapeEdgeMetrics(edge, self)222+ else:223+ return FlowchartPortraitEdgeMetrics(edge, self)224+ else:225+ if edge.node1.group.orientation == 'landscape':226+ return LandscapeEdgeMetrics(edge, self)227+ else:228+ return PortraitEdgeMetrics(edge, self)229+230+ def font_for(self, element):231+ return self.fontmap.find(element)232+233+ def pagesize(self, width, height):234+ return self.spreadsheet.pagesize(width, height)235+236+237+class SubMetrics(object):238+ def __getattr__(self, name):239+ # avoid recursion-error on Python 2.6240+ if 'metrics' not in self.__dict__:241+ raise AttributeError()242+243+ return getattr(self.metrics, name)244+245+246+class SpreadSheetMetrics(SubMetrics):247+ def __init__(self, metrics):248+ self.metrics = metrics249+ self.node_width = defaultdict(lambda: metrics.node_width)250+ self.node_height = defaultdict(lambda: metrics.node_height)251+ self.span_width = defaultdict(lambda: metrics.span_width)252+ self.span_height = defaultdict(lambda: metrics.span_height)253+254+ def set_node_width(self, x, width):255+ if (width is not None and 0 < width and256+ (x not in self.node_width or self.node_width[x] < width)):257+ self.node_width[x] = width258+259+ def set_node_height(self, y, height):260+ if (height is not None and 0 < height and261+ (y not in self.node_height or self.node_height[y] < height)):262+ self.node_height[y] = height263+264+ def set_span_width(self, x, width):265+ if (width is not None and 0 < width and266+ (x not in self.span_width or self.span_width[x] < width)):267+ self.span_width[x] = width268+269+ def add_span_width(self, x, width):270+ self.span_width[x] += width271+272+ def set_span_height(self, y, height):273+ if (height is not None and 0 < height and274+ (y not in self.span_height or self.span_height[y] < height)):275+ self.span_height[y] = height276+277+ def add_span_height(self, y, height):278+ self.span_height[y] += height279+280+ def node(self, node, use_padding=True):281+ x1, y1 = self._node_topleft(node, use_padding)282+ x2, y2 = self._node_bottomright(node, use_padding)283+284+ return NodeMetrics(self.metrics, x1, y1, x2, y2)285+286+ def _node_topleft(self, node, use_padding=True):287+ x, y = node.xy288+ margin = self.page_margin289+ padding = self.page_padding290+291+ node_width = sum(self.node_width[i] for i in range(x))292+ node_height = sum(self.node_height[i] for i in range(y))293+ span_width = sum(self.span_width[i] for i in range(x + 1))294+ span_height = sum(self.span_height[i] for i in range(y + 1))295+296+ if use_padding:297+ width = node.width or self.metrics.node_width298+ xdiff = (self.node_width[x] - width) // 2299+ if xdiff < 0:300+ xdiff = 0301+302+ height = node.height or self.metrics.node_height303+ ydiff = (self.node_height[y] - height) // 2304+ if ydiff < 0:305+ ydiff = 0306+ else:307+ xdiff = 0308+ ydiff = 0309+310+ x1 = margin.x + padding[3] + node_width + span_width + xdiff311+ y1 = margin.y + padding[0] + node_height + span_height + ydiff312+313+ return XY(x1, y1)314+315+ def _node_bottomright(self, node, use_padding=True):316+ x = node.xy.x + node.colwidth - 1317+ y = node.xy.y + node.colheight - 1318+ margin = self.page_margin319+ padding = self.page_padding320+321+ node_width = sum(self.node_width[i] for i in range(x + 1))322+ node_height = sum(self.node_height[i] for i in range(y + 1))323+ span_width = sum(self.span_width[i] for i in range(x + 1))324+ span_height = sum(self.span_height[i] for i in range(y + 1))325+326+ if use_padding:327+ width = node.width or self.metrics.node_width328+ xdiff = (self.node_width[x] - width) // 2329+ if xdiff < 0:330+ xdiff = 0331+332+ height = node.height or self.metrics.node_height333+ ydiff = (self.node_height[y] - height) // 2334+ if ydiff < 0:335+ ydiff = 0336+ else:337+ xdiff = 0338+ ydiff = 0339+340+ x2 = margin.x + padding[3] + node_width + span_width - xdiff341+ y2 = margin.y + padding[0] + node_height + span_height - ydiff342+343+ return XY(x2, y2)344+345+ def pagesize(self, width, height):346+ margin = self.metrics.page_margin347+ padding = self.metrics.page_padding348+349+ dummy = DiagramNode(None)350+ dummy.xy = XY(width - 1, height - 1)351+ x, y = self._node_bottomright(dummy, use_padding=False)352+ x_span = self.span_width[width]353+ y_span = self.span_height[height]354+ return Size(x + margin.x + padding[1] + x_span,355+ y + margin.y + padding[2] + y_span)356+357+358+class NodeMetrics(SubMetrics):359+ def __init__(self, metrics, x1, y1, x2, y2):360+ self.metrics = metrics361+ self._box = Box(x1, y1, x2, y2)362+363+ def __getattr__(self, name):364+ if hasattr(self._box, name):365+ return getattr(self._box, name)366+ else:367+ return getattr(self.metrics, name)368+369+ def __getitem__(self, key):370+ return self.box[key]371+372+ @property373+ def box(self):374+ return self._box375+376+ @property377+ def marginbox(self):378+ return Box(self._box.x1 - self.span_width // 8,379+ self._box.y1 - self.span_height // 4,380+ self._box.x2 + self.span_width // 8,381+ self._box.y2 + self.span_height // 4)382+383+ @property384+ def corebox(self):385+ return Box(self._box.x1 + self.node_padding,386+ self._box.y1 + self.node_padding,387+ self._box.x2 - self.node_padding * 2,388+ self._box.y2 - self.node_padding * 2)389+390+ @property391+ def grouplabelbox(self):392+ return Box(self._box.x1, self._box.y1 - self.span_height // 2,393+ self._box.x2, self._box.y1)394+395+396+class EdgeMetrics(SubMetrics):397+ def __init__(self, edge, metrics):398+ self.metrics = metrics399+ self.edge = edge400+401+ @property402+ def headshapes(self):403+ pass404+405+ @property406+ def _shaft(self):407+ pass408+409+ @property410+ def heads(self):411+ heads = []412+ head1, head2 = self.headshapes413+414+ if head1:415+ heads.append(self._head(self.edge.node1, head1))416+417+ if head2:418+ heads.append(self._head(self.edge.node2, head2))419+420+ return heads421+422+ def _head(self, node, direct):423+ head = []424+ cell = self.cellsize425+ node = self.node(node)426+427+ if direct == 'up':428+ xy = node.bottom429+ head.append(XY(xy.x, xy.y + 1))430+ head.append(XY(xy.x - cell // 2, xy.y + cell))431+ head.append(XY(xy.x, xy.y + cell * 2))432+ head.append(XY(xy.x + cell // 2, xy.y + cell))433+ head.append(XY(xy.x, xy.y + 1))434+ elif direct == 'down':435+ xy = node.top436+ head.append(XY(xy.x, xy.y - 1))437+ head.append(XY(xy.x - cell // 2, xy.y - cell))438+ head.append(XY(xy.x, xy.y - cell * 2))439+ head.append(XY(xy.x + cell // 2, xy.y - cell))440+ head.append(XY(xy.x, xy.y - 1))441+ elif direct == 'right':442+ xy = node.left443+ head.append(XY(xy.x - 1, xy.y))444+ head.append(XY(xy.x - cell, xy.y - cell // 2))445+ head.append(XY(xy.x - cell * 2, xy.y))446+ head.append(XY(xy.x - cell, xy.y + cell // 2))447+ head.append(XY(xy.x - 1, xy.y))448+ elif direct == 'left':449+ xy = node.right450+ head.append(XY(xy.x + 1, xy.y))451+ head.append(XY(xy.x + cell, xy.y - cell // 2))452+ head.append(XY(xy.x + cell * 2, xy.y))453+ head.append(XY(xy.x + cell, xy.y + cell // 2))454+ head.append(XY(xy.x + 1, xy.y))455+ elif direct == 'rup':456+ xy = node.bottom457+ head.append(XY(xy.x, xy.y + cell))458+ head.append(XY(xy.x - cell, xy.y + 1))459+ head.append(XY(xy.x, xy.y + 1 * 2))460+ head.append(XY(xy.x + cell, xy.y + 1))461+ head.append(XY(xy.x, xy.y + cell))462+ elif direct == 'rdown':463+ xy = node.top464+ head.append(XY(xy.x, xy.y - cell))465+ head.append(XY(xy.x - cell, xy.y - 1))466+ head.append(XY(xy.x, xy.y - 1 * 2))467+ head.append(XY(xy.x + cell, xy.y - 1))468+ head.append(XY(xy.x, xy.y - cell))469+ elif direct == 'rright':470+ xy = node.left471+ head.append(XY(xy.x - cell, xy.y))472+ head.append(XY(xy.x - 1, xy.y - cell))473+ head.append(XY(xy.x - 1 * 2, xy.y))474+ head.append(XY(xy.x - 1, xy.y + cell))475+ head.append(XY(xy.x - cell, xy.y))476+ elif direct == 'rleft':477+ xy = node.right478+ head.append(XY(xy.x + cell, xy.y))479+ head.append(XY(xy.x + 1, xy.y - cell))480+ head.append(XY(xy.x + 1 * 2, xy.y))481+ head.append(XY(xy.x + 1, xy.y + cell))482+ head.append(XY(xy.x + cell, xy.y))483+484+ if self.edge.hstyle not in ('composition', 'aggregation'):485+ head.pop(2)486+487+ return head488+489+ @property490+ def shaft(self):491+ cell = self.cellsize492+ lines = self._shaft493+ head1, head2 = self.headshapes494+495+ if head1:496+ pt = lines.polylines[0].pop(0)497+ if head1 == 'up':498+ lines.polylines[0].insert(0, XY(pt.x, pt.y + cell))499+ elif head1 == 'right':500+ lines.polylines[0].insert(0, XY(pt.x - cell, pt.y))501+ elif head1 == 'left':502+ lines.polylines[0].insert(0, XY(pt.x + cell, pt.y))503+ elif head1 == 'down':504+ lines.polylines[0].insert(0, XY(pt.x, pt.y - cell))505+ elif head1 == 'rup':506+ lines.polylines[0].insert(0, XY(pt.x, pt.y + cell))507+ elif head1 == 'rright':508+ lines.polylines[0].insert(0, XY(pt.x - cell, pt.y))509+ elif head1 == 'rleft':510+ lines.polylines[0].insert(0, XY(pt.x + cell, pt.y))511+ elif head1 == 'rdown':512+ lines.polylines[0].insert(0, XY(pt.x, pt.y - cell))513+514+ if head2:515+ pt = lines.polylines[-1].pop()516+ if head2 == 'up':517+ lines.polylines[-1].append(XY(pt.x, pt.y + cell))518+ elif head2 == 'right':519+ lines.polylines[-1].append(XY(pt.x - cell, pt.y))520+ elif head2 == 'left':521+ lines.polylines[-1].append(XY(pt.x + cell, pt.y))522+ elif head2 == 'down':523+ lines.polylines[-1].append(XY(pt.x, pt.y - cell))524+ elif head2 == 'rup':525+ lines.polylines[-1].append(XY(pt.x, pt.y + cell))526+ elif head2 == 'rright':527+ lines.polylines[-1].append(XY(pt.x - cell, pt.y))528+ elif head2 == 'rleft':529+ lines.polylines[-1].append(XY(pt.x + cell, pt.y))530+ elif head2 == 'rdown':531+ lines.polylines[-1].append(XY(pt.x, pt.y - cell))532+533+ return lines534+535+ @property536+ def labelbox(self):537+ pass538+539+540+class LandscapeEdgeMetrics(EdgeMetrics):541+ @property542+ def headshapes(self):543+ heads = []544+ _dir = self.edge.direction545+546+ if self.edge.dir in ('back', 'both'):547+ if _dir in ('left-up', 'left', 'same',548+ 'right-up', 'right', 'right-down'):549+ heads.append('left')550+ elif _dir == 'up':551+ if self.edge.skipped:552+ heads.append('left')553+ else:554+ heads.append('down')555+ elif _dir in ('left-down', 'down'):556+ if self.edge.skipped:557+ heads.append('left')558+ else:559+ heads.append('up')560+561+ if self.edge.hstyle in ('manyone', 'manymany'):562+ heads[-1] = 'r' + heads[-1]563+ else:564+ heads.append(None)565+566+ if self.edge.dir in ('forward', 'both'):567+ if _dir in ('right-up', 'right', 'right-down'):568+ heads.append('right')569+ elif _dir == 'up':570+ heads.append('up')571+ elif _dir in ('left-up', 'left', 'left-down', 'down', 'same'):572+ heads.append('down')573+574+ if self.edge.hstyle in ('onemany', 'manymany'):575+ heads[-1] = 'r' + heads[-1]576+ else:577+ heads.append(None)578+579+ return heads580+581+ @property582+ def _shaft(self):583+ span = XY(self.span_width, self.span_height)584+ _dir = self.edge.direction585+586+ node1 = self.node(self.edge.node1)587+ cell1 = self.cell(self.edge.node1, use_padding=False)588+ node2 = self.node(self.edge.node2)589+ cell2 = self.cell(self.edge.node2, use_padding=False)590+591+ shaft = EdgeLines()592+ if _dir == 'right':593+ shaft.moveTo(node1.right)594+595+ if self.edge.skipped:596+ shaft.lineTo(cell1.right.x + span.x // 2, cell1.right.y)597+ shaft.lineTo(cell1.right.x + span.x // 2,598+ cell1.bottomright.y + span.y // 2)599+ shaft.lineTo(cell2.left.x - span.x // 4,600+ cell2.bottomright.y + span.y // 2)601+ shaft.lineTo(cell2.left.x - span.x // 4, cell2.left.y)602+603+ shaft.lineTo(node2.left)604+605+ elif _dir == 'right-up':606+ shaft.moveTo(node1.right)607+608+ if self.edge.skipped:609+ shaft.lineTo(cell1.right.x + span.x // 2, cell1.right.y)610+ shaft.lineTo(cell1.right.x + span.x // 2,611+ cell2.bottomleft.y + span.y // 2)612+ shaft.lineTo(cell2.left.x - span.x // 4,613+ cell2.bottomleft.y + span.y // 2)614+ shaft.lineTo(cell2.left.x - span.x // 4, cell2.left.y)615+ else:616+ shaft.lineTo(cell2.left.x - span.x // 4, cell1.right.y)617+ shaft.lineTo(cell2.left.x - span.x // 4, cell2.left.y)618+619+ shaft.lineTo(node2.left)620+621+ elif _dir == 'right-down':622+ shaft.moveTo(node1.right)623+ shaft.lineTo(cell1.right.x + span.x // 2, cell1.right.y)624+625+ if self.edge.skipped:626+ shaft.lineTo(cell1.right.x + span.x // 2,627+ cell2.topleft.y - span.y // 2)628+ shaft.lineTo(cell2.left.x - span.x // 4,629+ cell2.topleft.y - span.y // 2)630+ shaft.lineTo(cell2.left.x - span.x // 4, cell2.left.y)631+ else:632+ shaft.lineTo(cell1.right.x + span.x // 2, cell2.left.y)633+634+ shaft.lineTo(node2.left)635+636+ elif _dir == 'up':637+ if self.edge.skipped:638+ shaft.moveTo(node1.right)639+ shaft.lineTo(cell1.right.x + span.x // 4, cell1.right.y)640+ shaft.lineTo(cell1.right.x + span.x // 4,641+ cell2.bottom.y + span.y // 2)642+ shaft.lineTo(cell2.bottom.x, cell2.bottom.y + span.y // 2)643+ else:644+ shaft.moveTo(node1.top)645+646+ shaft.lineTo(node2.bottom)647+648+ elif _dir in ('left-up', 'left', 'same'):649+ shaft.moveTo(node1.right)650+ shaft.lineTo(cell1.right.x + span.x // 4, cell1.right.y)651+ shaft.lineTo(cell1.right.x + span.x // 4,652+ cell2.top.y - span.y // 2 + span.y // 8)653+ shaft.lineTo(cell2.top.x,654+ cell2.top.y - span.y // 2 + span.y // 8)655+ shaft.lineTo(node2.top)656+657+ elif _dir == 'left-down':658+ if self.edge.skipped:659+ shaft.moveTo(node1.right)660+ shaft.lineTo(cell1.right.x + span.x // 2, cell1.right.y)661+ shaft.lineTo(cell1.right.x + span.x // 2,662+ cell2.top.y - span.y // 2)663+ shaft.lineTo(cell2.top.x, cell2.top.y - span.y // 2)664+ else:665+ shaft.moveTo(node1.bottom)666+ shaft.lineTo(cell1.bottom.x,667+ cell2.top.y - span.y // 2)668+ shaft.lineTo(cell2.top.x, cell2.top.y - span.y // 2)669+670+ shaft.lineTo(node2.top)671+672+ elif _dir == 'down':673+ if self.edge.skipped:674+ shaft.moveTo(node1.right)675+ shaft.lineTo(cell1.right.x + span.x // 2, cell1.right.y)676+ shaft.lineTo(cell1.right.x + span.x // 2,677+ cell2.top.y - span.y // 2 + span.y // 8)678+ shaft.lineTo(cell2.top.x,679+ cell2.top.y - span.y // 2 + span.y // 8)680+ else:681+ shaft.moveTo(node1.bottom)682+683+ shaft.lineTo(node2.top)684+685+ return shaft686+687+ @property688+ def labelbox(self):689+ span = XY(self.span_width, self.span_height)690+ node = XY(self.node_width, self.node_height)691+692+ _dir = self.edge.direction693+ node1 = self.cell(self.edge.node1, use_padding=False)694+ node2 = self.cell(self.edge.node2, use_padding=False)695+696+ if _dir == 'right':697+ if self.edge.skipped:698+ box = Box(node1.bottomright.x + span.x,699+ node1.bottomright.y,700+ node2.bottomleft.x - span.x,701+ node2.bottomleft.y + span.y // 2)702+ else:703+ box = Box(node1.topright.x, node1.topright.y - span.y // 8,704+ node2.left.x, node2.left.y - span.y // 8)705+706+ elif _dir == 'right-up':707+ box = Box(node2.left.x - span.x, node1.top.y - node.y // 2,708+ node2.bottomleft.x, node1.top.y)709+710+ elif _dir == 'right-down':711+ box = Box(node1.right.x, node2.topleft.y - span.y // 8,712+ node1.right.x + span.x, node2.left.y - span.y // 8)713+714+ elif _dir in ('up', 'left-up', 'left', 'same'):715+ if self.edge.node2.xy.y < self.edge.node1.xy.y:716+ box = Box(node1.topright.x - span.x // 2 + span.x // 4,717+ node1.topright.y - span.y // 2,718+ node1.topright.x + span.x // 2 + span.x // 4,719+ node1.topright.y)720+ else:721+ box = Box(node1.top.x + span.x // 4,722+ node1.top.y - span.y,723+ node1.topright.x + span.x // 4,724+ node1.topright.y - span.y // 2)725+726+ elif _dir in ('left-down', 'down'):727+ box = Box(node2.top.x + span.x // 4,728+ node2.top.y - span.y,729+ node2.topright.x + span.x // 4,730+ node2.topright.y - span.y // 2)731+732+ # shrink box733+ box = Box(box[0] + span.x // 8, box[1],734+ box[2] - span.x // 8, box[3])735+736+ return box737+738+739+class PortraitEdgeMetrics(EdgeMetrics):740+ @property741+ def headshapes(self):742+ heads = []743+ _dir = self.edge.direction744+745+ if self.edge.dir in ('back', 'both'):746+ if _dir == 'right':747+ if self.edge.skipped:748+ heads.append('up')749+ else:750+ heads.append('left')751+ elif _dir in ('up', 'right-up', 'same'):752+ heads.append('up')753+ elif _dir in ('left-up', 'left'):754+ heads.append('left')755+ elif _dir in ('left-down', 'down', 'right-down'):756+ if self.edge.skipped:757+ heads.append('left')758+ else:759+ heads.append('up')760+761+ if self.edge.hstyle in ('manyone', 'manymany'):762+ heads[-1] = 'r' + heads[-1]763+ else:764+ heads.append(None)765+766+ if self.edge.dir in ('forward', 'both'):767+ if _dir == 'right':768+ if self.edge.skipped:769+ heads.append('down')770+ else:771+ heads.append('right')772+ elif _dir in ('up', 'right-up', 'same'):773+ heads.append('down')774+ elif _dir in ('left-up', 'left',775+ 'left-down', 'down', 'right-down'):776+ heads.append('down')777+778+ if self.edge.hstyle in ('onemany', 'manymany'):779+ heads[-1] = 'r' + heads[-1]780+ else:781+ heads.append(None)782+783+ return heads784+785+ @property786+ def _shaft(self):787+ span = XY(self.span_width, self.span_height)788+ _dir = self.edge.direction789+790+ node1 = self.node(self.edge.node1)791+ cell1 = self.cell(self.edge.node1, use_padding=False)792+ node2 = self.node(self.edge.node2)793+ cell2 = self.cell(self.edge.node2, use_padding=False)794+795+ shaft = EdgeLines()796+ if _dir in ('up', 'right-up', 'same', 'right'):797+ if _dir == 'right' and not self.edge.skipped:798+ shaft.moveTo(node1.right)799+ shaft.lineTo(node2.left)800+ else:801+ shaft.moveTo(node1.bottom)802+ shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y // 2)803+ shaft.lineTo(cell2.right.x + span.x // 4,804+ cell1.bottom.y + span.y // 2)805+ shaft.lineTo(cell2.right.x + span.x // 4,806+ cell2.top.y - span.y // 2 + span.y // 8)807+ shaft.lineTo(cell2.top.x,808+ cell2.top.y - span.y // 2 + span.y // 8)809+ shaft.lineTo(node2.top)810+811+ elif _dir == 'right-down':812+ shaft.moveTo(node1.bottom)813+ shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y // 2)814+815+ if self.edge.skipped:816+ shaft.lineTo(cell2.left.x - span.x // 2,817+ cell1.bottom.y + span.y // 2)818+ shaft.lineTo(cell2.topleft.x - span.x // 2,819+ cell2.topleft.y - span.y // 2)820+ shaft.lineTo(cell2.top.x, cell2.top.y - span.y // 2)821+ else:822+ shaft.lineTo(cell2.top.x, cell1.bottom.y + span.y // 2)823+824+ shaft.lineTo(node2.top)825+826+ elif _dir in ('left-up', 'left', 'same'):827+ shaft.moveTo(node1.right)828+ shaft.lineTo(cell1.right.x + span.x // 4, cell1.right.y)829+ shaft.lineTo(cell1.right.x + span.x // 4,830+ cell2.top.y - span.y // 2 + span.y // 8)831+ shaft.lineTo(cell2.top.x,832+ cell2.top.y - span.y // 2 + span.y // 8)833+ shaft.lineTo(node2.top)834+835+ elif _dir == 'left-down':836+ shaft.moveTo(node1.bottom)837+838+ if self.edge.skipped:839+ shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y // 2)840+ shaft.lineTo(cell2.right.x + span.x // 2,841+ cell1.bottom.y + span.y // 2)842+ shaft.lineTo(cell2.right.x + span.x // 2,843+ cell2.top.y - span.y // 2)844+ else:845+ shaft.lineTo(cell1.bottom.x, cell2.top.y - span.y // 2)846+847+ shaft.lineTo(cell2.top.x, cell2.top.y - span.y // 2)848+ shaft.lineTo(node2.top)849+850+ elif _dir == 'down':851+ shaft.moveTo(node1.bottom)852+853+ if self.edge.skipped:854+ shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y // 2)855+ shaft.lineTo(cell1.right.x + span.x // 2,856+ cell1.bottom.y + span.y // 2)857+ shaft.lineTo(cell2.right.x + span.x // 2,858+ cell2.top.y - span.y // 2)859+ shaft.lineTo(cell2.top.x, cell2.top.y - span.y // 2)860+861+ shaft.lineTo(node2.top)862+863+ return shaft864+865+ @property866+ def labelbox(self):867+ span = XY(self.span_width, self.span_height)868+869+ _dir = self.edge.direction870+ node1 = self.cell(self.edge.node1, use_padding=False)871+ node2 = self.cell(self.edge.node2, use_padding=False)872+873+ if _dir == 'right':874+ if self.edge.skipped:875+ box = Box(node1.bottomright.x + span.x,876+ node1.bottomright.y,877+ node2.bottomleft.x - span.x,878+ node2.bottomleft.y + span.y // 2)879+ else:880+ box = Box(node1.topright.x, node1.topright.y - span.y // 8,881+ node2.left.x, node2.left.y - span.y // 8)882+883+ elif _dir == 'right-up':884+ box = Box(node2.left.x - span.x, node2.left.y,885+ node2.bottomleft.x, node2.bottomleft.y)886+887+ elif _dir == 'right-down':888+ box = Box(node2.topleft.x, node2.topleft.y - span.y // 2,889+ node2.top.x, node2.top.y)890+891+ elif _dir in ('up', 'left-up', 'left', 'same'):892+ if self.edge.node2.xy.y < self.edge.node1.xy.y:893+ box = Box(node1.topright.x - span.x // 2 + span.x // 4,894+ node1.topright.y - span.y // 2,895+ node1.topright.x + span.x // 2 + span.x // 4,896+ node1.topright.y)897+ else:898+ box = Box(node1.top.x + span.x // 4,899+ node1.top.y - span.y,900+ node1.topright.x + span.x // 4,901+ node1.topright.y - span.y // 2)902+903+ elif _dir == 'down':904+ box = Box(node2.top.x + span.x // 4,905+ node2.top.y - span.y // 2,906+ node2.topright.x + span.x // 4,907+ node2.topright.y)908+909+ elif _dir == 'left-down':910+ box = Box(node1.bottomleft.x, node1.bottomleft.y,911+ node1.bottom.x, node1.bottom.y + span.y // 2)912+913+ # shrink box914+ box = Box(box[0] + span.x // 8, box[1],915+ box[2] - span.x // 8, box[3])916+917+ return box918+919+920+class FlowchartLandscapeEdgeMetrics(LandscapeEdgeMetrics):921+ @property922+ def headshapes(self):923+ heads = []924+925+ if self.edge.direction == 'right-down':926+ if self.edge.dir in ('back', 'both'):927+ if self.edge.hstyle in ('manyone', 'manymany'):928+ heads.append('rup')929+ else:930+ heads.append('up')931+ else:932+ heads.append(None)933+934+ if self.edge.dir in ('forward', 'both'):935+ if self.edge.hstyle in ('onemany', 'manymany'):936+ heads.append('rright')937+ else:938+ heads.append('right')939+ else:940+ heads.append(None)941+ else:942+ heads = super(FlowchartLandscapeEdgeMetrics, self).headshapes943+944+ return heads945+946+ @property947+ def _shaft(self):948+ if self.edge.direction == 'right-down':949+ span = XY(self.span_width, self.span_height)950+ node1 = self.node(self.edge.node1)951+ cell1 = self.cell(self.edge.node1, use_padding=False)952+ node2 = self.node(self.edge.node2)953+ cell2 = self.cell(self.edge.node2, use_padding=False)954+955+ shaft = EdgeLines()956+ shaft.moveTo(node1.bottom)957+958+ if self.edge.skipped:959+ shaft.lineTo(cell1.bottom.x, cell1.bottom.y + span.y // 2)960+ shaft.lineTo(cell2.left.x - span.x // 4,961+ cell1.bottom.y + span.y // 2)962+ shaft.lineTo(cell2.left.x - span.x // 4, cell2.left.y)963+ else:964+ shaft.lineTo(cell1.bottom.x, cell2.left.y)965+966+ shaft.lineTo(node2.left)967+ else:968+ shaft = super(FlowchartLandscapeEdgeMetrics, self)._shaft969+970+ return shaft971+972+ @property973+ def labelbox(self):974+ _dir = self.edge.direction975+ if _dir == 'right':976+ span = XY(self.span_width, self.span_height)977+ cell1 = self.cell(self.edge.node1, use_padding=False)978+ cell2 = self.cell(self.edge.node2, use_padding=False)979+980+ if self.edge.skipped:981+ box = Box(cell1.bottom.x, cell1.bottom.y,982+ cell1.bottomright.x,983+ cell1.bottomright.y + span.y // 2)984+ else:985+ box = Box(cell1.bottom.x, cell2.left.y - span.y // 2,986+ cell1.bottom.x, cell2.left.y)987+ else:988+ box = super(FlowchartLandscapeEdgeMetrics, self).labelbox989+990+ return box991+992+993+class FlowchartPortraitEdgeMetrics(PortraitEdgeMetrics):994+ @property995+ def headshapes(self):996+ heads = []997+998+ if self.edge.direction == 'right-down':999+ if self.edge.dir in ('back', 'both'):1000+ if self.edge.hstyle in ('manyone', 'manymany'):1001+ heads.append('left')1002+ else:1003+ heads.append('left')1004+ else:1005+ heads.append(None)1006+1007+ if self.edge.dir in ('forward', 'both'):1008+ if self.edge.dir in ('onemany', 'manymany'):1009+ heads.append('rdown')1010+ else:1011+ heads.append('down')1012+ else:1013+ heads.append(None)1014+ else:1015+ heads = super(FlowchartPortraitEdgeMetrics, self).headshapes1016+1017+ return heads1018+1019+ @property1020+ def _shaft(self):1021+ if self.edge.direction == 'right-down':1022+ span = XY(self.span_width, self.span_height)1023+ node1 = self.node(self.edge.node1)1024+ cell1 = self.cell(self.edge.node1, use_padding=False)1025+ node2 = self.node(self.edge.node2)1026+ cell2 = self.cell(self.edge.node2, use_padding=False)1027+1028+ shaft = EdgeLines()1029+ shaft.moveTo(node1.right)1030+1031+ if self.edge.skipped:1032+ shaft.lineTo(cell1.right.x + span.x * 3 // 4,1033+ cell1.right.y)1034+ shaft.lineTo(cell1.right.x + span.x * 3 // 4,1035+ cell2.topleft.y - span.y // 2)1036+ shaft.lineTo(cell2.top.x, cell2.top.y - span.y // 2)1037+ else:1038+ shaft.lineTo(cell2.top.x, cell1.right.y)1039+1040+ shaft.lineTo(node2.top)1041+ else:1042+ shaft = super(FlowchartPortraitEdgeMetrics, self)._shaft1043+1044+ return shaft1045+1046+ @property1047+ def labelbox(self):1048+ _dir = self.edge.direction1049+ span = XY(self.span_width, self.span_height)1050+ cell1 = self.cell(self.edge.node1, use_padding=False)1051+ cell2 = self.cell(self.edge.node2, use_padding=False)1052+1053+ if _dir == 'down':1054+ box = Box(cell2.topleft.x, cell2.top.y - span.y // 2,1055+ cell2.top.x, cell2.top.y)1056+ elif _dir == 'right':1057+ if self.edge.skipped:1058+ box = Box(cell1.bottom.x, cell1.bottom.y,1059+ cell1.bottomright.x,1060+ cell1.bottomright.y + span.y // 2)1061+ else:1062+ box = Box(cell1.bottom.x, cell2.left.y - span.y // 2,1063+ cell1.bottom.x, cell2.left.y)1064+ else:1065+ box = super(FlowchartPortraitEdgeMetrics, self).labelbox1066+1067+ return box
A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/__init__.py +50 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/actor.py +120 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/base.py +136 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/beginpoint.py +61 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/box.py +46 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/circle.py +60 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/cloud.py +135 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/diamond.py +63 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/dots.py +53 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/ellipse.py +53 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/endpoint.py +68 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/database.py +135 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/input.py +63 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/loopin.py +68 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/loopout.py +68 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/flowchart/terminator.py +118 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/mail.py +63 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/minidiamond.py +53 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/none.py +35 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/note.py +61 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/roundedbox.py +132 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/square.py +61 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/noderenderer/textbox.py +49 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/parser.py +263 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/plugins/__init__.py +98 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/plugins/attributes.py +36 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/plugins/autoclass.py +37 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/__init__.py +190 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/bootstrap.py +327 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/compat.py +46 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/config.py +24 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/fontmap.py +171 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/images.py +152 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/logging.py +28 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/myitertools.py +52 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/rst/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/rst/directives.py +397 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/rst/nodes.py +56 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/urlutil.py +25 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/blockdiag/utils/uuid.py +25 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/entry.py +194 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/__init__.py +16 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/builder.py +305 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/command.py +31 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/drawer.py +178 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/elements.py +165 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/metrics.py +184 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/parser.py +299 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/utils/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/utils/rst/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/utils/rst/directives.py +49 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/nwdiag/utils/rst/nodes.py +25 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/__init__.py +18 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/builder.py +120 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/command.py +31 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/drawer.py +47 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/elements.py +79 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/metrics.py +63 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/noderenderers.py +53 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/parser.py +219 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/utils/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/utils/rst/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/utils/rst/directives.py +66 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/packetdiag/utils/rst/nodes.py +25 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/protocol.py +93 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/__init__.py +18 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/builder.py +123 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/command.py +31 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/drawer.py +64 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/elements.py +237 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/metrics.py +76 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/parser.py +262 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/utils/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/utils/math.py +41 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/utils/rst/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/utils/rst/directives.py +73 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/rackdiag/utils/rst/nodes.py +25 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/render.py +112 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/__init__.py +16 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/builder.py +233 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/command.py +31 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/drawer.py +178 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/elements.py +325 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/metrics.py +536 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/parser.py +286 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/utils/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/utils/rst/__init__.py +14 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/utils/rst/directives.py +44 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/seqdiag/utils/rst/nodes.py +25 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/svg.py +171 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/src/upstream_runtime.py +113 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/test/test_protocol.py +32 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/test/test_render.py +92 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/test/test_upstream_runtime.py +83 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/uv.lock +381 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/vendor-licenses/actdiag-LICENSE +202 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/vendor-licenses/blockdiag-LICENSE +202 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/vendor-licenses/dejavu-COPYRIGHT +78 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/vendor-licenses/nwdiag-LICENSE +202 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip-python-units/blockdiag-family/vendor-licenses/seqdiag-LICENSE +202 -0

Not shown: this page already shows 6000 lines of diff.

R diagramzip-render/wrangler.compatibility/blockdiag-family.jsonc → diagramzip-python-units/blockdiag-family/wrangler.jsonc +12 -9

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/README.md +7 -3

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/scripts/deploy-compatibility-units.mjs +1 -1

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/scripts/deploy-worker-units.mjs +0 -1

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/scripts/smoke-units.mjs +10 -0

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/src/catalog.ts +19 -1

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/src/index.ts +11 -2

Not shown: this page already shows 6000 lines of diff.

M diagramzip-render/test/catalog.test.ts +13 -2

Not shown: this page already shows 6000 lines of diff.

A diagramzip-render/test/gateway.test.ts +34 -0

Not shown: this page already shows 6000 lines of diff.

M diagramzip/src/preview.js +6 -1

Not shown: this page already shows 6000 lines of diff.

M diagramzip/src/renderer-units.js +10 -0

Not shown: this page already shows 6000 lines of diff.

A diagramzip/test/preview-routing.test.js +90 -0

Not shown: this page already shows 6000 lines of diff.

M diagramzip/test/renderer-units.test.js +13 -1

Not shown: this page already shows 6000 lines of diff.

M docs/refactor-engines.md +68 -30

Not shown: this page already shows 6000 lines of diff.

R server/src/main/resources/web/diagramzip/assets/index-C8MuTfFX.js → server/src/main/resources/web/diagramzip/assets/index-WmdMHtkV.js +10 -10

Not shown: this page already shows 6000 lines of diff.

M server/src/main/resources/web/diagramzip/index.html +1 -1

Not shown: this page already shows 6000 lines of diff.