weasyvsdx 0.1.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.
Files changed (39) hide show
  1. weasyvsdx-0.1.0/.gitignore +52 -0
  2. weasyvsdx-0.1.0/CHANGELOG.md +50 -0
  3. weasyvsdx-0.1.0/LICENSE +21 -0
  4. weasyvsdx-0.1.0/PKG-INFO +275 -0
  5. weasyvsdx-0.1.0/README.md +238 -0
  6. weasyvsdx-0.1.0/pyproject.toml +113 -0
  7. weasyvsdx-0.1.0/src/weasyvsdx/__init__.py +88 -0
  8. weasyvsdx-0.1.0/src/weasyvsdx/convert.py +223 -0
  9. weasyvsdx-0.1.0/src/weasyvsdx/errors.py +5 -0
  10. weasyvsdx-0.1.0/src/weasyvsdx/fetch.py +99 -0
  11. weasyvsdx-0.1.0/src/weasyvsdx/geometry.py +630 -0
  12. weasyvsdx-0.1.0/src/weasyvsdx/gradients.py +252 -0
  13. weasyvsdx-0.1.0/src/weasyvsdx/hatch.py +101 -0
  14. weasyvsdx-0.1.0/src/weasyvsdx/hidden.py +48 -0
  15. weasyvsdx-0.1.0/src/weasyvsdx/hidden_text.py +82 -0
  16. weasyvsdx-0.1.0/src/weasyvsdx/images.py +204 -0
  17. weasyvsdx-0.1.0/src/weasyvsdx/markers.py +193 -0
  18. weasyvsdx-0.1.0/src/weasyvsdx/package.py +207 -0
  19. weasyvsdx-0.1.0/src/weasyvsdx/pathdata.py +77 -0
  20. weasyvsdx-0.1.0/src/weasyvsdx/paths.py +105 -0
  21. weasyvsdx-0.1.0/src/weasyvsdx/py.typed +0 -0
  22. weasyvsdx-0.1.0/src/weasyvsdx/raw.py +101 -0
  23. weasyvsdx-0.1.0/src/weasyvsdx/text.py +352 -0
  24. weasyvsdx-0.1.0/tests/conftest.py +5 -0
  25. weasyvsdx-0.1.0/tests/helpers.py +168 -0
  26. weasyvsdx-0.1.0/tests/test_convert.py +233 -0
  27. weasyvsdx-0.1.0/tests/test_fetch.py +118 -0
  28. weasyvsdx-0.1.0/tests/test_geometry.py +304 -0
  29. weasyvsdx-0.1.0/tests/test_gradients.py +229 -0
  30. weasyvsdx-0.1.0/tests/test_hatch.py +114 -0
  31. weasyvsdx-0.1.0/tests/test_hidden.py +27 -0
  32. weasyvsdx-0.1.0/tests/test_hidden_text.py +117 -0
  33. weasyvsdx-0.1.0/tests/test_images.py +212 -0
  34. weasyvsdx-0.1.0/tests/test_install.py +93 -0
  35. weasyvsdx-0.1.0/tests/test_markers.py +164 -0
  36. weasyvsdx-0.1.0/tests/test_pathdata.py +30 -0
  37. weasyvsdx-0.1.0/tests/test_paths.py +70 -0
  38. weasyvsdx-0.1.0/tests/test_raw.py +61 -0
  39. weasyvsdx-0.1.0/tests/test_text.py +268 -0
@@ -0,0 +1,52 @@
1
+ # IDE
2
+ .idea/
3
+ .vscode/
4
+ *.swp
5
+ *.swo
6
+
7
+ # macOS
8
+ .DS_Store
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # Virtual environments
33
+ .venv/
34
+ venv/
35
+ ENV/
36
+
37
+ # Testing
38
+ .pytest_cache/
39
+ .coverage
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+
44
+ # Type checking
45
+ .mypy_cache/
46
+
47
+ # Ruff
48
+ .ruff_cache/
49
+ coverage.xml
50
+
51
+ # Visual regression bench output
52
+ tools/visual/out/
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-24
11
+
12
+ ### Added
13
+
14
+ - Visio drawings (VSDX, VSDM, VSD and the templates and stencils of Visio) render in WeasyPrint as vector
15
+ SVG: an `<img>`, `<object>` or `<embed>`, a CSS image, an `<image>` inside an SVG. `vsd2xhtml` of
16
+ libvisio draws the first page. An untyped response is a drawing by the suffix of its URL, before or after
17
+ a redirect.
18
+ - The text of a drawing wraps and aligns as in Visio: each text block is HTML in a `<foreignObject>`, from
19
+ the output of `vsd2raw`, with its padding, vertical alignment, rotation, paragraphs, bullets and fonts,
20
+ and the character scale of Visio, which widens the glyphs.
21
+ - Arrowheads take their own shape and size, and a line under one ends at its base, as in Visio. A hollow
22
+ one hides the line under it.
23
+ - A shape on a hidden or non-printing layer is left out, as in Visio.
24
+ - The text a shape of a VSDX hides is left out: the names of the Azure icons no longer show under them.
25
+ - Black behind dark text, which libvisio reads from a palette color, draws white: labels no longer show as
26
+ black bars.
27
+ - A text with a line break of Visio (U+2028) shows, broken at it, instead of going missing.
28
+ - A bitmap inside an EMF that Office draws flipped no longer shows mirrored.
29
+ - A fill pattern draws as hatch lines, on the fill color where it has one, instead of black.
30
+ - A shape of a VSDX that fills with a gradient of Visio 2013 or later gets it, linear or radial, instead of
31
+ black: the icons of Azure show in color.
32
+ - A shape of a VSDX that inherits its geometry from a master group draws, from the XML of the master:
33
+ libvisio drew nothing for it, and parts of the Azure icons went missing.
34
+ - A text block of no width or height is as wide and as high as its text and never wraps, as in Visio.
35
+ - A path that libvisio writes without a leading moveto starts at the end of its first segment, not at the
36
+ corner of the page.
37
+ - EMF images become SVG through `emf2svg-conv`, and so does the EMF that Office puts inside a WMF image,
38
+ and the picture an embedded OLE object shows.
39
+ - The tools run in processes of their own, with a timeout and, on Linux, limits on memory and output.
40
+ The EMF images of a page share one timeout.
41
+ A drawing that does not convert renders as a broken image.
42
+ - A drawing that libvisio draws as a page of no size renders as a broken image.
43
+ - `install()` warns when `vsd2xhtml` reports another libvisio than 0.1.7, the release weasyvsdx is tested
44
+ with: 0.1.8 to 0.1.11 fill the shapes of a VSDX with the colors of its theme over their own.
45
+ - A font of Microsoft that the system lacks falls back to its free replacement: Calibri to Carlito, Cambria to
46
+ Caladea, Segoe UI to Selawik.
47
+ - A comparison page on GitHub Pages shows each drawing of the corpus in LibreOffice, as libvisio writes it,
48
+ and in weasyvsdx.
49
+ - The bench also compares the Azure diagrams with the SVG that Visio exported for their article.
50
+ LibreOffice reads them through libvisio and shares its errors.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergey Grigoriev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.5
2
+ Name: weasyvsdx
3
+ Version: 0.1.0
4
+ Summary: Visio drawings (VSDX, VSD) for WeasyPrint, drawn as vector SVG through libvisio.
5
+ Project-URL: Homepage, https://github.com/grigoriev/weasyvsdx
6
+ Project-URL: Repository, https://github.com/grigoriev/weasyvsdx
7
+ Project-URL: Issues, https://github.com/grigoriev/weasyvsdx/issues
8
+ Author-email: Sergey Grigoriev <grigoriev@fastmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
22
+ Classifier: Topic :: Printing
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: olefile>=0.47
26
+ Requires-Dist: weasyprint<71,>=70
27
+ Requires-Dist: weasysvg>=0.3.1
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy==2.4.0; extra == 'dev'
30
+ Requires-Dist: pypdfium2==5.14.0; extra == 'dev'
31
+ Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
32
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
33
+ Requires-Dist: ruff==0.17.2; extra == 'dev'
34
+ Provides-Extra: visual
35
+ Requires-Dist: numpy==2.5.4; (python_version >= '3.12') and extra == 'visual'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # weasyvsdx
39
+
40
+ [![CI](https://github.com/grigoriev/weasyvsdx/actions/workflows/ci.yml/badge.svg)](https://github.com/grigoriev/weasyvsdx/actions/workflows/ci.yml)
41
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/grigoriev/weasyvsdx/badge)](https://scorecard.dev/viewer/?uri=github.com/grigoriev/weasyvsdx)
42
+ [![Release](https://img.shields.io/github/v/release/grigoriev/weasyvsdx)](https://github.com/grigoriev/weasyvsdx/releases)
43
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
44
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
45
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/grigoriev/weasyvsdx/blob/main/LICENSE)
47
+
48
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=grigoriev_weasyvsdx&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=grigoriev_weasyvsdx)
49
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=grigoriev_weasyvsdx&metric=coverage)](https://sonarcloud.io/summary/new_code?id=grigoriev_weasyvsdx)
50
+
51
+ A plugin for [WeasyPrint](https://weasyprint.org) 70.x that draws Microsoft Visio drawings (VSDX, VSD) as
52
+ vector SVG, through [libvisio](https://github.com/LibreOffice/libvisio).
53
+
54
+ ## Why?
55
+
56
+ WeasyPrint cannot read Visio drawings. Services that turn HTML into PDF convert them with LibreOffice
57
+ first, to a PNG. LibreOffice adds some 730 MB to an image and hundreds of findings to its scans, and the
58
+ drawing ends up as pixels.
59
+
60
+ weasyvsdx hands each drawing to the tools of libvisio, the library LibreOffice itself imports Visio with,
61
+ and draws the result as SVG inside WeasyPrint. The drawing stays vector and its text stays selectable. The
62
+ tools take some 41 MB on Debian.
63
+
64
+ ## Installation
65
+
66
+ weasyvsdx runs on CPython 3.11 to 3.14.
67
+
68
+ Install the tools of libvisio, and emf2svg for the EMF images that many drawings hold. On Debian 13 and
69
+ Ubuntu 24.04, which have the libvisio weasyvsdx needs (see "libvisio version"):
70
+
71
+ ```sh
72
+ apt-get install libvisio-tools emf2svg
73
+ ```
74
+
75
+ Then install weasyvsdx from PyPI:
76
+
77
+ ```sh
78
+ pip install weasyvsdx
79
+ ```
80
+
81
+ weasyvsdx depends on WeasyPrint and on [weasysvg](https://github.com/grigoriev/weasysvg), which draws the
82
+ text of the drawings. WeasyPrint needs Pango, see its
83
+ [installation guide](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html).
84
+
85
+ ### libvisio version
86
+
87
+ Use libvisio 0.1.7. The tests and the bench run with it, and `install()` warns on any other release.
88
+ libvisio 0.1.8 to 0.1.11, the latest, fill the shapes of a VSDX with the colors of its theme over their own
89
+ colors: on the Azure diagrams of the bench, icons turn into olive disks and subnets into olive boxes.
90
+
91
+ | Distribution | libvisio |
92
+ |---|---|
93
+ | Debian 13 (trixie) | 0.1.7 |
94
+ | Ubuntu 24.04 | 0.1.7 |
95
+ | Debian testing, unstable | 0.1.11 |
96
+ | Ubuntu 26.04 | 0.1.10 |
97
+ | Alpine 3.22, 3.23 | 0.1.8 |
98
+ | Wolfi | none |
99
+
100
+ To keep it, pin the release of the base image, for example `python:3.14-slim-trixie`, and check it at
101
+ build time:
102
+
103
+ ```sh
104
+ test "$(vsd2xhtml --version)" = "vsd2xhtml 0.1.7"
105
+ ```
106
+
107
+ libvisio 0.1.9 and later also read the text background of a VSD, which 0.1.7 reads as black. weasyvsdx
108
+ draws black behind dark text as white, the most common background, so the labels show. A colored
109
+ background draws white as well.
110
+
111
+ ### Verify
112
+
113
+ The build of each release has signed provenance on GitHub, and the GitHub release carries its bundle.
114
+ PyPI and the GitHub release hold the same files, so verify a file from either with
115
+ `gh attestation verify <file> --repo grigoriev/weasyvsdx`. On PyPI each file also has a publish
116
+ attestation (PEP 740): it ties the upload to `release.yml` of this repository.
117
+
118
+ ## Usage
119
+
120
+ Call `install()` once, before the first rendering:
121
+
122
+ ```python
123
+ import weasyprint
124
+ import weasyvsdx
125
+
126
+ weasyvsdx.install()
127
+ weasyprint.HTML(string='<img src="plan.vsdx">', base_url=".").write_pdf("plan.pdf")
128
+ ```
129
+
130
+ An image renders as a Visio drawing when its media type says so, `application/vnd.ms-visio.drawing` for
131
+ VSDX or `application/vnd.visio` for VSD, or when a file or URL without a type ends in `.vsdx`, `.vsd` or
132
+ another Visio suffix. A data URL counts by its type alone. This works for `<img>`, `<object>`, `<embed>`,
133
+ CSS images and `<image>` inside an SVG.
134
+
135
+ `install()` patches WeasyPrint in place, for the whole process, and returns the path of `vsd2xhtml`. It
136
+ raises `ConverterNotFoundError` when `vsd2xhtml` is not on the PATH. A later call changes the settings.
137
+
138
+ | Argument | Default | Effect |
139
+ |---|---|---|
140
+ | `converter` | `"vsd2xhtml"` | the `vsd2xhtml` to run, a name on the PATH or a path |
141
+ | `text` | `True` | lay the text out anew from `vsd2raw`, next to `vsd2xhtml`; installs weasysvg with its defaults unless its `foreign_object` is installed |
142
+ | `emf` | `True` | turn EMF images into SVG through `emf2svg-conv`, where it is on the PATH |
143
+ | `timeout` | `30.0` | seconds one run of a tool may take |
144
+ | `max_input_mb` | `64` | the largest drawing, in MB |
145
+ | `max_output_mb` | `256` | the most a tool may write, in MB (Linux) |
146
+ | `max_memory_mb` | `2048` | the address space of each tool, in MB (Linux) |
147
+
148
+ A drawing that does not convert, or that passes a limit, renders as a broken image: WeasyPrint logs the
149
+ error and leaves it out, and the rest of the document renders.
150
+
151
+ ## Fonts
152
+
153
+ Visio drawings ask for fonts of Microsoft, Segoe UI and Calibri most. weasyvsdx names a free replacement
154
+ after each, so an installed original still wins:
155
+
156
+ | Font | Replacement | Debian package |
157
+ |---|---|---|
158
+ | Calibri | Carlito, same metrics | `fonts-crosextra-carlito` |
159
+ | Cambria | Caladea, same metrics | `fonts-crosextra-caladea` |
160
+ | Segoe UI, Light, Semilight, Semibold | Selawik, close, not equal | none: the [release of microsoft/Selawik](https://github.com/microsoft/Selawik/releases) |
161
+
162
+ Without them, text falls back to the sans-serif font of the system, which breaks lines elsewhere than Visio.
163
+ Where the license allows it, install the original fonts instead.
164
+
165
+ ## How it works
166
+
167
+ 1. `vsd2xhtml` of libvisio writes each page of the drawing as SVG. weasyvsdx takes the first page, as
168
+ LibreOffice does.
169
+ 2. The generic SVG generator behind `vsd2xhtml` drops part of what libvisio reads. `vsd2raw` prints the
170
+ same calls with all their properties, and weasyvsdx takes the rest from there:
171
+ - **Text**: each text block becomes a `<foreignObject>` of its size, with its padding, vertical
172
+ alignment and rotation, and its paragraphs and spans as HTML: alignment, line spacing, indents, bullets,
173
+ font, size, scale, weight, style, color, underline, highlight, super- and subscript. WeasyPrint lays it out,
174
+ so labels wrap inside their shapes as in Visio. The generator wrote each text as one line at the left
175
+ edge of its block.
176
+ - **Arrowheads**: each marker gets the shape and size of its line end. The generator drew one stock
177
+ arrow for all of them, sized by the width of the line.
178
+ - **Hidden shapes**: a shape on a hidden or non-printing layer goes. The generator drew it.
179
+ - **Hidden text**: the text a shape of a VSDX hides (HideText, or a text cleared on the shape) goes. The
180
+ generator wrote the name of each Azure icon under it.
181
+ - **Hatches**: a fill pattern of Visio draws as the lines of its hatch. The generator filled it black.
182
+ - **Gradients**: a shape of a VSDX that fills with a gradient of Visio 2013 or later gets it, linear or
183
+ radial, from the XML of the page, its masters and styles. libvisio reads none, and filled it black.
184
+ - **Shapes of master groups**: a shape of a VSDX that inherits its geometry from a master group is drawn
185
+ from the XML of the master, with its fill, line, dashes and gradient. libvisio drew nothing for it, and
186
+ parts of the Azure icons went missing.
187
+ - **Line ends**: a line under an arrowhead ends at its base, as in Visio, not at its tip.
188
+ 3. EMF images become SVG through `emf2svg-conv`, and so does the EMF that Office puts inside a WMF image,
189
+ and the picture an embedded OLE object shows.
190
+ WeasyPrint reads neither, and left them out.
191
+ 4. A path that starts without a moveto loses its first segment, whose start libvisio does not give. Drawn as
192
+ it was, it ran from the corner of the page.
193
+
194
+ The tools parse untrusted input in C, and a broken file can crash them. Each runs in a process of its own,
195
+ in a temporary directory, with a time limit and, on Linux, limits on its memory and its output.
196
+ `vsd2xhtml` and `vsd2raw` run at the same time. The SVG then goes through WeasyPrint like any other SVG,
197
+ and every URL inside it through the URL fetcher of the caller.
198
+
199
+ ## How it compares to LibreOffice and Visio
200
+
201
+ The bench in [`tools/visual`](https://github.com/grigoriev/weasyvsdx/tree/main/tools/visual) renders 291
202
+ drawings and compares each with its PNG export by LibreOffice 25.2:
203
+
204
+ - the 41 test drawings of libvisio, Apache POI and LibreOffice in [`corpus/visio`](https://github.com/grigoriev/weasyvsdx/tree/main/corpus/visio);
205
+ - the 250 Visio diagrams of the Azure Architecture Center, which the bench downloads into a cache: their
206
+ icons may not be copied into this repository, see [`corpus/SOURCES.md`](https://github.com/grigoriev/weasyvsdx/blob/main/corpus/SOURCES.md).
207
+
208
+ The [comparison page](https://grigoriev.github.io/weasyvsdx/corpus/comparison.html) shows each drawing of the
209
+ corpus in LibreOffice, as libvisio writes it, and in weasyvsdx, side by side, with a swipe between any two;
210
+ [`corpus/COMPARISON.md`](https://github.com/grigoriev/weasyvsdx/blob/main/corpus/COMPARISON.md) holds the same in the repository. The Azure
211
+ diagrams are not on it: their icons may not be redistributed.
212
+
213
+ All but two convert: libvisio crashes on `recursion-cycle.vsdx` and refuses `tab-short-prefix.vsdx`, two
214
+ of its tests that are broken on purpose. LibreOffice fails on the same two. Geometry, fills, gradients,
215
+ dashes, arrowheads and images match. The text differs most, and LibreOffice is no exact reference for it:
216
+ it lays text out with fonts of its own choice, and breaks words in narrow blocks where Visio does not. On
217
+ some drawings LibreOffice is wrong where weasyvsdx is right, for example it fills the whole of
218
+ `netapp-files-moodle-zonal` blue.
219
+
220
+ LibreOffice reads the drawings through libvisio, as weasyvsdx does, and shares its errors. So the bench
221
+ also compares 206 Azure diagrams with the SVG that Visio exported for their article. The median mismatch
222
+ is 25.0% against Visio, 49.5% against LibreOffice: weasyvsdx draws the gradients and the master
223
+ shapes of the Azure icons, which LibreOffice fills black or leaves out. Much of the rest is the icons of Azure, see "Known gaps".
224
+
225
+ The median Azure diagram converts in 0.08 s, and 9 in 10 in less than 0.5 s. Drawings that embed bitmaps
226
+ of tens of MB take seconds, 17 of the 250 more than one: libvisio writes the bitmaps out as base64. One,
227
+ `adaptive-apps`, takes 46 s, more than the default timeout of 30 s: it converts only with a larger `timeout`.
228
+
229
+ ## Known gaps
230
+
231
+ - A shape whose ID another shape that libvisio draws shares keeps what libvisio drew: no gradient, and no
232
+ geometry from its master. So do the shapes of a VSD, gradients of the rarer kinds (FillGradientDir 8 to
233
+ 13) and colors of the theme of a drawing.
234
+ - Only the first page renders. LibreOffice and the converters before it did the same.
235
+ - Visio drawings ask for Calibri and Segoe UI most, fonts of Microsoft. A font the system lacks falls back
236
+ to its free replacement, else to sans-serif: Calibri to Carlito and Cambria to Caladea, which share their
237
+ metrics, so text breaks where Visio breaks it; Segoe UI to Selawik, the open fallback of Microsoft, close
238
+ to it but not equal. Install them, see "Fonts".
239
+ - Character scaling (the "Scale" of a Visio font) applies where all text of a block shares one scale; a block
240
+ of mixed scales draws unscaled. Fields show the value libvisio formats.
241
+ - A WMF image without an EMF inside is left out, and so is an embedded OLE object whose picture is no EMF.
242
+ Office writes the EMF into each WMF it saves, and into the picture of each OLE object.
243
+ - Text that a VSD hides stays: only a VSDX holds HideText where weasyvsdx reads it.
244
+ - A shape on a hidden or non-printing layer goes, but its text would stay: libvisio marks the path of
245
+ such a shape as hidden, not its text.
246
+ - An EMF draws its GDI records only. Part of an icon that Office drew in EMF+ alone, the newer records an EMF
247
+ can carry in its comments, stays out: `emf2svg-conv` does not read EMF+, LibreOffice does.
248
+
249
+ ## Development
250
+
251
+ Tests and the bench run in Docker, since WeasyPrint needs Pango and weasyvsdx the tools of libvisio. See
252
+ [CONTRIBUTING.md](https://github.com/grigoriev/weasyvsdx/blob/main/CONTRIBUTING.md).
253
+
254
+ ```sh
255
+ make lint # ruff and mypy, on the host
256
+ make test # pytest with coverage, in Docker
257
+ make visual # the visual bench, report in tools/visual/out/report.html
258
+ make comparison # corpus/COMPARISON.md and corpus/comparison.html, the panels in corpus/render
259
+ ```
260
+
261
+ ## Contributing
262
+
263
+ See [CONTRIBUTING.md](https://github.com/grigoriev/weasyvsdx/blob/main/CONTRIBUTING.md). Report vulnerabilities privately, see [SECURITY.md](https://github.com/grigoriev/weasyvsdx/blob/main/SECURITY.md).
264
+
265
+ ## Disclaimer
266
+
267
+ This software is provided "as is", without warranty of any kind, as the LICENSE states. Use
268
+ it at your own risk. Sergey Grigoriev is not liable for damage from its use, as far as the law
269
+ allows. It is published free of charge, outside of any commercial offering, with no
270
+ obligation to support it. Security reports are welcome, see [SECURITY.md](https://github.com/grigoriev/weasyvsdx/blob/main/SECURITY.md).
271
+
272
+ ## License
273
+
274
+ MIT, see [LICENSE](https://github.com/grigoriev/weasyvsdx/blob/main/LICENSE). The test corpus keeps the licenses of its sources, see
275
+ [corpus/SOURCES.md](https://github.com/grigoriev/weasyvsdx/blob/main/corpus/SOURCES.md).
@@ -0,0 +1,238 @@
1
+ # weasyvsdx
2
+
3
+ [![CI](https://github.com/grigoriev/weasyvsdx/actions/workflows/ci.yml/badge.svg)](https://github.com/grigoriev/weasyvsdx/actions/workflows/ci.yml)
4
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/grigoriev/weasyvsdx/badge)](https://scorecard.dev/viewer/?uri=github.com/grigoriev/weasyvsdx)
5
+ [![Release](https://img.shields.io/github/v/release/grigoriev/weasyvsdx)](https://github.com/grigoriev/weasyvsdx/releases)
6
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
7
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/grigoriev/weasyvsdx/blob/main/LICENSE)
10
+
11
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=grigoriev_weasyvsdx&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=grigoriev_weasyvsdx)
12
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=grigoriev_weasyvsdx&metric=coverage)](https://sonarcloud.io/summary/new_code?id=grigoriev_weasyvsdx)
13
+
14
+ A plugin for [WeasyPrint](https://weasyprint.org) 70.x that draws Microsoft Visio drawings (VSDX, VSD) as
15
+ vector SVG, through [libvisio](https://github.com/LibreOffice/libvisio).
16
+
17
+ ## Why?
18
+
19
+ WeasyPrint cannot read Visio drawings. Services that turn HTML into PDF convert them with LibreOffice
20
+ first, to a PNG. LibreOffice adds some 730 MB to an image and hundreds of findings to its scans, and the
21
+ drawing ends up as pixels.
22
+
23
+ weasyvsdx hands each drawing to the tools of libvisio, the library LibreOffice itself imports Visio with,
24
+ and draws the result as SVG inside WeasyPrint. The drawing stays vector and its text stays selectable. The
25
+ tools take some 41 MB on Debian.
26
+
27
+ ## Installation
28
+
29
+ weasyvsdx runs on CPython 3.11 to 3.14.
30
+
31
+ Install the tools of libvisio, and emf2svg for the EMF images that many drawings hold. On Debian 13 and
32
+ Ubuntu 24.04, which have the libvisio weasyvsdx needs (see "libvisio version"):
33
+
34
+ ```sh
35
+ apt-get install libvisio-tools emf2svg
36
+ ```
37
+
38
+ Then install weasyvsdx from PyPI:
39
+
40
+ ```sh
41
+ pip install weasyvsdx
42
+ ```
43
+
44
+ weasyvsdx depends on WeasyPrint and on [weasysvg](https://github.com/grigoriev/weasysvg), which draws the
45
+ text of the drawings. WeasyPrint needs Pango, see its
46
+ [installation guide](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html).
47
+
48
+ ### libvisio version
49
+
50
+ Use libvisio 0.1.7. The tests and the bench run with it, and `install()` warns on any other release.
51
+ libvisio 0.1.8 to 0.1.11, the latest, fill the shapes of a VSDX with the colors of its theme over their own
52
+ colors: on the Azure diagrams of the bench, icons turn into olive disks and subnets into olive boxes.
53
+
54
+ | Distribution | libvisio |
55
+ |---|---|
56
+ | Debian 13 (trixie) | 0.1.7 |
57
+ | Ubuntu 24.04 | 0.1.7 |
58
+ | Debian testing, unstable | 0.1.11 |
59
+ | Ubuntu 26.04 | 0.1.10 |
60
+ | Alpine 3.22, 3.23 | 0.1.8 |
61
+ | Wolfi | none |
62
+
63
+ To keep it, pin the release of the base image, for example `python:3.14-slim-trixie`, and check it at
64
+ build time:
65
+
66
+ ```sh
67
+ test "$(vsd2xhtml --version)" = "vsd2xhtml 0.1.7"
68
+ ```
69
+
70
+ libvisio 0.1.9 and later also read the text background of a VSD, which 0.1.7 reads as black. weasyvsdx
71
+ draws black behind dark text as white, the most common background, so the labels show. A colored
72
+ background draws white as well.
73
+
74
+ ### Verify
75
+
76
+ The build of each release has signed provenance on GitHub, and the GitHub release carries its bundle.
77
+ PyPI and the GitHub release hold the same files, so verify a file from either with
78
+ `gh attestation verify <file> --repo grigoriev/weasyvsdx`. On PyPI each file also has a publish
79
+ attestation (PEP 740): it ties the upload to `release.yml` of this repository.
80
+
81
+ ## Usage
82
+
83
+ Call `install()` once, before the first rendering:
84
+
85
+ ```python
86
+ import weasyprint
87
+ import weasyvsdx
88
+
89
+ weasyvsdx.install()
90
+ weasyprint.HTML(string='<img src="plan.vsdx">', base_url=".").write_pdf("plan.pdf")
91
+ ```
92
+
93
+ An image renders as a Visio drawing when its media type says so, `application/vnd.ms-visio.drawing` for
94
+ VSDX or `application/vnd.visio` for VSD, or when a file or URL without a type ends in `.vsdx`, `.vsd` or
95
+ another Visio suffix. A data URL counts by its type alone. This works for `<img>`, `<object>`, `<embed>`,
96
+ CSS images and `<image>` inside an SVG.
97
+
98
+ `install()` patches WeasyPrint in place, for the whole process, and returns the path of `vsd2xhtml`. It
99
+ raises `ConverterNotFoundError` when `vsd2xhtml` is not on the PATH. A later call changes the settings.
100
+
101
+ | Argument | Default | Effect |
102
+ |---|---|---|
103
+ | `converter` | `"vsd2xhtml"` | the `vsd2xhtml` to run, a name on the PATH or a path |
104
+ | `text` | `True` | lay the text out anew from `vsd2raw`, next to `vsd2xhtml`; installs weasysvg with its defaults unless its `foreign_object` is installed |
105
+ | `emf` | `True` | turn EMF images into SVG through `emf2svg-conv`, where it is on the PATH |
106
+ | `timeout` | `30.0` | seconds one run of a tool may take |
107
+ | `max_input_mb` | `64` | the largest drawing, in MB |
108
+ | `max_output_mb` | `256` | the most a tool may write, in MB (Linux) |
109
+ | `max_memory_mb` | `2048` | the address space of each tool, in MB (Linux) |
110
+
111
+ A drawing that does not convert, or that passes a limit, renders as a broken image: WeasyPrint logs the
112
+ error and leaves it out, and the rest of the document renders.
113
+
114
+ ## Fonts
115
+
116
+ Visio drawings ask for fonts of Microsoft, Segoe UI and Calibri most. weasyvsdx names a free replacement
117
+ after each, so an installed original still wins:
118
+
119
+ | Font | Replacement | Debian package |
120
+ |---|---|---|
121
+ | Calibri | Carlito, same metrics | `fonts-crosextra-carlito` |
122
+ | Cambria | Caladea, same metrics | `fonts-crosextra-caladea` |
123
+ | Segoe UI, Light, Semilight, Semibold | Selawik, close, not equal | none: the [release of microsoft/Selawik](https://github.com/microsoft/Selawik/releases) |
124
+
125
+ Without them, text falls back to the sans-serif font of the system, which breaks lines elsewhere than Visio.
126
+ Where the license allows it, install the original fonts instead.
127
+
128
+ ## How it works
129
+
130
+ 1. `vsd2xhtml` of libvisio writes each page of the drawing as SVG. weasyvsdx takes the first page, as
131
+ LibreOffice does.
132
+ 2. The generic SVG generator behind `vsd2xhtml` drops part of what libvisio reads. `vsd2raw` prints the
133
+ same calls with all their properties, and weasyvsdx takes the rest from there:
134
+ - **Text**: each text block becomes a `<foreignObject>` of its size, with its padding, vertical
135
+ alignment and rotation, and its paragraphs and spans as HTML: alignment, line spacing, indents, bullets,
136
+ font, size, scale, weight, style, color, underline, highlight, super- and subscript. WeasyPrint lays it out,
137
+ so labels wrap inside their shapes as in Visio. The generator wrote each text as one line at the left
138
+ edge of its block.
139
+ - **Arrowheads**: each marker gets the shape and size of its line end. The generator drew one stock
140
+ arrow for all of them, sized by the width of the line.
141
+ - **Hidden shapes**: a shape on a hidden or non-printing layer goes. The generator drew it.
142
+ - **Hidden text**: the text a shape of a VSDX hides (HideText, or a text cleared on the shape) goes. The
143
+ generator wrote the name of each Azure icon under it.
144
+ - **Hatches**: a fill pattern of Visio draws as the lines of its hatch. The generator filled it black.
145
+ - **Gradients**: a shape of a VSDX that fills with a gradient of Visio 2013 or later gets it, linear or
146
+ radial, from the XML of the page, its masters and styles. libvisio reads none, and filled it black.
147
+ - **Shapes of master groups**: a shape of a VSDX that inherits its geometry from a master group is drawn
148
+ from the XML of the master, with its fill, line, dashes and gradient. libvisio drew nothing for it, and
149
+ parts of the Azure icons went missing.
150
+ - **Line ends**: a line under an arrowhead ends at its base, as in Visio, not at its tip.
151
+ 3. EMF images become SVG through `emf2svg-conv`, and so does the EMF that Office puts inside a WMF image,
152
+ and the picture an embedded OLE object shows.
153
+ WeasyPrint reads neither, and left them out.
154
+ 4. A path that starts without a moveto loses its first segment, whose start libvisio does not give. Drawn as
155
+ it was, it ran from the corner of the page.
156
+
157
+ The tools parse untrusted input in C, and a broken file can crash them. Each runs in a process of its own,
158
+ in a temporary directory, with a time limit and, on Linux, limits on its memory and its output.
159
+ `vsd2xhtml` and `vsd2raw` run at the same time. The SVG then goes through WeasyPrint like any other SVG,
160
+ and every URL inside it through the URL fetcher of the caller.
161
+
162
+ ## How it compares to LibreOffice and Visio
163
+
164
+ The bench in [`tools/visual`](https://github.com/grigoriev/weasyvsdx/tree/main/tools/visual) renders 291
165
+ drawings and compares each with its PNG export by LibreOffice 25.2:
166
+
167
+ - the 41 test drawings of libvisio, Apache POI and LibreOffice in [`corpus/visio`](https://github.com/grigoriev/weasyvsdx/tree/main/corpus/visio);
168
+ - the 250 Visio diagrams of the Azure Architecture Center, which the bench downloads into a cache: their
169
+ icons may not be copied into this repository, see [`corpus/SOURCES.md`](https://github.com/grigoriev/weasyvsdx/blob/main/corpus/SOURCES.md).
170
+
171
+ The [comparison page](https://grigoriev.github.io/weasyvsdx/corpus/comparison.html) shows each drawing of the
172
+ corpus in LibreOffice, as libvisio writes it, and in weasyvsdx, side by side, with a swipe between any two;
173
+ [`corpus/COMPARISON.md`](https://github.com/grigoriev/weasyvsdx/blob/main/corpus/COMPARISON.md) holds the same in the repository. The Azure
174
+ diagrams are not on it: their icons may not be redistributed.
175
+
176
+ All but two convert: libvisio crashes on `recursion-cycle.vsdx` and refuses `tab-short-prefix.vsdx`, two
177
+ of its tests that are broken on purpose. LibreOffice fails on the same two. Geometry, fills, gradients,
178
+ dashes, arrowheads and images match. The text differs most, and LibreOffice is no exact reference for it:
179
+ it lays text out with fonts of its own choice, and breaks words in narrow blocks where Visio does not. On
180
+ some drawings LibreOffice is wrong where weasyvsdx is right, for example it fills the whole of
181
+ `netapp-files-moodle-zonal` blue.
182
+
183
+ LibreOffice reads the drawings through libvisio, as weasyvsdx does, and shares its errors. So the bench
184
+ also compares 206 Azure diagrams with the SVG that Visio exported for their article. The median mismatch
185
+ is 25.0% against Visio, 49.5% against LibreOffice: weasyvsdx draws the gradients and the master
186
+ shapes of the Azure icons, which LibreOffice fills black or leaves out. Much of the rest is the icons of Azure, see "Known gaps".
187
+
188
+ The median Azure diagram converts in 0.08 s, and 9 in 10 in less than 0.5 s. Drawings that embed bitmaps
189
+ of tens of MB take seconds, 17 of the 250 more than one: libvisio writes the bitmaps out as base64. One,
190
+ `adaptive-apps`, takes 46 s, more than the default timeout of 30 s: it converts only with a larger `timeout`.
191
+
192
+ ## Known gaps
193
+
194
+ - A shape whose ID another shape that libvisio draws shares keeps what libvisio drew: no gradient, and no
195
+ geometry from its master. So do the shapes of a VSD, gradients of the rarer kinds (FillGradientDir 8 to
196
+ 13) and colors of the theme of a drawing.
197
+ - Only the first page renders. LibreOffice and the converters before it did the same.
198
+ - Visio drawings ask for Calibri and Segoe UI most, fonts of Microsoft. A font the system lacks falls back
199
+ to its free replacement, else to sans-serif: Calibri to Carlito and Cambria to Caladea, which share their
200
+ metrics, so text breaks where Visio breaks it; Segoe UI to Selawik, the open fallback of Microsoft, close
201
+ to it but not equal. Install them, see "Fonts".
202
+ - Character scaling (the "Scale" of a Visio font) applies where all text of a block shares one scale; a block
203
+ of mixed scales draws unscaled. Fields show the value libvisio formats.
204
+ - A WMF image without an EMF inside is left out, and so is an embedded OLE object whose picture is no EMF.
205
+ Office writes the EMF into each WMF it saves, and into the picture of each OLE object.
206
+ - Text that a VSD hides stays: only a VSDX holds HideText where weasyvsdx reads it.
207
+ - A shape on a hidden or non-printing layer goes, but its text would stay: libvisio marks the path of
208
+ such a shape as hidden, not its text.
209
+ - An EMF draws its GDI records only. Part of an icon that Office drew in EMF+ alone, the newer records an EMF
210
+ can carry in its comments, stays out: `emf2svg-conv` does not read EMF+, LibreOffice does.
211
+
212
+ ## Development
213
+
214
+ Tests and the bench run in Docker, since WeasyPrint needs Pango and weasyvsdx the tools of libvisio. See
215
+ [CONTRIBUTING.md](https://github.com/grigoriev/weasyvsdx/blob/main/CONTRIBUTING.md).
216
+
217
+ ```sh
218
+ make lint # ruff and mypy, on the host
219
+ make test # pytest with coverage, in Docker
220
+ make visual # the visual bench, report in tools/visual/out/report.html
221
+ make comparison # corpus/COMPARISON.md and corpus/comparison.html, the panels in corpus/render
222
+ ```
223
+
224
+ ## Contributing
225
+
226
+ See [CONTRIBUTING.md](https://github.com/grigoriev/weasyvsdx/blob/main/CONTRIBUTING.md). Report vulnerabilities privately, see [SECURITY.md](https://github.com/grigoriev/weasyvsdx/blob/main/SECURITY.md).
227
+
228
+ ## Disclaimer
229
+
230
+ This software is provided "as is", without warranty of any kind, as the LICENSE states. Use
231
+ it at your own risk. Sergey Grigoriev is not liable for damage from its use, as far as the law
232
+ allows. It is published free of charge, outside of any commercial offering, with no
233
+ obligation to support it. Security reports are welcome, see [SECURITY.md](https://github.com/grigoriev/weasyvsdx/blob/main/SECURITY.md).
234
+
235
+ ## License
236
+
237
+ MIT, see [LICENSE](https://github.com/grigoriev/weasyvsdx/blob/main/LICENSE). The test corpus keeps the licenses of its sources, see
238
+ [corpus/SOURCES.md](https://github.com/grigoriev/weasyvsdx/blob/main/corpus/SOURCES.md).