svgslide 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.
- svgslide-0.1.0/CHANGES.md +16 -0
- svgslide-0.1.0/LICENSE +50 -0
- svgslide-0.1.0/MANIFEST.in +1 -0
- svgslide-0.1.0/PKG-INFO +138 -0
- svgslide-0.1.0/README.md +107 -0
- svgslide-0.1.0/pyproject.toml +49 -0
- svgslide-0.1.0/setup.cfg +4 -0
- svgslide-0.1.0/svgslide/__init__.py +227 -0
- svgslide-0.1.0/svgslide/__main__.py +144 -0
- svgslide-0.1.0/svgslide/_convert.py +1847 -0
- svgslide-0.1.0/svgslide/_drawingml.py +227 -0
- svgslide-0.1.0/svgslide/_geom.py +644 -0
- svgslide-0.1.0/svgslide/_images.py +153 -0
- svgslide-0.1.0/svgslide/_merge.py +278 -0
- svgslide-0.1.0/svgslide/_package.py +155 -0
- svgslide-0.1.0/svgslide/_pdf.py +658 -0
- svgslide-0.1.0/svgslide/_style.py +462 -0
- svgslide-0.1.0/svgslide/_svgout.py +427 -0
- svgslide-0.1.0/svgslide/_text.py +68 -0
- svgslide-0.1.0/svgslide.egg-info/PKG-INFO +138 -0
- svgslide-0.1.0/svgslide.egg-info/SOURCES.txt +24 -0
- svgslide-0.1.0/svgslide.egg-info/dependency_links.txt +1 -0
- svgslide-0.1.0/svgslide.egg-info/entry_points.txt +2 -0
- svgslide-0.1.0/svgslide.egg-info/requires.txt +3 -0
- svgslide-0.1.0/svgslide.egg-info/top_level.txt +1 -0
- svgslide-0.1.0/tests/test_convert.py +1393 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changes
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Converts SVG drawings and PDF pages into PowerPoint (`.pptx`) files whose
|
|
8
|
+
shapes, text and pictures are separate, editable objects; can also write the
|
|
9
|
+
converted drawing as SVG for other editors.
|
|
10
|
+
- Python API (`convert`, and `convert_figure` for Matplotlib figures) and a
|
|
11
|
+
`svgslide` command-line tool.
|
|
12
|
+
- Optional merging of identical shapes and coordinate rounding for large
|
|
13
|
+
plots (`merge`, `merge_scope`, `precision`).
|
|
14
|
+
- Replace fonts in the output file (e.g. `fonts={"sans-serif": "Arial"}`,
|
|
15
|
+
`--font sans-serif=Arial` or `fonts="Arial"`, `--font Arial`).
|
|
16
|
+
- PDF input needs `pip install "svgslide[pdf]"` (pypdfium2).
|
svgslide-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Krystian Łazowski
|
|
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.
|
|
22
|
+
|
|
23
|
+
---------------------------------------------------------------------------
|
|
24
|
+
Acknowledgement
|
|
25
|
+
|
|
26
|
+
svgslide is written from scratch, but its DrawingML output follows the
|
|
27
|
+
structure used by mpl-office (https://pypi.org/project/mpl-office/), which
|
|
28
|
+
served as the reference for the DrawingML details. Its licence:
|
|
29
|
+
|
|
30
|
+
MIT License
|
|
31
|
+
|
|
32
|
+
Copyright (c) 2026 Jerritt Collord
|
|
33
|
+
|
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
35
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
36
|
+
in the Software without restriction, including without limitation the rights
|
|
37
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
38
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
39
|
+
furnished to do so, subject to the following conditions:
|
|
40
|
+
|
|
41
|
+
The above copyright notice and this permission notice shall be included in all
|
|
42
|
+
copies or substantial portions of the Software.
|
|
43
|
+
|
|
44
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
45
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
46
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
47
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
48
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
49
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
50
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include CHANGES.md
|
svgslide-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: svgslide
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert an SVG drawing or a PDF into an editable PowerPoint (.pptx) file
|
|
5
|
+
Author-email: Krystian Łazowski <k.lazowski@exeter.ac.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/krystianll/svgslide
|
|
8
|
+
Project-URL: Issues, https://github.com/krystianll/svgslide/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/krystianll/svgslide/blob/main/CHANGES.md
|
|
10
|
+
Keywords: svg,pdf,pptx,powerpoint,drawingml,matplotlib,figures,converter
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
23
|
+
Classifier: Topic :: Office/Business :: Office Suites
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: pdf
|
|
29
|
+
Requires-Dist: pypdfium2>=5.14; extra == "pdf"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# svgslide
|
|
33
|
+
|
|
34
|
+
Convert an SVG drawing or a PDF into a PowerPoint file (`.pptx`) whose parts
|
|
35
|
+
are separate, editable PowerPoint objects: shapes you can recolour and
|
|
36
|
+
resize, real text boxes, pictures, and groups that follow the drawing's
|
|
37
|
+
groups. Made with scientific figures in mind (Matplotlib, seaborn, ggplot),
|
|
38
|
+
but works well for most simple vector images (see Limitations).
|
|
39
|
+
|
|
40
|
+
Pure Python (3.9+). SVG input needs nothing else; PDF input uses the
|
|
41
|
+
optional pypdfium2.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pip install svgslide # SVG input
|
|
45
|
+
pip install "svgslide[pdf]" # SVG and PDF input
|
|
46
|
+
svgslide figure.svg # writes figure.pptx
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Python
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import svgslide
|
|
53
|
+
|
|
54
|
+
svgslide.convert("figure.svg", "figure.pptx")
|
|
55
|
+
svgslide.convert("figure.pdf", "figure.pptx", pages=[1, 3])
|
|
56
|
+
svgslide.convert_figure(fig, "figure.pptx") # a Matplotlib figure
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Options (both functions):
|
|
60
|
+
|
|
61
|
+
- `fonts`: replace fonts in the output file, e.g.
|
|
62
|
+
`fonts={"sans-serif": "Arial"}`, or `fonts="Arial"` for every font; in a
|
|
63
|
+
table, `"*"` stands for every font not named.
|
|
64
|
+
- `pages` (PDF): which pages to convert; each page becomes its own slide.
|
|
65
|
+
- `merge`, `merge_scope`: shapes that look the same become one shape when
|
|
66
|
+
there are more than `merge` of them (default 2000; `0` merges every set,
|
|
67
|
+
`None` never). `merge_scope` says what may merge: `"group"` (default,
|
|
68
|
+
within one group of the drawing), `"touching"` (shapes that overlap or
|
|
69
|
+
touch) or `"figure"` (anywhere). Useful for dense scatter plots and
|
|
70
|
+
genome-browser tracks.
|
|
71
|
+
- `precision`: round outline coordinates to a grid in points (e.g. `0.01`)
|
|
72
|
+
for smaller files.
|
|
73
|
+
- `format`: `"pptx"` or `"svg"`; a destination ending in `.svg` writes the
|
|
74
|
+
converted drawing as SVG, for editors that do not open `.pptx`.
|
|
75
|
+
|
|
76
|
+
Each call returns a report with the counts of shapes, text boxes, pictures
|
|
77
|
+
and groups, the fonts found, and the SVG or PDF features it could not
|
|
78
|
+
convert.
|
|
79
|
+
|
|
80
|
+
## Command line
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
svgslide INPUT... [-o OUTPUT] [--font NAME|OLD=NEW]... [--pages LIST]
|
|
84
|
+
[--merge N|none] [--merge-scope group|touching|figure] [--precision PT]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Each input (`.svg`, `.svgz` or `.pdf`) gives a `.pptx` next to it, or
|
|
88
|
+
`-o OUTPUT` (`.pptx` or `.svg`) for a single input. `--font sans-serif=Arial`
|
|
89
|
+
replaces one font, a bare `--font Arial` every font not named, so
|
|
90
|
+
`--font Arial --font "Symbol=Symbol"` keeps Symbol. The fonts found and
|
|
91
|
+
anything that could not be converted are printed for each file.
|
|
92
|
+
|
|
93
|
+
## What is converted
|
|
94
|
+
|
|
95
|
+
- Shapes and paths, with their colours, transparency, line widths, dashes,
|
|
96
|
+
line ends and gradients; CSS styles; reused symbols and markers.
|
|
97
|
+
- Text as text boxes with font, size, weight, colour, alignment and rotation;
|
|
98
|
+
text along a curve becomes one text box per letter.
|
|
99
|
+
- Text that a program saved only as letter outlines: Matplotlib and seaborn
|
|
100
|
+
labels become real text again; Cairo labels (R `svg()`, `pdftocairo`)
|
|
101
|
+
become one shape per label.
|
|
102
|
+
- Embedded PNG, JPEG and GIF images as pictures; rectangular clipping.
|
|
103
|
+
- PDF pages: shapes, text, images and clipping, read with pypdfium2.
|
|
104
|
+
|
|
105
|
+
## Limitations
|
|
106
|
+
|
|
107
|
+
- PowerPoint has no clipping for shapes: clipping by a rectangle is applied
|
|
108
|
+
exactly, any other clip shape only approximately.
|
|
109
|
+
- Not converted (listed in the report): filters such as blur and drop
|
|
110
|
+
shadows, masks, pattern fills, Inkscape flowed text, animation, external
|
|
111
|
+
image files; in PDFs, smooth shadings and patterns (patterns become their
|
|
112
|
+
average colour), soft masks and blend modes.
|
|
113
|
+
- The focal point of radial gradients is ignored.
|
|
114
|
+
- Text from Cairo-made files (R `svg()`, `pdftocairo`) cannot become editable
|
|
115
|
+
text: those files hold letter shapes, not characters.
|
|
116
|
+
- Letter spacing of text along a curve is estimated, so it can differ a
|
|
117
|
+
little from the original.
|
|
118
|
+
- Fonts are not embedded: PowerPoint uses the named font if it is installed,
|
|
119
|
+
otherwise a substitute. Use `fonts=` / `--font` to pick fonts you have.
|
|
120
|
+
- Merged shapes keep each point's full outline, so a merged `.pptx` can be
|
|
121
|
+
larger than an unmerged one, although it has far fewer objects.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
python -m venv .venv
|
|
127
|
+
.venv/bin/pip install -e ".[pdf]" pytest matplotlib
|
|
128
|
+
.venv/bin/python -m pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Tests needing pypdfium2 or Matplotlib are skipped without them.
|
|
132
|
+
`SVGSLIDE_CORPUS=/folder/of/svgs` adds a smoke test over a folder of SVG
|
|
133
|
+
files; picture comparison tests run when LibreOffice (`soffice`) and
|
|
134
|
+
Inkscape are on `PATH`.
|
|
135
|
+
|
|
136
|
+
## Licence
|
|
137
|
+
|
|
138
|
+
MIT. The DrawingML structure follows mpl-office (MIT); see `LICENSE`.
|
svgslide-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# svgslide
|
|
2
|
+
|
|
3
|
+
Convert an SVG drawing or a PDF into a PowerPoint file (`.pptx`) whose parts
|
|
4
|
+
are separate, editable PowerPoint objects: shapes you can recolour and
|
|
5
|
+
resize, real text boxes, pictures, and groups that follow the drawing's
|
|
6
|
+
groups. Made with scientific figures in mind (Matplotlib, seaborn, ggplot),
|
|
7
|
+
but works well for most simple vector images (see Limitations).
|
|
8
|
+
|
|
9
|
+
Pure Python (3.9+). SVG input needs nothing else; PDF input uses the
|
|
10
|
+
optional pypdfium2.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pip install svgslide # SVG input
|
|
14
|
+
pip install "svgslide[pdf]" # SVG and PDF input
|
|
15
|
+
svgslide figure.svg # writes figure.pptx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Python
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import svgslide
|
|
22
|
+
|
|
23
|
+
svgslide.convert("figure.svg", "figure.pptx")
|
|
24
|
+
svgslide.convert("figure.pdf", "figure.pptx", pages=[1, 3])
|
|
25
|
+
svgslide.convert_figure(fig, "figure.pptx") # a Matplotlib figure
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Options (both functions):
|
|
29
|
+
|
|
30
|
+
- `fonts`: replace fonts in the output file, e.g.
|
|
31
|
+
`fonts={"sans-serif": "Arial"}`, or `fonts="Arial"` for every font; in a
|
|
32
|
+
table, `"*"` stands for every font not named.
|
|
33
|
+
- `pages` (PDF): which pages to convert; each page becomes its own slide.
|
|
34
|
+
- `merge`, `merge_scope`: shapes that look the same become one shape when
|
|
35
|
+
there are more than `merge` of them (default 2000; `0` merges every set,
|
|
36
|
+
`None` never). `merge_scope` says what may merge: `"group"` (default,
|
|
37
|
+
within one group of the drawing), `"touching"` (shapes that overlap or
|
|
38
|
+
touch) or `"figure"` (anywhere). Useful for dense scatter plots and
|
|
39
|
+
genome-browser tracks.
|
|
40
|
+
- `precision`: round outline coordinates to a grid in points (e.g. `0.01`)
|
|
41
|
+
for smaller files.
|
|
42
|
+
- `format`: `"pptx"` or `"svg"`; a destination ending in `.svg` writes the
|
|
43
|
+
converted drawing as SVG, for editors that do not open `.pptx`.
|
|
44
|
+
|
|
45
|
+
Each call returns a report with the counts of shapes, text boxes, pictures
|
|
46
|
+
and groups, the fonts found, and the SVG or PDF features it could not
|
|
47
|
+
convert.
|
|
48
|
+
|
|
49
|
+
## Command line
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
svgslide INPUT... [-o OUTPUT] [--font NAME|OLD=NEW]... [--pages LIST]
|
|
53
|
+
[--merge N|none] [--merge-scope group|touching|figure] [--precision PT]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each input (`.svg`, `.svgz` or `.pdf`) gives a `.pptx` next to it, or
|
|
57
|
+
`-o OUTPUT` (`.pptx` or `.svg`) for a single input. `--font sans-serif=Arial`
|
|
58
|
+
replaces one font, a bare `--font Arial` every font not named, so
|
|
59
|
+
`--font Arial --font "Symbol=Symbol"` keeps Symbol. The fonts found and
|
|
60
|
+
anything that could not be converted are printed for each file.
|
|
61
|
+
|
|
62
|
+
## What is converted
|
|
63
|
+
|
|
64
|
+
- Shapes and paths, with their colours, transparency, line widths, dashes,
|
|
65
|
+
line ends and gradients; CSS styles; reused symbols and markers.
|
|
66
|
+
- Text as text boxes with font, size, weight, colour, alignment and rotation;
|
|
67
|
+
text along a curve becomes one text box per letter.
|
|
68
|
+
- Text that a program saved only as letter outlines: Matplotlib and seaborn
|
|
69
|
+
labels become real text again; Cairo labels (R `svg()`, `pdftocairo`)
|
|
70
|
+
become one shape per label.
|
|
71
|
+
- Embedded PNG, JPEG and GIF images as pictures; rectangular clipping.
|
|
72
|
+
- PDF pages: shapes, text, images and clipping, read with pypdfium2.
|
|
73
|
+
|
|
74
|
+
## Limitations
|
|
75
|
+
|
|
76
|
+
- PowerPoint has no clipping for shapes: clipping by a rectangle is applied
|
|
77
|
+
exactly, any other clip shape only approximately.
|
|
78
|
+
- Not converted (listed in the report): filters such as blur and drop
|
|
79
|
+
shadows, masks, pattern fills, Inkscape flowed text, animation, external
|
|
80
|
+
image files; in PDFs, smooth shadings and patterns (patterns become their
|
|
81
|
+
average colour), soft masks and blend modes.
|
|
82
|
+
- The focal point of radial gradients is ignored.
|
|
83
|
+
- Text from Cairo-made files (R `svg()`, `pdftocairo`) cannot become editable
|
|
84
|
+
text: those files hold letter shapes, not characters.
|
|
85
|
+
- Letter spacing of text along a curve is estimated, so it can differ a
|
|
86
|
+
little from the original.
|
|
87
|
+
- Fonts are not embedded: PowerPoint uses the named font if it is installed,
|
|
88
|
+
otherwise a substitute. Use `fonts=` / `--font` to pick fonts you have.
|
|
89
|
+
- Merged shapes keep each point's full outline, so a merged `.pptx` can be
|
|
90
|
+
larger than an unmerged one, although it has far fewer objects.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
python -m venv .venv
|
|
96
|
+
.venv/bin/pip install -e ".[pdf]" pytest matplotlib
|
|
97
|
+
.venv/bin/python -m pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Tests needing pypdfium2 or Matplotlib are skipped without them.
|
|
101
|
+
`SVGSLIDE_CORPUS=/folder/of/svgs` adds a smoke test over a folder of SVG
|
|
102
|
+
files; picture comparison tests run when LibreOffice (`soffice`) and
|
|
103
|
+
Inkscape are on `PATH`.
|
|
104
|
+
|
|
105
|
+
## Licence
|
|
106
|
+
|
|
107
|
+
MIT. The DrawingML structure follows mpl-office (MIT); see `LICENSE`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "svgslide"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Convert an SVG drawing or a PDF into an editable PowerPoint (.pptx) file"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Krystian Łazowski", email = "k.lazowski@exeter.ac.uk" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["svg", "pdf", "pptx", "powerpoint", "drawingml", "matplotlib", "figures", "converter"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
29
|
+
"Topic :: Office/Business :: Office Suites",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/krystianll/svgslide"
|
|
35
|
+
Issues = "https://github.com/krystianll/svgslide/issues"
|
|
36
|
+
Changelog = "https://github.com/krystianll/svgslide/blob/main/CHANGES.md"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
# 5.14: first release whose PDFium tells pattern paints apart
|
|
40
|
+
pdf = ["pypdfium2>=5.14"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
svgslide = "svgslide.__main__:main"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
packages = ["svgslide"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
svgslide-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""svgslide: convert an SVG drawing or a PDF into an editable PowerPoint file.
|
|
2
|
+
|
|
3
|
+
>>> import svgslide
|
|
4
|
+
>>> report = svgslide.convert("figure.svg", "figure.pptx", fonts={"DejaVu Sans": "Arial"})
|
|
5
|
+
>>> report.skipped
|
|
6
|
+
[]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import gzip
|
|
11
|
+
import io
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
|
|
17
|
+
from . import _convert, _drawingml, _merge, _package
|
|
18
|
+
|
|
19
|
+
__all__ = ['convert', 'convert_figure']
|
|
20
|
+
__version__ = '0.1.0'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Report:
|
|
24
|
+
"""What :func:`convert` wrote and what it had to leave out.
|
|
25
|
+
|
|
26
|
+
Attributes: ``shapes``, ``text_boxes``, ``pictures``, ``groups`` (counts),
|
|
27
|
+
``slide_size`` ((width, height) in EMU) and ``skipped``: a list of
|
|
28
|
+
``(feature, element)`` pairs such as ``("filter", "#shadow")``; for a
|
|
29
|
+
PDF the counts are summed over the slides and each element names its
|
|
30
|
+
page, e.g. ``("shading (smooth colour gradient)", "page 2 object 14")``.
|
|
31
|
+
``merged`` is the number of shapes that went into merged shapes (a set of
|
|
32
|
+
n shapes merged into one counts n - 1). ``fonts`` lists the font names
|
|
33
|
+
of the drawing's text as written (PDF subset prefixes removed), in the
|
|
34
|
+
order first seen; text drawn as glyph outlines has none.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
__slots__ = ('shapes', 'text_boxes', 'pictures', 'groups', 'skipped', 'slide_size', 'merged', 'fonts')
|
|
38
|
+
|
|
39
|
+
def __init__(self, counts, skipped, slide_size, merged=0, fonts=()):
|
|
40
|
+
self.merged = merged
|
|
41
|
+
self.fonts = list(fonts)
|
|
42
|
+
self.shapes = counts['shape']
|
|
43
|
+
self.text_boxes = counts['text']
|
|
44
|
+
self.pictures = counts['picture']
|
|
45
|
+
self.groups = counts['group']
|
|
46
|
+
self.skipped = skipped
|
|
47
|
+
self.slide_size = slide_size
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return ('Report(shapes=%d, text_boxes=%d, pictures=%d, groups=%d, merged=%d, fonts=%r, skipped=%r)'
|
|
51
|
+
% (self.shapes, self.text_boxes, self.pictures, self.groups, self.merged, self.fonts,
|
|
52
|
+
self.skipped))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read_source(source):
|
|
56
|
+
if isinstance(source, (bytes, bytearray)):
|
|
57
|
+
data = bytes(source)
|
|
58
|
+
elif isinstance(source, str) and source.lstrip(' \t\r\n').startswith('<'):
|
|
59
|
+
data = source.encode('utf-8')
|
|
60
|
+
# an XML declaration naming another encoding would now be wrong
|
|
61
|
+
if data.lstrip().startswith(b'<?xml'):
|
|
62
|
+
end = data.find(b'?>')
|
|
63
|
+
data = data[end + 2:] if end > 0 else data
|
|
64
|
+
else:
|
|
65
|
+
with open(os.fspath(source), 'rb') as fh:
|
|
66
|
+
data = fh.read()
|
|
67
|
+
if data[:2] == b'\x1f\x8b':
|
|
68
|
+
data = gzip.decompress(data)
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_pdf(data):
|
|
73
|
+
# a PDF starts with %PDF-; readers accept a byte order mark or white
|
|
74
|
+
# space before it
|
|
75
|
+
return data[:1024].lstrip(b'\xef\xbb\xbf \t\r\n\f\x00').startswith(b'%PDF-')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _check_merge(merge, merge_scope):
|
|
79
|
+
if merge is not None and (isinstance(merge, bool) or not isinstance(merge, int) or merge < 0):
|
|
80
|
+
raise ValueError('merge must be a whole number >= 0 or None, got %r' % (merge,))
|
|
81
|
+
if merge_scope not in _merge.SCOPES:
|
|
82
|
+
raise ValueError('merge_scope must be one of %s, got %r' % (', '.join(_merge.SCOPES), merge_scope))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_fonts(fonts):
|
|
86
|
+
if fonts is None:
|
|
87
|
+
return None
|
|
88
|
+
if isinstance(fonts, str):
|
|
89
|
+
return {'*': fonts}
|
|
90
|
+
if isinstance(fonts, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in fonts.items()):
|
|
91
|
+
return dict(fonts)
|
|
92
|
+
raise ValueError('fonts must be a font name, a {name: replacement} dict or None, got %r' % (fonts,))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_precision(precision):
|
|
96
|
+
if precision is None:
|
|
97
|
+
return None
|
|
98
|
+
if isinstance(precision, bool) or not isinstance(precision, (int, float)) or \
|
|
99
|
+
not math.isfinite(precision) or precision <= 0:
|
|
100
|
+
raise ValueError('precision must be a positive number of points or None, got %r' % (precision,))
|
|
101
|
+
return float(precision)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format(destination, format):
|
|
105
|
+
if format is None:
|
|
106
|
+
name = '' if hasattr(destination, 'write') else os.fspath(destination)
|
|
107
|
+
return 'svg' if str(name).lower().endswith('.svg') else 'pptx'
|
|
108
|
+
if format not in ('pptx', 'svg'):
|
|
109
|
+
raise ValueError('format must be "pptx" or "svg", got %r' % (format,))
|
|
110
|
+
return format
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def convert(source, destination, *, fonts=None, pages=None, merge=2000, merge_scope='group', format=None,
|
|
114
|
+
precision=None):
|
|
115
|
+
"""Convert an SVG or a PDF into a .pptx whose parts are editable shapes.
|
|
116
|
+
|
|
117
|
+
source: a file path, or the SVG itself as ``str`` or ``bytes``, or the
|
|
118
|
+
PDF as ``bytes`` (recognised by its content, not the file name;
|
|
119
|
+
reading PDFs needs ``pip install svgslide[pdf]``).
|
|
120
|
+
destination: a file path or a binary file object.
|
|
121
|
+
fonts: optional ``{font_name: office_font_name}`` replacement table,
|
|
122
|
+
e.g. ``{"DejaVu Sans": "Arial"}`` (names matched without regard to
|
|
123
|
+
case); the key ``"*"`` stands for every font not named, and a plain
|
|
124
|
+
string replaces every font (``"Arial"``). Names not in it are kept
|
|
125
|
+
as written.
|
|
126
|
+
pages: PDF only: the 1-based page number or list of page numbers to
|
|
127
|
+
convert, e.g. ``2`` or ``[1, 3]``; default all pages. Each page
|
|
128
|
+
becomes a slide; the slides have the first page's size and other
|
|
129
|
+
pages are scaled to fit and centred. An SVG gives one slide.
|
|
130
|
+
merge: shapes that look the same (fill, line, opacity) become one shape
|
|
131
|
+
(one outline per member) when a set has more than this many members;
|
|
132
|
+
0 merges every set of two or more, None never merges. Text boxes and
|
|
133
|
+
pictures never merge. A member joins only if nothing that looks
|
|
134
|
+
different, drawn in between, overlaps it, so the picture is unchanged.
|
|
135
|
+
merge_scope: what forms a set: ``"group"`` (shapes in one SVG group,
|
|
136
|
+
the default), ``"touching"`` (shapes whose boxes overlap or touch,
|
|
137
|
+
chained) or ``"figure"`` (anywhere in the drawing).
|
|
138
|
+
format: ``"pptx"`` or ``"svg"`` (the converted drawing as an SVG file,
|
|
139
|
+
one page only); default from the destination's suffix (``.svg``),
|
|
140
|
+
else ``"pptx"``.
|
|
141
|
+
precision: None (full precision), or a grid size in points, e.g. 0.01:
|
|
142
|
+
the coordinates of free-form and merged shapes are rounded to that
|
|
143
|
+
grid and written with fewer digits (smaller files); shape positions
|
|
144
|
+
and sizes, text boxes and pictures stay exact.
|
|
145
|
+
|
|
146
|
+
Returns a :class:`Report` with counts and the skipped features.
|
|
147
|
+
"""
|
|
148
|
+
_check_merge(merge, merge_scope)
|
|
149
|
+
fonts = _check_fonts(fonts)
|
|
150
|
+
precision = _check_precision(precision)
|
|
151
|
+
grid = precision * 12700 if precision else None # EMU
|
|
152
|
+
fmt = _format(destination, format)
|
|
153
|
+
data = _read_source(source)
|
|
154
|
+
if _is_pdf(data):
|
|
155
|
+
from . import _pdf
|
|
156
|
+
docs = _pdf.read(data, pages)
|
|
157
|
+
else:
|
|
158
|
+
if pages is not None:
|
|
159
|
+
raise ValueError('pages= applies to PDF input only; this source is an SVG')
|
|
160
|
+
# comments are kept: Matplotlib writes each outlined text's string in one
|
|
161
|
+
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
|
|
162
|
+
root = ET.fromstring(data, parser=parser)
|
|
163
|
+
if _convert.css.local_name(root.tag) != 'svg':
|
|
164
|
+
raise ValueError('not an SVG document (root element is <%s>)' % _convert.css.local_name(root.tag))
|
|
165
|
+
docs = [(root, None, [])]
|
|
166
|
+
if fmt == 'svg' and len(docs) > 1:
|
|
167
|
+
raise ValueError('SVG output holds one page; this PDF has %d pages here: choose one with pages=' % len(docs))
|
|
168
|
+
series = max(merge, 1) if merge is not None and merge_scope == 'group' else None
|
|
169
|
+
slides = []
|
|
170
|
+
trees = []
|
|
171
|
+
merged = 0
|
|
172
|
+
found = {}
|
|
173
|
+
counts = {'shape': 0, 'text': 0, 'picture': 0, 'group': 0}
|
|
174
|
+
skipped = []
|
|
175
|
+
size = None
|
|
176
|
+
for root, page, own_skipped in docs:
|
|
177
|
+
conv = _convert.Converter(root, fonts, series, grid)
|
|
178
|
+
nodes, absorbed = _merge.merge(conv.run(), merge, merge_scope, grid)
|
|
179
|
+
merged += conv.absorbed + absorbed
|
|
180
|
+
for name in conv.found_fonts:
|
|
181
|
+
found.setdefault(name, None)
|
|
182
|
+
trees.append(nodes)
|
|
183
|
+
ser = _drawingml.Serializer()
|
|
184
|
+
parts = []
|
|
185
|
+
ser.write(nodes, parts)
|
|
186
|
+
slides.append((''.join(parts), conv.media))
|
|
187
|
+
for k, v in ser.counts.items():
|
|
188
|
+
counts[k] += v
|
|
189
|
+
skipped.extend(own_skipped)
|
|
190
|
+
if page is None:
|
|
191
|
+
skipped.extend(conv.skipped)
|
|
192
|
+
else:
|
|
193
|
+
skipped.extend((f, ('page %d %s' % (page, e)).rstrip()) for f, e in conv.skipped)
|
|
194
|
+
if size is None:
|
|
195
|
+
size = (conv.slide_w, conv.slide_h)
|
|
196
|
+
if fmt == 'svg':
|
|
197
|
+
from . import _svgout
|
|
198
|
+
data = _svgout.write(trees[0], size[0], size[1], slides[0][1], precision)
|
|
199
|
+
else:
|
|
200
|
+
data = _package.build(slides, size[0], size[1])
|
|
201
|
+
if hasattr(destination, 'write'):
|
|
202
|
+
destination.write(data)
|
|
203
|
+
else:
|
|
204
|
+
with open(os.fspath(destination), 'wb') as fh:
|
|
205
|
+
fh.write(data)
|
|
206
|
+
return Report(counts, skipped, size, merged, found)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def convert_figure(figure, destination, *, fonts=None, merge=2000, merge_scope='group', format=None,
|
|
210
|
+
precision=None):
|
|
211
|
+
"""Convert a Matplotlib figure into a one-slide .pptx with editable parts.
|
|
212
|
+
|
|
213
|
+
figure: a Matplotlib ``Figure``, or any object with a Matplotlib-like
|
|
214
|
+
``savefig(file, format='svg', dpi=...)``. svgslide does not import
|
|
215
|
+
Matplotlib: when Matplotlib is already loaded, the figure is saved
|
|
216
|
+
with ``svg.fonttype = 'none'`` so that its text stays text.
|
|
217
|
+
destination, fonts, merge, merge_scope, format, precision: as for :func:`convert`.
|
|
218
|
+
|
|
219
|
+
Returns the :class:`Report`.
|
|
220
|
+
"""
|
|
221
|
+
buf = io.BytesIO()
|
|
222
|
+
mpl = sys.modules.get('matplotlib')
|
|
223
|
+
ctx = mpl.rc_context({'svg.fonttype': 'none'}) if mpl is not None else contextlib.nullcontext()
|
|
224
|
+
with ctx:
|
|
225
|
+
figure.savefig(buf, format='svg', dpi='figure')
|
|
226
|
+
return convert(buf.getvalue(), destination, fonts=fonts, merge=merge, merge_scope=merge_scope, format=format,
|
|
227
|
+
precision=precision)
|