pidraw 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. pidraw/__init__.py +192 -0
  2. pidraw/async_api.py +93 -0
  3. pidraw/backend/__init__.py +5 -0
  4. pidraw/backend/png.py +215 -0
  5. pidraw/backend/svg.py +265 -0
  6. pidraw/benchmark.py +294 -0
  7. pidraw/cache.py +220 -0
  8. pidraw/cli/__init__.py +1 -0
  9. pidraw/cli/commands.py +815 -0
  10. pidraw/cli/logging.py +49 -0
  11. pidraw/cli/main.py +366 -0
  12. pidraw/cli/setup.py +176 -0
  13. pidraw/core/__init__.py +41 -0
  14. pidraw/core/converters/__init__.py +41 -0
  15. pidraw/core/converters/ascii.py +107 -0
  16. pidraw/core/converters/base.py +29 -0
  17. pidraw/core/converters/d2.py +179 -0
  18. pidraw/core/converters/graphviz.py +153 -0
  19. pidraw/core/converters/mermaid.py +290 -0
  20. pidraw/core/converters/plantuml.py +94 -0
  21. pidraw/core/models.py +244 -0
  22. pidraw/core/shapes.py +153 -0
  23. pidraw/core/style.py +80 -0
  24. pidraw/detector.py +124 -0
  25. pidraw/diagnostics.py +89 -0
  26. pidraw/docs.py +344 -0
  27. pidraw/engines/__init__.py +99 -0
  28. pidraw/engines/base.py +36 -0
  29. pidraw/engines/bpmn.py +82 -0
  30. pidraw/engines/d2.py +130 -0
  31. pidraw/engines/excalidraw.py +205 -0
  32. pidraw/engines/graphviz.py +107 -0
  33. pidraw/engines/kroki.py +89 -0
  34. pidraw/engines/markmap.py +106 -0
  35. pidraw/engines/mermaid.py +204 -0
  36. pidraw/engines/native.py +48 -0
  37. pidraw/engines/nomnoml.py +82 -0
  38. pidraw/engines/plantuml.py +196 -0
  39. pidraw/engines/structurizr.py +102 -0
  40. pidraw/engines/tikz.py +129 -0
  41. pidraw/engines/vega.py +78 -0
  42. pidraw/engines/vega_lite.py +128 -0
  43. pidraw/engines/wavedrom.py +82 -0
  44. pidraw/exceptions.py +105 -0
  45. pidraw/formats.py +246 -0
  46. pidraw/incremental.py +178 -0
  47. pidraw/large.py +138 -0
  48. pidraw/layout/__init__.py +38 -0
  49. pidraw/layout/base.py +20 -0
  50. pidraw/layout/flow.py +82 -0
  51. pidraw/layout/grid.py +54 -0
  52. pidraw/layout/layered.py +104 -0
  53. pidraw/layout/tree.py +74 -0
  54. pidraw/models.py +54 -0
  55. pidraw/optimizer/__init__.py +15 -0
  56. pidraw/optimizer/levels.py +135 -0
  57. pidraw/optimizer/passes.py +525 -0
  58. pidraw/optimizer/svg_optimizer.py +219 -0
  59. pidraw/optimizer/validators.py +30 -0
  60. pidraw/pipeline.py +81 -0
  61. pidraw/pool.py +267 -0
  62. pidraw/py.typed +0 -0
  63. pidraw/quality/__init__.py +9 -0
  64. pidraw/quality/processor.py +247 -0
  65. pidraw/recovery.py +127 -0
  66. pidraw/registry.py +121 -0
  67. pidraw/renderer.py +311 -0
  68. pidraw/renderer_class.py +139 -0
  69. pidraw/result.py +58 -0
  70. pidraw/themes/__init__.py +39 -0
  71. pidraw/themes/base.py +29 -0
  72. pidraw/themes/blueprint.py +41 -0
  73. pidraw/themes/dark.py +39 -0
  74. pidraw/themes/light.py +39 -0
  75. pidraw/themes/minimal.py +40 -0
  76. pidraw/themes/professional.py +44 -0
  77. pidraw/typography.py +122 -0
  78. pidraw/utils/__init__.py +1 -0
  79. pidraw-1.2.0.dist-info/METADATA +360 -0
  80. pidraw-1.2.0.dist-info/RECORD +84 -0
  81. pidraw-1.2.0.dist-info/WHEEL +5 -0
  82. pidraw-1.2.0.dist-info/entry_points.txt +2 -0
  83. pidraw-1.2.0.dist-info/licenses/LICENSE +21 -0
  84. pidraw-1.2.0.dist-info/top_level.txt +1 -0
pidraw/__init__.py ADDED
@@ -0,0 +1,192 @@
1
+ """PiDraw — universal diagram rendering platform.
2
+
3
+ Converts diagram source code from many diagram languages into
4
+ optimised SVG through a plugin-based renderer architecture.
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ try:
10
+ __version__ = version("pidraw")
11
+ except PackageNotFoundError:
12
+ __version__ = "0.0.0+dev"
13
+
14
+
15
+ import pidraw.engines # noqa: F401 — trigger auto-registration
16
+ from pidraw.async_api import arender, arender_file
17
+ from pidraw.backend.png import svg_to_png
18
+ from pidraw.backend.svg import SvgBackend
19
+ from pidraw.benchmark import BenchmarkReport, BenchmarkResult, run_benchmarks
20
+ from pidraw.cache import CacheManager, CacheStats
21
+ from pidraw.core.converters import (
22
+ ASCIIConverter,
23
+ D2Converter,
24
+ GraphvizConverter,
25
+ MermaidConverter,
26
+ PlantUMLConverter,
27
+ convert,
28
+ get_converter,
29
+ list_converters,
30
+ )
31
+ from pidraw.core.models import (
32
+ ArrowStyle,
33
+ Diagram,
34
+ Edge,
35
+ EdgeStyle,
36
+ FontWeight,
37
+ Group,
38
+ Label,
39
+ Layout,
40
+ LayoutType,
41
+ Node,
42
+ Point,
43
+ Position,
44
+ Shape,
45
+ ShapeType,
46
+ Size,
47
+ Style,
48
+ TextAlign,
49
+ Viewport,
50
+ )
51
+ from pidraw.detector import detect, detect_language
52
+ from pidraw.diagnostics import analyze
53
+ from pidraw.engines.base import BaseRenderer
54
+ from pidraw.engines.d2 import D2Renderer
55
+ from pidraw.engines.graphviz import GraphvizRenderer
56
+ from pidraw.engines.mermaid import MermaidRenderer
57
+ from pidraw.engines.plantuml import PlantUMLRenderer
58
+ from pidraw.exceptions import (
59
+ EngineNotAvailableError,
60
+ LanguageNotSupportedError,
61
+ LayoutError,
62
+ OptimizationError,
63
+ ParseError,
64
+ PiDrawError,
65
+ PluginError,
66
+ PngConversionError,
67
+ RecoverableRenderingError,
68
+ RendererNotFoundError,
69
+ RenderError,
70
+ RenderingError,
71
+ RenderTimeoutError,
72
+ UnsupportedLanguageError,
73
+ )
74
+ from pidraw.formats import FormatInfo, format_table, list_formats
75
+ from pidraw.incremental import IncrementalRenderer
76
+ from pidraw.large import render_large_file
77
+ from pidraw.layout import apply_layout
78
+ from pidraw.models import AnalysisResult, DetectionResult, DiagramLanguage
79
+ from pidraw.optimizer import (
80
+ OptimizationResult,
81
+ optimize_by_level,
82
+ optimize_many,
83
+ optimize_svg,
84
+ )
85
+ from pidraw.pipeline import ExportPipeline, render_native, render_native_from_diagram
86
+ from pidraw.pool import RenderPool
87
+ from pidraw.quality import QualityProcessor
88
+ from pidraw.registry import (
89
+ clear_registry,
90
+ discover_plugins,
91
+ get_renderer,
92
+ list_renderers,
93
+ register_renderer,
94
+ )
95
+ from pidraw.renderer import render, render_file, render_many
96
+ from pidraw.renderer_class import Renderer
97
+ from pidraw.result import RenderResult
98
+ from pidraw.themes import apply_theme, get_theme, list_themes
99
+ from pidraw.typography import FontSpec, estimate_text_size
100
+
101
+ __all__ = [
102
+ "render",
103
+ "render_file",
104
+ "render_many",
105
+ "render_large_file",
106
+ "render_native",
107
+ "render_native_from_diagram",
108
+ "Renderer",
109
+ "RenderResult",
110
+ "arender",
111
+ "arender_file",
112
+ "detect",
113
+ "detect_language",
114
+ "analyze",
115
+ "register_renderer",
116
+ "get_renderer",
117
+ "list_renderers",
118
+ "clear_registry",
119
+ "discover_plugins",
120
+ "BaseRenderer",
121
+ "MermaidRenderer",
122
+ "GraphvizRenderer",
123
+ "PlantUMLRenderer",
124
+ "D2Renderer",
125
+ "DiagramLanguage",
126
+ "DetectionResult",
127
+ "AnalysisResult",
128
+ "optimize_svg",
129
+ "optimize_many",
130
+ "optimize_by_level",
131
+ "OptimizationResult",
132
+ "CacheManager",
133
+ "CacheStats",
134
+ "RenderPool",
135
+ "QualityProcessor",
136
+ "IncrementalRenderer",
137
+ "FormatInfo",
138
+ "list_formats",
139
+ "format_table",
140
+ "run_benchmarks",
141
+ "BenchmarkReport",
142
+ "BenchmarkResult",
143
+ "RecoverableRenderingError",
144
+ "svg_to_png",
145
+ "SvgBackend",
146
+ "ExportPipeline",
147
+ "Diagram",
148
+ "Node",
149
+ "Edge",
150
+ "Label",
151
+ "Shape",
152
+ "ShapeType",
153
+ "Style",
154
+ "EdgeStyle",
155
+ "ArrowStyle",
156
+ "Layout",
157
+ "LayoutType",
158
+ "Group",
159
+ "Viewport",
160
+ "Position",
161
+ "Size",
162
+ "Point",
163
+ "FontWeight",
164
+ "TextAlign",
165
+ "FontSpec",
166
+ "estimate_text_size",
167
+ "apply_layout",
168
+ "apply_theme",
169
+ "get_theme",
170
+ "list_themes",
171
+ "convert",
172
+ "get_converter",
173
+ "list_converters",
174
+ "MermaidConverter",
175
+ "PlantUMLConverter",
176
+ "GraphvizConverter",
177
+ "D2Converter",
178
+ "ASCIIConverter",
179
+ "PiDrawError",
180
+ "LanguageNotSupportedError",
181
+ "EngineNotAvailableError",
182
+ "RenderError",
183
+ "ParseError",
184
+ "LayoutError",
185
+ "RenderTimeoutError",
186
+ "OptimizationError",
187
+ "PngConversionError",
188
+ "UnsupportedLanguageError",
189
+ "RendererNotFoundError",
190
+ "RenderingError",
191
+ "PluginError",
192
+ ]
pidraw/async_api.py ADDED
@@ -0,0 +1,93 @@
1
+ """Async rendering API for PiDraw.
2
+
3
+ Provides ``arender`` and ``arender_file`` as async counterparts to
4
+ the synchronous ``render`` / ``render_file`` functions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pidraw.renderer import render
10
+ from pidraw.result import RenderResult
11
+
12
+
13
+ async def arender(
14
+ source: str,
15
+ *,
16
+ language: str | None = None,
17
+ optimize: str | None = None,
18
+ output_format: str = "svg",
19
+ timeout: float = 30.0,
20
+ theme: str = "light",
21
+ ) -> RenderResult:
22
+ """Async version of :func:`render`. Safe to call from any async framework.
23
+
24
+ Parameters
25
+ ----------
26
+ source : str
27
+ The diagram source code.
28
+ language : str | None
29
+ Explicit language identifier. Auto-detected when ``None``.
30
+ optimize : str | None
31
+ Optimisation level (``"fast"``, ``"balanced"``, ``"maximum"``).
32
+ output_format : str
33
+ Output format. ``"svg"`` (default) or ``"png"``.
34
+ timeout : float
35
+ Maximum render time in seconds.
36
+ theme : str
37
+ Theme name to apply.
38
+
39
+ Returns
40
+ -------
41
+ RenderResult
42
+ The rendered output.
43
+
44
+ Raises
45
+ ------
46
+ PiDrawError
47
+ On any rendering failure.
48
+ """
49
+ import asyncio
50
+
51
+ loop = asyncio.get_event_loop()
52
+
53
+ def _sync_render() -> RenderResult:
54
+ return render(
55
+ source,
56
+ language=language,
57
+ format=output_format,
58
+ optimize=optimize or False,
59
+ timeout=timeout,
60
+ theme=theme,
61
+ )
62
+
63
+ return await loop.run_in_executor(None, _sync_render)
64
+
65
+
66
+ async def arender_file(path: str, **kwargs: object) -> RenderResult:
67
+ """Async version of :func:`render_file`.
68
+
69
+ Parameters
70
+ ----------
71
+ path : str
72
+ Path to the diagram source file.
73
+ **kwargs
74
+ Forwarded to :func:`arender`.
75
+
76
+ Returns
77
+ -------
78
+ RenderResult
79
+ The rendered output.
80
+ """
81
+ import asyncio
82
+
83
+ source: str
84
+ try:
85
+ import aiofiles # noqa: F401
86
+
87
+ async with aiofiles.open(path, encoding="utf-8") as f: # type: ignore[union-attr]
88
+ source = await f.read() # type: ignore[union-attr]
89
+ except ImportError:
90
+ source = await asyncio.to_thread(
91
+ lambda: open(path, "r", encoding="utf-8").read()
92
+ )
93
+ return await arender(source, **kwargs)
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from pidraw.backend.svg import SvgBackend
4
+
5
+ __all__ = ["SvgBackend"]
pidraw/backend/png.py ADDED
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ import tempfile
8
+ from importlib import util as importlib_util
9
+ from io import BytesIO
10
+ from typing import Callable, Optional
11
+
12
+ from pidraw.exceptions import PngConversionError
13
+
14
+
15
+ # Pattern to match SVG background rectangles (solid fills that cover the full viewport)
16
+ _BG_RECT_RE = re.compile(
17
+ r"<rect\s[^>]*?(?:width\s*=\s*['\"]100%(?:[^>]*?height\s*=\s*['\"]100%['\"])"
18
+ r"|height\s*=\s*['\"]100%(?:[^>]*?width\s*=\s*['\"]100%['\"]))[^>]*?\/?\s*>",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def strip_svg_background(svg: str) -> str:
24
+ return _BG_RECT_RE.sub("", svg)
25
+
26
+
27
+ def trim_png(png_bytes: bytes, padding: int = 0) -> bytes:
28
+ try:
29
+ from PIL import Image
30
+
31
+ Image.MAX_IMAGE_PIXELS = None
32
+ img = Image.open(BytesIO(png_bytes))
33
+ if img.mode != "RGBA":
34
+ img = img.convert("RGBA")
35
+ bbox = img.getbbox()
36
+ if bbox is None:
37
+ return png_bytes
38
+ if padding <= 0:
39
+ padding = max(50, int(min(img.width, img.height) * 0.08))
40
+ left = max(0, bbox[0] - padding)
41
+ upper = max(0, bbox[1] - padding)
42
+ right = min(img.width, bbox[2] + padding)
43
+ lower = min(img.height, bbox[3] + padding)
44
+ cropped = img.crop((left, upper, right, lower))
45
+ buf = BytesIO()
46
+ cropped.save(buf, format="PNG")
47
+ buf.seek(0)
48
+ return buf.read()
49
+ except Exception:
50
+ return png_bytes
51
+
52
+
53
+ def svg_to_png(
54
+ svg: str,
55
+ scale: float = 1.0,
56
+ background_color: Optional[str] = None,
57
+ output_width: Optional[int] = None,
58
+ output_height: Optional[int] = None,
59
+ transparent: bool = True,
60
+ trim: bool = True,
61
+ ) -> bytes:
62
+ if transparent:
63
+ svg = strip_svg_background(svg)
64
+ backend = _detect_backend()
65
+ try:
66
+ result = backend(
67
+ svg,
68
+ scale=scale,
69
+ background_color=background_color if not transparent else None,
70
+ output_width=output_width,
71
+ output_height=output_height,
72
+ transparent=transparent,
73
+ )
74
+ except Exception as exc:
75
+ raise PngConversionError(
76
+ f"{backend.__name__} failed: {exc}"
77
+ ) from exc
78
+ if transparent and trim:
79
+ result = trim_png(result)
80
+ return result
81
+
82
+
83
+ BackendFn = Callable[..., bytes]
84
+
85
+
86
+ def _detect_backend() -> BackendFn:
87
+ errors: list[str] = []
88
+
89
+ if shutil.which("resvg") is not None:
90
+ return _render_resvg
91
+ else:
92
+ errors.append("resvg CLI not found on PATH")
93
+
94
+ if importlib_util.find_spec("cairosvg") is not None:
95
+ try:
96
+ import cairosvg # type: ignore[import-not-found, import-untyped]
97
+
98
+ cairosvg.svg2png
99
+ return _render_cairosvg
100
+ except OSError as e:
101
+ errors.append(f"cairosvg (Cairo library not found: {e})")
102
+ else:
103
+ errors.append("cairosvg not installed")
104
+
105
+ if importlib_util.find_spec("playwright") is not None:
106
+ return _render_playwright
107
+ else:
108
+ errors.append("playwright not installed")
109
+
110
+ raise ImportError(
111
+ "No SVG-to-PNG backend available.\n\n"
112
+ "Options:\n"
113
+ " Install resvg CLI: https://github.com/RazrFalcon/resvg/releases\n"
114
+ " pip install pidraw[png-playwright] && playwright install chromium\n"
115
+ " pip install pidraw[png] (cairosvg - needs Cairo DLLs)\n\n"
116
+ "Detected issues:\n"
117
+ + "\n".join(f" - {e}" for e in errors)
118
+ )
119
+
120
+
121
+ def _render_resvg(
122
+ svg: str,
123
+ scale: float = 1.0,
124
+ background_color: Optional[str] = None,
125
+ output_width: Optional[int] = None,
126
+ output_height: Optional[int] = None,
127
+ transparent: bool = True,
128
+ ) -> bytes:
129
+ tmp_dir: Optional[str] = None
130
+ try:
131
+ tmp_dir = tempfile.mkdtemp(prefix="pidraw_resvg_")
132
+ input_path = os.path.join(tmp_dir, "input.svg")
133
+ output_path = os.path.join(tmp_dir, "output.png")
134
+ with open(input_path, "w", encoding="utf-8") as f:
135
+ f.write(svg)
136
+ cmd = ["resvg", input_path, output_path]
137
+ if output_width is not None and output_height is not None:
138
+ cmd.extend(["--width", str(output_width), "--height", str(output_height)])
139
+ if scale != 1.0:
140
+ cmd.extend(["--dpi", str(int(96 * scale))])
141
+ subprocess.run(cmd, capture_output=True, timeout=60, check=True)
142
+ with open(output_path, "rb") as f:
143
+ return f.read()
144
+ except subprocess.TimeoutExpired:
145
+ raise PngConversionError("resvg timed out after 60s")
146
+ except subprocess.CalledProcessError as exc:
147
+ stderr = exc.stderr.decode("utf-8", errors="replace") if exc.stderr else ""
148
+ raise PngConversionError(f"resvg failed: {stderr}")
149
+ except FileNotFoundError:
150
+ raise PngConversionError("resvg executable not found")
151
+ finally:
152
+ if tmp_dir is not None and os.path.isdir(tmp_dir):
153
+ try:
154
+ shutil.rmtree(tmp_dir)
155
+ except OSError:
156
+ pass
157
+
158
+
159
+ def _render_cairosvg(
160
+ svg: str,
161
+ scale: float = 1.0,
162
+ background_color: Optional[str] = None,
163
+ output_width: Optional[int] = None,
164
+ output_height: Optional[int] = None,
165
+ transparent: bool = True,
166
+ ) -> bytes:
167
+ import cairosvg # type: ignore[import-untyped]
168
+
169
+ kwargs: dict = {"scale": scale}
170
+ bg = None if transparent else background_color
171
+ if bg is not None:
172
+ kwargs["background_color"] = bg
173
+ elif transparent:
174
+ kwargs["background_color"] = "transparent"
175
+ if output_width is not None:
176
+ kwargs["output_width"] = output_width
177
+ if output_height is not None:
178
+ kwargs["output_height"] = output_height
179
+
180
+ return cairosvg.svg2png(bytestring=svg.encode("utf-8"), **kwargs)
181
+
182
+
183
+ def _render_playwright(
184
+ svg: str,
185
+ scale: float = 1.0,
186
+ background_color: Optional[str] = None,
187
+ output_width: Optional[int] = None,
188
+ output_height: Optional[int] = None,
189
+ transparent: bool = True,
190
+ ) -> bytes:
191
+ from playwright.sync_api import sync_playwright # type: ignore[import-not-found, import-untyped]
192
+
193
+ has_bg = background_color is not None and not transparent
194
+ bg_css = f"background: {background_color};" if has_bg else ""
195
+ html_parts = [
196
+ "<!DOCTYPE html><html><head><style>",
197
+ f" body {{ margin: 0; {bg_css} }}",
198
+ " svg { max-width: 100%; height: auto; }",
199
+ "</style></head><body>",
200
+ svg,
201
+ "</body></html>",
202
+ ]
203
+ html = "\n".join(html_parts)
204
+
205
+ with sync_playwright() as p:
206
+ browser = p.chromium.launch()
207
+ page = browser.new_page(
208
+ device_scale_factor=scale,
209
+ viewport={"width": output_width or 800, "height": output_height or 600},
210
+ )
211
+ page.set_content(html)
212
+ png_bytes = page.screenshot(full_page=True, omit_background=transparent)
213
+ browser.close()
214
+
215
+ return png_bytes