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.
- pidraw/__init__.py +192 -0
- pidraw/async_api.py +93 -0
- pidraw/backend/__init__.py +5 -0
- pidraw/backend/png.py +215 -0
- pidraw/backend/svg.py +265 -0
- pidraw/benchmark.py +294 -0
- pidraw/cache.py +220 -0
- pidraw/cli/__init__.py +1 -0
- pidraw/cli/commands.py +815 -0
- pidraw/cli/logging.py +49 -0
- pidraw/cli/main.py +366 -0
- pidraw/cli/setup.py +176 -0
- pidraw/core/__init__.py +41 -0
- pidraw/core/converters/__init__.py +41 -0
- pidraw/core/converters/ascii.py +107 -0
- pidraw/core/converters/base.py +29 -0
- pidraw/core/converters/d2.py +179 -0
- pidraw/core/converters/graphviz.py +153 -0
- pidraw/core/converters/mermaid.py +290 -0
- pidraw/core/converters/plantuml.py +94 -0
- pidraw/core/models.py +244 -0
- pidraw/core/shapes.py +153 -0
- pidraw/core/style.py +80 -0
- pidraw/detector.py +124 -0
- pidraw/diagnostics.py +89 -0
- pidraw/docs.py +344 -0
- pidraw/engines/__init__.py +99 -0
- pidraw/engines/base.py +36 -0
- pidraw/engines/bpmn.py +82 -0
- pidraw/engines/d2.py +130 -0
- pidraw/engines/excalidraw.py +205 -0
- pidraw/engines/graphviz.py +107 -0
- pidraw/engines/kroki.py +89 -0
- pidraw/engines/markmap.py +106 -0
- pidraw/engines/mermaid.py +204 -0
- pidraw/engines/native.py +48 -0
- pidraw/engines/nomnoml.py +82 -0
- pidraw/engines/plantuml.py +196 -0
- pidraw/engines/structurizr.py +102 -0
- pidraw/engines/tikz.py +129 -0
- pidraw/engines/vega.py +78 -0
- pidraw/engines/vega_lite.py +128 -0
- pidraw/engines/wavedrom.py +82 -0
- pidraw/exceptions.py +105 -0
- pidraw/formats.py +246 -0
- pidraw/incremental.py +178 -0
- pidraw/large.py +138 -0
- pidraw/layout/__init__.py +38 -0
- pidraw/layout/base.py +20 -0
- pidraw/layout/flow.py +82 -0
- pidraw/layout/grid.py +54 -0
- pidraw/layout/layered.py +104 -0
- pidraw/layout/tree.py +74 -0
- pidraw/models.py +54 -0
- pidraw/optimizer/__init__.py +15 -0
- pidraw/optimizer/levels.py +135 -0
- pidraw/optimizer/passes.py +525 -0
- pidraw/optimizer/svg_optimizer.py +219 -0
- pidraw/optimizer/validators.py +30 -0
- pidraw/pipeline.py +81 -0
- pidraw/pool.py +267 -0
- pidraw/py.typed +0 -0
- pidraw/quality/__init__.py +9 -0
- pidraw/quality/processor.py +247 -0
- pidraw/recovery.py +127 -0
- pidraw/registry.py +121 -0
- pidraw/renderer.py +311 -0
- pidraw/renderer_class.py +139 -0
- pidraw/result.py +58 -0
- pidraw/themes/__init__.py +39 -0
- pidraw/themes/base.py +29 -0
- pidraw/themes/blueprint.py +41 -0
- pidraw/themes/dark.py +39 -0
- pidraw/themes/light.py +39 -0
- pidraw/themes/minimal.py +40 -0
- pidraw/themes/professional.py +44 -0
- pidraw/typography.py +122 -0
- pidraw/utils/__init__.py +1 -0
- pidraw-1.2.0.dist-info/METADATA +360 -0
- pidraw-1.2.0.dist-info/RECORD +84 -0
- pidraw-1.2.0.dist-info/WHEEL +5 -0
- pidraw-1.2.0.dist-info/entry_points.txt +2 -0
- pidraw-1.2.0.dist-info/licenses/LICENSE +21 -0
- 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)
|
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
|