eulumdat-plot 0.0.1__tar.gz → 1.0.0__tar.gz

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.
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: eulumdat-plot
3
+ Version: 1.0.0
4
+ Summary: Photometric polar diagram generator for EULUMDAT (.ldt) files — extension to eulumdat-py
5
+ Author: 123VincentB
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/123VincentB/eulumdat-plot
8
+ Project-URL: Repository, https://github.com/123VincentB/eulumdat-plot
9
+ Project-URL: Issues, https://github.com/123VincentB/eulumdat-plot/issues
10
+ Project-URL: Changelog, https://github.com/123VincentB/eulumdat-plot/blob/main/CHANGELOG.md
11
+ Keywords: eulumdat,ldt,photometry,lighting,luminaire,polar,candela,diagram,svg,lumtopic
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Manufacturing
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: eulumdat-py>=1.0.0
25
+ Requires-Dist: numpy>=1.21
26
+ Requires-Dist: svgwrite>=1.4
27
+ Provides-Extra: export
28
+ Requires-Dist: vl-convert-python>=1.6; extra == "export"
29
+ Requires-Dist: Pillow>=9.0; extra == "export"
30
+ Provides-Extra: cubic
31
+ Requires-Dist: scipy>=1.7; extra == "cubic"
32
+ Provides-Extra: full
33
+ Requires-Dist: eulumdat-plot[export]; extra == "full"
34
+ Requires-Dist: eulumdat-plot[cubic]; extra == "full"
35
+ Provides-Extra: dev
36
+ Requires-Dist: build; extra == "dev"
37
+ Requires-Dist: twine; extra == "dev"
38
+ Requires-Dist: pytest>=7.0; extra == "dev"
39
+ Requires-Dist: eulumdat-plot[full]; extra == "dev"
40
+
41
+ # eulumdat-plot
42
+
43
+ Photometric polar diagram generator for EULUMDAT (`.ldt`) files —
44
+ designed for **product datasheets and publication-ready documents**.
45
+
46
+ Reads a `.ldt` file and produces a **Lumtopic-style SVG**: a square image
47
+ with a top banner and a polar candela distribution diagram showing the
48
+ C0/C180 (solid) and C90/C270 (dotted) curves, scaled to fill the plot area.
49
+
50
+ > **For scientific / interactive plots** (matplotlib, axis labels, legends),
51
+ > see the
52
+ > [eulumdat-py examples](https://github.com/123VincentB/eulumdat-py/blob/main/examples/02_polar_diagram.md).
53
+
54
+ Part of the [`eulumdat-*`](https://github.com/123VincentB) ecosystem, built
55
+ on top of [`eulumdat-py`](https://pypi.org/project/eulumdat-py/).
56
+
57
+ ---
58
+
59
+ ![Photometric diagram example](docs/img/sample_01.svg)
60
+
61
+ ---
62
+
63
+ ## Features
64
+
65
+ - Reads any EULUMDAT file — all symmetry types (ISYM 0–4) handled by `eulumdat-py`
66
+ - Generates a **publication-ready SVG** polar diagram (Lumtopic style)
67
+ - Dynamic radial scale (3–6 concentric circles, round values)
68
+ - Dominant-hemisphere detection for automatic scale label placement
69
+ - Proportional scaling via `Layout.for_size(n)` — one parameter controls everything
70
+ - Optional I(γ) interpolation (linear or cubic spline) for smooth curves
71
+ - Optional raster export to **PNG** and **JPEG** (cross-platform, no native DLL)
72
+ - Debug mode for visual validation of C-plane assignment
73
+
74
+ ## Installation
75
+
76
+ Core package (SVG generation only):
77
+
78
+ ```bash
79
+ pip install eulumdat-plot
80
+ ```
81
+
82
+ With raster export (PNG / JPEG):
83
+
84
+ ```bash
85
+ pip install "eulumdat-plot[export]"
86
+ ```
87
+
88
+ With cubic spline interpolation:
89
+
90
+ ```bash
91
+ pip install "eulumdat-plot[cubic]"
92
+ ```
93
+
94
+ Everything:
95
+
96
+ ```bash
97
+ pip install "eulumdat-plot[full]"
98
+ ```
99
+
100
+ ## Quick start
101
+
102
+ ```python
103
+ from eulumdat_plot import plot_ldt
104
+
105
+ # Generate an SVG next to the source file
106
+ svg = plot_ldt("luminaire.ldt")
107
+
108
+ # With a distribution code in the banner centre
109
+ svg = plot_ldt("luminaire.ldt", code="D53")
110
+ ```
111
+
112
+ ## Scaling
113
+
114
+ All visual parameters (stroke widths, font sizes, margins) scale
115
+ proportionally from the 1181 px reference with a single call:
116
+
117
+ ```python
118
+ from eulumdat_plot import plot_ldt, Layout
119
+
120
+ svg = plot_ldt("luminaire.ldt", layout=Layout.for_size(600))
121
+ ```
122
+
123
+ ## Raster export
124
+
125
+ ```python
126
+ from eulumdat_plot import plot_ldt, Layout
127
+ from eulumdat_plot.export import svg_to_png, svg_to_jpg
128
+
129
+ svg = plot_ldt("luminaire.ldt", layout=Layout.for_size(1181))
130
+ png = svg_to_png(svg, size_px=600)
131
+ jpg = svg_to_jpg(svg, size_px=600, quality=95)
132
+ ```
133
+
134
+ The export size is independent of the SVG canvas size.
135
+
136
+ ## API reference
137
+
138
+ ### `plot_ldt()`
139
+
140
+ ```python
141
+ def plot_ldt(
142
+ ldt_path: str | Path,
143
+ svg_path: str | Path | None = None,
144
+ *,
145
+ code: str = "",
146
+ layout: Layout | None = None,
147
+ interpolate: bool = True,
148
+ interp_step_deg: float = 1.0,
149
+ interp_method: str = "linear",
150
+ debug: bool = False,
151
+ ) -> Path
152
+ ```
153
+
154
+ | Parameter | Default | Description |
155
+ |---|---|---|
156
+ | `ldt_path` | — | Source `.ldt` file |
157
+ | `svg_path` | same name, `.svg` | Output SVG path |
158
+ | `code` | `""` | Distribution code shown in the banner centre |
159
+ | `layout` | `Layout()` | Visual parameters |
160
+ | `interpolate` | `True` | Resample I(γ) before plotting |
161
+ | `interp_step_deg` | `1.0` | Angular step for resampling (degrees) |
162
+ | `interp_method` | `"linear"` | `"linear"` or `"cubic"` (requires scipy) |
163
+ | `debug` | `False` | Colour-code C-planes for visual validation |
164
+
165
+ ### `Layout.for_size()`
166
+
167
+ ```python
168
+ Layout.for_size(size_px: int) -> Layout
169
+ ```
170
+
171
+ Creates a `Layout` with all dimensions scaled proportionally from the
172
+ 1181 px reference. `Layout.for_size(1181)` is identical to `Layout()`.
173
+
174
+ ### `svg_to_png()` / `svg_to_jpg()`
175
+
176
+ ```python
177
+ svg_to_png(svg_path, png_path=None, *, size_px=1181, background="#FFFFFF") -> Path
178
+ svg_to_jpg(svg_path, jpg_path=None, *, size_px=1181, background="#FFFFFF", quality=95) -> Path
179
+ ```
180
+
181
+ Requires `pip install "eulumdat-plot[export]"`.
182
+
183
+ ## Examples
184
+
185
+ | File | Description |
186
+ |---|---|
187
+ | [`examples/01_basic_usage.md`](examples/01_basic_usage.md) | Generate an SVG from a `.ldt` file |
188
+ | [`examples/02_resize_and_export.md`](examples/02_resize_and_export.md) | Scaling, raster export, batch processing |
189
+
190
+ ## Project structure
191
+
192
+ ```
193
+ eulumdat-plot/
194
+ ├── data/
195
+ │ ├── input/ # sample .ldt files (ISYM 0–4)
196
+ │ └── output/ # generated SVG / PNG / JPEG
197
+ ├── docs/
198
+ │ └── img/
199
+ │ └── sample_01.svg
200
+ ├── examples/
201
+ │ ├── 01_basic_usage.md
202
+ │ └── 02_resize_and_export.md
203
+ ├── src/
204
+ │ └── eulumdat_plot/
205
+ │ ├── __init__.py
206
+ │ ├── plot.py # public API — LDT → SVG pipeline
207
+ │ ├── renderer.py # SVG renderer + Layout dataclass
208
+ │ └── export.py # raster export (PNG / JPEG)
209
+ ├── tests/
210
+ │ ├── test_smoke.py # 46 real LDT files, all ISYM types
211
+ │ └── test_scaling.py # Layout.for_size() proportionality
212
+ ├── pyproject.toml
213
+ ├── CHANGELOG.md
214
+ └── README.md
215
+ ```
216
+
217
+ ## EULUMDAT ecosystem
218
+
219
+ | Package | Status | Description |
220
+ |---|---|---|
221
+ | [`eulumdat-py`](https://pypi.org/project/eulumdat-py/) | v0.1.4 | Read / write EULUMDAT files |
222
+ | [`eulumdat-symmetry`](https://pypi.org/project/eulumdat-symmetry/) | v1.0.0 | Symmetrise EULUMDAT files |
223
+ | `eulumdat-plot` | v1.0.0 | Photometric polar diagram — **this package** |
224
+ | `eulumdat-luminance` | planned | Luminance table cd/m² (γ 55°–85°) |
225
+ | `eulumdat-ugr` | planned | UGR calculation (CIE 117, CIE 190) |
226
+
227
+ ## Requirements
228
+
229
+ - Python ≥ 3.9
230
+ - `eulumdat-py` ≥ 1.0.0
231
+ - `numpy` ≥ 1.21
232
+ - `svgwrite` ≥ 1.4
233
+ - *(optional)* `vl-convert-python` ≥ 1.6 + `Pillow` ≥ 9.0 — raster export
234
+ - *(optional)* `scipy` ≥ 1.7 — cubic spline interpolation
235
+
236
+ ## License
237
+
238
+ MIT — © 2024 [123VincentB](https://github.com/123VincentB)
@@ -0,0 +1,198 @@
1
+ # eulumdat-plot
2
+
3
+ Photometric polar diagram generator for EULUMDAT (`.ldt`) files —
4
+ designed for **product datasheets and publication-ready documents**.
5
+
6
+ Reads a `.ldt` file and produces a **Lumtopic-style SVG**: a square image
7
+ with a top banner and a polar candela distribution diagram showing the
8
+ C0/C180 (solid) and C90/C270 (dotted) curves, scaled to fill the plot area.
9
+
10
+ > **For scientific / interactive plots** (matplotlib, axis labels, legends),
11
+ > see the
12
+ > [eulumdat-py examples](https://github.com/123VincentB/eulumdat-py/blob/main/examples/02_polar_diagram.md).
13
+
14
+ Part of the [`eulumdat-*`](https://github.com/123VincentB) ecosystem, built
15
+ on top of [`eulumdat-py`](https://pypi.org/project/eulumdat-py/).
16
+
17
+ ---
18
+
19
+ ![Photometric diagram example](docs/img/sample_01.svg)
20
+
21
+ ---
22
+
23
+ ## Features
24
+
25
+ - Reads any EULUMDAT file — all symmetry types (ISYM 0–4) handled by `eulumdat-py`
26
+ - Generates a **publication-ready SVG** polar diagram (Lumtopic style)
27
+ - Dynamic radial scale (3–6 concentric circles, round values)
28
+ - Dominant-hemisphere detection for automatic scale label placement
29
+ - Proportional scaling via `Layout.for_size(n)` — one parameter controls everything
30
+ - Optional I(γ) interpolation (linear or cubic spline) for smooth curves
31
+ - Optional raster export to **PNG** and **JPEG** (cross-platform, no native DLL)
32
+ - Debug mode for visual validation of C-plane assignment
33
+
34
+ ## Installation
35
+
36
+ Core package (SVG generation only):
37
+
38
+ ```bash
39
+ pip install eulumdat-plot
40
+ ```
41
+
42
+ With raster export (PNG / JPEG):
43
+
44
+ ```bash
45
+ pip install "eulumdat-plot[export]"
46
+ ```
47
+
48
+ With cubic spline interpolation:
49
+
50
+ ```bash
51
+ pip install "eulumdat-plot[cubic]"
52
+ ```
53
+
54
+ Everything:
55
+
56
+ ```bash
57
+ pip install "eulumdat-plot[full]"
58
+ ```
59
+
60
+ ## Quick start
61
+
62
+ ```python
63
+ from eulumdat_plot import plot_ldt
64
+
65
+ # Generate an SVG next to the source file
66
+ svg = plot_ldt("luminaire.ldt")
67
+
68
+ # With a distribution code in the banner centre
69
+ svg = plot_ldt("luminaire.ldt", code="D53")
70
+ ```
71
+
72
+ ## Scaling
73
+
74
+ All visual parameters (stroke widths, font sizes, margins) scale
75
+ proportionally from the 1181 px reference with a single call:
76
+
77
+ ```python
78
+ from eulumdat_plot import plot_ldt, Layout
79
+
80
+ svg = plot_ldt("luminaire.ldt", layout=Layout.for_size(600))
81
+ ```
82
+
83
+ ## Raster export
84
+
85
+ ```python
86
+ from eulumdat_plot import plot_ldt, Layout
87
+ from eulumdat_plot.export import svg_to_png, svg_to_jpg
88
+
89
+ svg = plot_ldt("luminaire.ldt", layout=Layout.for_size(1181))
90
+ png = svg_to_png(svg, size_px=600)
91
+ jpg = svg_to_jpg(svg, size_px=600, quality=95)
92
+ ```
93
+
94
+ The export size is independent of the SVG canvas size.
95
+
96
+ ## API reference
97
+
98
+ ### `plot_ldt()`
99
+
100
+ ```python
101
+ def plot_ldt(
102
+ ldt_path: str | Path,
103
+ svg_path: str | Path | None = None,
104
+ *,
105
+ code: str = "",
106
+ layout: Layout | None = None,
107
+ interpolate: bool = True,
108
+ interp_step_deg: float = 1.0,
109
+ interp_method: str = "linear",
110
+ debug: bool = False,
111
+ ) -> Path
112
+ ```
113
+
114
+ | Parameter | Default | Description |
115
+ |---|---|---|
116
+ | `ldt_path` | — | Source `.ldt` file |
117
+ | `svg_path` | same name, `.svg` | Output SVG path |
118
+ | `code` | `""` | Distribution code shown in the banner centre |
119
+ | `layout` | `Layout()` | Visual parameters |
120
+ | `interpolate` | `True` | Resample I(γ) before plotting |
121
+ | `interp_step_deg` | `1.0` | Angular step for resampling (degrees) |
122
+ | `interp_method` | `"linear"` | `"linear"` or `"cubic"` (requires scipy) |
123
+ | `debug` | `False` | Colour-code C-planes for visual validation |
124
+
125
+ ### `Layout.for_size()`
126
+
127
+ ```python
128
+ Layout.for_size(size_px: int) -> Layout
129
+ ```
130
+
131
+ Creates a `Layout` with all dimensions scaled proportionally from the
132
+ 1181 px reference. `Layout.for_size(1181)` is identical to `Layout()`.
133
+
134
+ ### `svg_to_png()` / `svg_to_jpg()`
135
+
136
+ ```python
137
+ svg_to_png(svg_path, png_path=None, *, size_px=1181, background="#FFFFFF") -> Path
138
+ svg_to_jpg(svg_path, jpg_path=None, *, size_px=1181, background="#FFFFFF", quality=95) -> Path
139
+ ```
140
+
141
+ Requires `pip install "eulumdat-plot[export]"`.
142
+
143
+ ## Examples
144
+
145
+ | File | Description |
146
+ |---|---|
147
+ | [`examples/01_basic_usage.md`](examples/01_basic_usage.md) | Generate an SVG from a `.ldt` file |
148
+ | [`examples/02_resize_and_export.md`](examples/02_resize_and_export.md) | Scaling, raster export, batch processing |
149
+
150
+ ## Project structure
151
+
152
+ ```
153
+ eulumdat-plot/
154
+ ├── data/
155
+ │ ├── input/ # sample .ldt files (ISYM 0–4)
156
+ │ └── output/ # generated SVG / PNG / JPEG
157
+ ├── docs/
158
+ │ └── img/
159
+ │ └── sample_01.svg
160
+ ├── examples/
161
+ │ ├── 01_basic_usage.md
162
+ │ └── 02_resize_and_export.md
163
+ ├── src/
164
+ │ └── eulumdat_plot/
165
+ │ ├── __init__.py
166
+ │ ├── plot.py # public API — LDT → SVG pipeline
167
+ │ ├── renderer.py # SVG renderer + Layout dataclass
168
+ │ └── export.py # raster export (PNG / JPEG)
169
+ ├── tests/
170
+ │ ├── test_smoke.py # 46 real LDT files, all ISYM types
171
+ │ └── test_scaling.py # Layout.for_size() proportionality
172
+ ├── pyproject.toml
173
+ ├── CHANGELOG.md
174
+ └── README.md
175
+ ```
176
+
177
+ ## EULUMDAT ecosystem
178
+
179
+ | Package | Status | Description |
180
+ |---|---|---|
181
+ | [`eulumdat-py`](https://pypi.org/project/eulumdat-py/) | v0.1.4 | Read / write EULUMDAT files |
182
+ | [`eulumdat-symmetry`](https://pypi.org/project/eulumdat-symmetry/) | v1.0.0 | Symmetrise EULUMDAT files |
183
+ | `eulumdat-plot` | v1.0.0 | Photometric polar diagram — **this package** |
184
+ | `eulumdat-luminance` | planned | Luminance table cd/m² (γ 55°–85°) |
185
+ | `eulumdat-ugr` | planned | UGR calculation (CIE 117, CIE 190) |
186
+
187
+ ## Requirements
188
+
189
+ - Python ≥ 3.9
190
+ - `eulumdat-py` ≥ 1.0.0
191
+ - `numpy` ≥ 1.21
192
+ - `svgwrite` ≥ 1.4
193
+ - *(optional)* `vl-convert-python` ≥ 1.6 + `Pillow` ≥ 9.0 — raster export
194
+ - *(optional)* `scipy` ≥ 1.7 — cubic spline interpolation
195
+
196
+ ## License
197
+
198
+ MIT — © 2024 [123VincentB](https://github.com/123VincentB)
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "eulumdat-plot"
7
+ version = "1.0.0"
8
+ description = "Photometric polar diagram generator for EULUMDAT (.ldt) files — extension to eulumdat-py"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "123VincentB" }]
12
+ requires-python = ">=3.9"
13
+
14
+ dependencies = [
15
+ "eulumdat-py >= 1.0.0",
16
+ "numpy >= 1.21",
17
+ "svgwrite >= 1.4",
18
+ ]
19
+
20
+ keywords = [
21
+ "eulumdat", "ldt", "photometry", "lighting", "luminaire",
22
+ "polar", "candela", "diagram", "svg", "lumtopic",
23
+ ]
24
+
25
+ classifiers = [
26
+ "Development Status :: 5 - Production/Stable",
27
+ "Intended Audience :: Science/Research",
28
+ "Intended Audience :: Manufacturing",
29
+ "Topic :: Scientific/Engineering :: Physics",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ ]
37
+
38
+ [project.optional-dependencies]
39
+ # Raster export (SVG → PNG / JPEG)
40
+ export = [
41
+ "vl-convert-python >= 1.6",
42
+ "Pillow >= 9.0",
43
+ ]
44
+ # Smooth curve interpolation
45
+ cubic = [
46
+ "scipy >= 1.7",
47
+ ]
48
+ # Everything
49
+ full = [
50
+ "eulumdat-plot[export]",
51
+ "eulumdat-plot[cubic]",
52
+ ]
53
+ # Development tools
54
+ dev = [
55
+ "build",
56
+ "twine",
57
+ "pytest >= 7.0",
58
+ "eulumdat-plot[full]",
59
+ ]
60
+
61
+ [project.urls]
62
+ Homepage = "https://github.com/123VincentB/eulumdat-plot"
63
+ Repository = "https://github.com/123VincentB/eulumdat-plot"
64
+ Issues = "https://github.com/123VincentB/eulumdat-plot/issues"
65
+ Changelog = "https://github.com/123VincentB/eulumdat-plot/blob/main/CHANGELOG.md"
66
+
67
+ [tool.setuptools.packages.find]
68
+ where = ["src"]
69
+
70
+ [tool.pytest.ini_options]
71
+ # Add src/ to sys.path so pytest finds eulumdat_plot without editable install.
72
+ pythonpath = ["src"]
73
+ testpaths = ["tests"]
@@ -0,0 +1,50 @@
1
+ """
2
+ eulumdat-plot — Photometric polar diagram generator for EULUMDAT (.ldt) files.
3
+
4
+ This package is an extension of ``eulumdat-py`` (``pyldt``).
5
+ It reads a ``.ldt`` file and generates a photometric polar diagram in the
6
+ style of the Lumtopic software: a square SVG image with a top banner
7
+ ("LED" / distribution code / "cd / klm") and a polar plot showing the
8
+ C0/C180 distribution (solid curve) and C90/C270 distribution (dotted curve).
9
+
10
+ Quick start
11
+ -----------
12
+ ::
13
+
14
+ from eulumdat_plot import plot_ldt
15
+
16
+ # Minimal — outputs "luminaire.svg" next to the source file.
17
+ svg = plot_ldt("luminaire.ldt")
18
+
19
+ # With distribution code and custom canvas size.
20
+ from eulumdat_plot import Layout
21
+ layout = Layout(width=800, height=800)
22
+ svg = plot_ldt("luminaire.ldt", code="D53", layout=layout)
23
+
24
+ # Raster export (requires the ``[export]`` optional dependency).
25
+ from eulumdat_plot.export import svg_to_png, svg_to_jpg
26
+ png = svg_to_png(svg)
27
+ jpg = svg_to_jpg(svg)
28
+
29
+ Public API
30
+ ----------
31
+ :func:`plot_ldt`
32
+ Main entry point: LDT file → SVG diagram.
33
+ :class:`Layout`
34
+ Dataclass holding all visual parameters (sizes, stroke widths, fonts…).
35
+ :func:`make_svg`
36
+ Low-level renderer: pre-computed NAT curves → SVG.
37
+ Useful if you need to build curves yourself and bypass the LDT pipeline.
38
+ :func:`polar_to_nat`
39
+ Convert polar ``(r, θ)`` to NAT ``(x, y)`` Cartesian coordinates.
40
+ """
41
+
42
+ from .plot import plot_ldt
43
+ from .renderer import Layout, make_svg, polar_to_nat
44
+
45
+ __all__ = [
46
+ "plot_ldt",
47
+ "Layout",
48
+ "make_svg",
49
+ "polar_to_nat",
50
+ ]
@@ -0,0 +1,145 @@
1
+ """
2
+ export.py — Raster export of photometric SVG diagrams.
3
+
4
+ Converts SVG files produced by :func:`plot.plot_ldt` (or any compatible
5
+ SVG) to PNG or JPEG.
6
+
7
+ Dependencies
8
+ ------------
9
+ ``vl-convert-python``
10
+ Pure-Python package embedding a compiled Rust/resvg SVG renderer.
11
+ No native library (DLL/SO) required — the renderer is bundled in the
12
+ wheel. Cross-platform: Windows, Linux, macOS.
13
+ Install with the optional extra::
14
+
15
+ pip install "eulumdat-plot[export]"
16
+
17
+ ``Pillow``
18
+ Used for pixel-exact resizing and JPEG compression.
19
+ Also installed via ``[export]``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import io
25
+ from pathlib import Path
26
+
27
+
28
+ def _check_deps() -> None:
29
+ """Raise a clear ImportError if vl-convert-python or Pillow are missing."""
30
+ missing = []
31
+ for pkg, pip_name in [("vl_convert", "vl-convert-python"), ("PIL", "Pillow")]:
32
+ try:
33
+ __import__(pkg)
34
+ except ImportError:
35
+ missing.append(pip_name)
36
+ if missing:
37
+ raise ImportError(
38
+ f"Missing package(s) for raster export: {', '.join(missing)}\n"
39
+ "Install with: pip install 'eulumdat-plot[export]'"
40
+ )
41
+
42
+
43
+ def _svg_to_pil(svg_path: Path, size_px: int, background: str):
44
+ """
45
+ Convert an SVG file to a square Pillow Image of exactly *size_px* pixels.
46
+
47
+ Uses ``vl_convert`` (resvg, compiled Rust, no native DLL) for rendering,
48
+ then ``Pillow`` for pixel-exact resizing and background compositing.
49
+
50
+ Parameters
51
+ ----------
52
+ svg_path : Path to the source SVG file.
53
+ size_px : Target output size in pixels (square).
54
+ background : Background colour as a CSS hex string (e.g. ``"#FFFFFF"``).
55
+
56
+ Returns
57
+ -------
58
+ PIL.Image.Image in RGB mode, size (size_px, size_px).
59
+ """
60
+ _check_deps()
61
+
62
+ import vl_convert as vlc
63
+ from PIL import Image
64
+
65
+ svg_content = svg_path.read_text(encoding="utf-8")
66
+
67
+ # Read the SVG canvas size from the file to compute the scale factor.
68
+ # Layout always writes width="N" height="N" as integers.
69
+ native_size = size_px # fallback if parsing fails
70
+ import re
71
+ m = re.search(r'<svg[^>]+width="(\d+)"', svg_content)
72
+ if m:
73
+ native_size = int(m.group(1))
74
+
75
+ scale = size_px / native_size
76
+ png_bytes = vlc.svg_to_png(svg_content, scale=scale)
77
+
78
+ img = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
79
+
80
+ # Resize to exactly size_px × size_px (float scale may be off by 1 px).
81
+ if img.size != (size_px, size_px):
82
+ img = img.resize((size_px, size_px), Image.LANCZOS)
83
+
84
+ # Composite over solid background.
85
+ bg = Image.new("RGBA", (size_px, size_px), background)
86
+ bg.paste(img, mask=img)
87
+ return bg.convert("RGB")
88
+
89
+
90
+ def svg_to_png(
91
+ svg_path: str | Path,
92
+ png_path: str | Path | None = None,
93
+ *,
94
+ size_px: int = 1181,
95
+ background: str = "#FFFFFF",
96
+ ) -> Path:
97
+ """
98
+ Convert an SVG file to PNG.
99
+
100
+ Parameters
101
+ ----------
102
+ svg_path : Path to the source SVG file.
103
+ png_path : Destination path. Defaults to svg_path with .png extension.
104
+ size_px : Output width and height in pixels (square output).
105
+ background : Background fill colour as a CSS hex string. Default: #FFFFFF.
106
+
107
+ Returns
108
+ -------
109
+ Absolute path to the generated PNG file.
110
+ """
111
+ svg_path = Path(svg_path)
112
+ png_path = Path(png_path) if png_path is not None else svg_path.with_suffix(".png")
113
+ img = _svg_to_pil(svg_path, size_px, background)
114
+ img.save(png_path, "PNG", optimize=True)
115
+ return png_path.resolve()
116
+
117
+
118
+ def svg_to_jpg(
119
+ svg_path: str | Path,
120
+ jpg_path: str | Path | None = None,
121
+ *,
122
+ size_px: int = 1181,
123
+ background: str = "#FFFFFF",
124
+ quality: int = 95,
125
+ ) -> Path:
126
+ """
127
+ Convert an SVG file to JPEG.
128
+
129
+ Parameters
130
+ ----------
131
+ svg_path : Path to the source SVG file.
132
+ jpg_path : Destination path. Defaults to svg_path with .jpg extension.
133
+ size_px : Output width and height in pixels (square output).
134
+ background : Background fill colour (CSS hex). Default: #FFFFFF.
135
+ quality : JPEG compression quality (1-100). Default: 95.
136
+
137
+ Returns
138
+ -------
139
+ Absolute path to the generated JPEG file.
140
+ """
141
+ svg_path = Path(svg_path)
142
+ jpg_path = Path(jpg_path) if jpg_path is not None else svg_path.with_suffix(".jpg")
143
+ img = _svg_to_pil(svg_path, size_px, background)
144
+ img.save(jpg_path, "JPEG", quality=quality, optimize=True)
145
+ return jpg_path.resolve()