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.
@@ -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
@@ -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`.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)