yerba 0.0.3a0.dev0__tar.gz → 0.0.3a0.dev2__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 (40) hide show
  1. {yerba-0.0.3a0.dev0/yerba.egg-info → yerba-0.0.3a0.dev2}/PKG-INFO +1 -1
  2. yerba-0.0.3a0.dev2/pyproject.toml +32 -0
  3. yerba-0.0.3a0.dev2/yerba/__init__.py +0 -0
  4. yerba-0.0.3a0.dev2/yerba/__main__.py +30 -0
  5. yerba-0.0.3a0.dev2/yerba/base/__init__.py +0 -0
  6. yerba-0.0.3a0.dev2/yerba/base/box.py +385 -0
  7. yerba-0.0.3a0.dev2/yerba/base/image.py +186 -0
  8. yerba-0.0.3a0.dev2/yerba/base/parser.py +167 -0
  9. yerba-0.0.3a0.dev2/yerba/base/properties.py +49 -0
  10. yerba-0.0.3a0.dev2/yerba/base/slide.py +194 -0
  11. yerba-0.0.3a0.dev2/yerba/base/template.py +887 -0
  12. yerba-0.0.3a0.dev2/yerba/base/template_protocol.py +147 -0
  13. yerba-0.0.3a0.dev2/yerba/base/ytext.py +206 -0
  14. yerba-0.0.3a0.dev2/yerba/defaults.py +73 -0
  15. yerba-0.0.3a0.dev2/yerba/logger_setup.py +28 -0
  16. yerba-0.0.3a0.dev2/yerba/main_rutine.py +205 -0
  17. yerba-0.0.3a0.dev2/yerba/managers/__init__.py +0 -0
  18. yerba-0.0.3a0.dev2/yerba/managers/color_manager.py +70 -0
  19. yerba-0.0.3a0.dev2/yerba/managers/id_manager.py +82 -0
  20. yerba-0.0.3a0.dev2/yerba/managers/singleton.py +13 -0
  21. yerba-0.0.3a0.dev2/yerba/templates/__init__.py +0 -0
  22. yerba-0.0.3a0.dev2/yerba/templates/basic.py +5 -0
  23. yerba-0.0.3a0.dev2/yerba/templates/nice.py +83 -0
  24. yerba-0.0.3a0.dev2/yerba/utils/__init__.py +0 -0
  25. yerba-0.0.3a0.dev2/yerba/utils/aux_classes.py +49 -0
  26. yerba-0.0.3a0.dev2/yerba/utils/aux_functions.py +159 -0
  27. yerba-0.0.3a0.dev2/yerba/utils/constants.py +36 -0
  28. yerba-0.0.3a0.dev2/yerba/utils/latex.py +154 -0
  29. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2/yerba.egg-info}/PKG-INFO +1 -1
  30. yerba-0.0.3a0.dev2/yerba.egg-info/SOURCES.txt +35 -0
  31. yerba-0.0.3a0.dev2/yerba.egg-info/top_level.txt +1 -0
  32. yerba-0.0.3a0.dev0/pyproject.toml +0 -28
  33. yerba-0.0.3a0.dev0/yerba.egg-info/SOURCES.txt +0 -9
  34. yerba-0.0.3a0.dev0/yerba.egg-info/top_level.txt +0 -1
  35. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/LICENSE +0 -0
  36. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/README.md +0 -0
  37. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/setup.cfg +0 -0
  38. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/yerba.egg-info/dependency_links.txt +0 -0
  39. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/yerba.egg-info/entry_points.txt +0 -0
  40. {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/yerba.egg-info/requires.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: yerba
3
- Version: 0.0.3a0.dev0
3
+ Version: 0.0.3a0.dev2
4
4
  Author-email: "Bernardo L. Español" <esp.bernardo@gmail.com>
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "yerba"
7
+ version = "0.0.3-alpha-dev2"
8
+ requires-python = ">=3.8"
9
+ dependencies = [
10
+ "manim",
11
+ "manim-mobject-svg",
12
+ "markdown-it-py",
13
+ "mdformat",
14
+ "mdit-py-plugins",
15
+ "numpy",
16
+ "pillow",
17
+ "pyyaml"
18
+ ]
19
+ authors = [
20
+ {name = "Bernardo L. Español", email = "esp.bernardo@gmail.com"},
21
+ ]
22
+ readme = "README.md"
23
+ license = {file = "LICENSE.txt"}
24
+
25
+ [project.scripts]
26
+ yerba = "yerba.__main__:cli_entry"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["."]
30
+ include = ["yerba*"]
31
+ exclude = []
32
+ namespaces = false
File without changes
@@ -0,0 +1,30 @@
1
+ import sys
2
+ import os
3
+
4
+ from .logger_setup import logger
5
+ from .main_rutine import MainRutine
6
+
7
+
8
+ def cli_entry():
9
+ if len(sys.argv) == 1:
10
+ logger.info("You should specify an input filename")
11
+ quit()
12
+ elif len(sys.argv) == 2:
13
+ filename = sys.argv[1]
14
+ if os.path.exists(filename):
15
+ pass
16
+ elif os.path.exists(filename+".md"):
17
+ filename = filename+".md"
18
+ else:
19
+ logger.error(f"File '{filename}' not found")
20
+ quit()
21
+ else:
22
+ logger.error("Too many args")
23
+ quit()
24
+
25
+ main_rutine = MainRutine(filename)
26
+ main_rutine.run()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ cli_entry()
File without changes
@@ -0,0 +1,385 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from manim import VMobject, VGroup, Rectangle
4
+
5
+ from ..defaults import box_params
6
+ from ..managers.color_manager import ColorManager
7
+ from ..utils.constants import (
8
+ UP, DOWN, LEFT, RIGHT, ORIGIN, SLIDE_HEIGHT, SLIDE_WIDTH, LEFT_EDGE,
9
+ RIGHT_EDGE, SLIDE_X_RAD, SLIDE_Y_RAD, TOP_EDGE, BOTTOM_EDGE, UL, DR
10
+ )
11
+ from ..utils.aux_functions import (
12
+ restructure_list_to_exclude_certain_family_members,
13
+ replace_in_list
14
+ )
15
+
16
+
17
+ class Box():
18
+ def __init__(self, center: np.ndarray,
19
+ width: float, height: float, arrange: str | None = "top left",
20
+ arrange_buff: float = box_params["arrange_buff"],
21
+ is_null: bool = False, is_unique=False):
22
+
23
+ self.center = center
24
+ self.width = width
25
+ self.height = height
26
+ self.arrange = None if arrange == "none" else arrange
27
+ self.arrange_buff = arrange_buff
28
+ self.is_null = is_null
29
+ self.is_unique = is_unique
30
+
31
+ self.grid: dict[str, Box] | None = None
32
+ self.mobjects: list = []
33
+
34
+ @classmethod
35
+ def from_vertex(cls, ul_vertex: tuple[float, float],
36
+ dr_vertex: tuple[float, float], **kwargs):
37
+ left_edge, top_edge = ul_vertex
38
+ right_edge, bottom_edge = dr_vertex
39
+ center = np.array(
40
+ [(right_edge+left_edge)/2, (top_edge+bottom_edge)/2, 0])
41
+ width = right_edge-left_edge
42
+ height = top_edge-bottom_edge
43
+ return cls(center, width, height, **kwargs)
44
+
45
+ @classmethod
46
+ def from_mo(cls, mo, **kwargs):
47
+ return cls.from_vertex(
48
+ ul_vertex=mo.get_corner(UL)[:-1],
49
+ dr_vertex=mo.get_corner(DR)[:-1],
50
+ **kwargs
51
+ )
52
+
53
+ @classmethod
54
+ def from_box(cls, box):
55
+ return cls(center=box.center.copy(),
56
+ width=box.width, height=box.height,
57
+ arrange=box.arrange, is_unique=True)
58
+
59
+ @classmethod
60
+ def get_full_box(cls, **kwargs):
61
+ return cls(ORIGIN, SLIDE_WIDTH, SLIDE_HEIGHT, **kwargs)
62
+
63
+ @classmethod
64
+ def get_left_box(cls, width, **kwargs):
65
+ return cls(LEFT_EDGE+RIGHT*width/2, width, SLIDE_HEIGHT, **kwargs)
66
+
67
+ @classmethod
68
+ def get_right_box(cls, width, **kwargs):
69
+ return cls(RIGHT_EDGE+LEFT*width/2, width, SLIDE_HEIGHT, **kwargs)
70
+
71
+ @classmethod
72
+ def get_top_box(cls, height, **kwargs):
73
+ return cls(TOP_EDGE+DOWN*height/2, SLIDE_WIDTH, height, **kwargs)
74
+
75
+ @classmethod
76
+ def get_bottom_box(cls, height, **kwargs):
77
+ return cls(BOTTOM_EDGE+UP*height/2, SLIDE_WIDTH, height, **kwargs)
78
+
79
+ @classmethod
80
+ def get_inner_box(cls, *, left_box=None, right_box=None,
81
+ top_box=None, bottom_box=None, arrange):
82
+
83
+ lg = SLIDE_X_RAD + left_box.center[0] + left_box.width/2 \
84
+ if left_box else 0
85
+ rg = SLIDE_X_RAD - right_box.center[0] + right_box.width/2 \
86
+ if right_box else 0
87
+ tg = SLIDE_Y_RAD - top_box.center[1] + top_box.height/2 \
88
+ if top_box else 0
89
+ bg = SLIDE_Y_RAD + bottom_box.center[1] + bottom_box.height/2 \
90
+ if bottom_box else 0
91
+
92
+ box = cls.get_full_box(arrange=arrange)
93
+ box.shrink(left_gap=lg, right_gap=rg, top_gap=tg, bottom_gap=bg)
94
+
95
+ return box
96
+
97
+ @classmethod
98
+ def get_null_box(cls):
99
+ return cls(ORIGIN, SLIDE_WIDTH, SLIDE_HEIGHT, is_null=True)
100
+
101
+ @staticmethod
102
+ def merge_boxes(boxes, arrange):
103
+ if len(boxes) == 1:
104
+ return boxes[0]
105
+ left_edge = min([b.center[0]-b.width/2 for b in boxes])
106
+ right_edge = max([b.center[0]+b.width/2 for b in boxes])
107
+ top_edge = max([b.center[1]+b.height/2 for b in boxes])
108
+ bottom_edge = min([b.center[1]-b.height/2 for b in boxes])
109
+
110
+ return Box.from_vertex(ul_vertex=(left_edge, top_edge),
111
+ dr_vertex=(right_edge, bottom_edge),
112
+ arrange=arrange)
113
+
114
+ def add(self, mobject) -> None:
115
+ if hasattr(mobject, "width_units") and mobject.width_units == "box":
116
+ mobject.set(width=self.width*mobject.width, width_units="manim")
117
+ if hasattr(mobject, "height_units") and mobject.height_units == "box":
118
+ mobject.set(height=self.height*mobject.height,
119
+ height_units="manim")
120
+
121
+ self.mobjects.append(mobject)
122
+
123
+ def remove(self, mobjects):
124
+ """Remove specified mobjects from the box."""
125
+ new_l = restructure_list_to_exclude_certain_family_members(
126
+ self.mobjects, mobjects)
127
+ self.mobjects = new_l
128
+
129
+ def replace(self, old_mo, new_mo):
130
+ """Remove specified mobjects from the box."""
131
+ replace_in_list(self.mobjects, old_mo, new_mo)
132
+
133
+ def remove_all_mobjects(self) -> None:
134
+ self.mobjects = []
135
+
136
+ def auto_arrange(self) -> None:
137
+ """TODO(bersp): DOC"""
138
+ if self.arrange is None or len(self.mobjects) == 0 or self.is_null:
139
+ return
140
+
141
+ centers = []
142
+ for mo in self.mobjects:
143
+ centers.append(mo.get_center())
144
+ mo.move_to(ORIGIN)
145
+
146
+ if self.arrange == "center":
147
+ (VGroup(*self.mobjects)
148
+ .move_to(self.center))
149
+ elif self.arrange == "hcenter":
150
+ (VGroup(*self.mobjects)
151
+ .arrange(direction=DOWN, buff=self.arrange_buff)
152
+ .move_to(self.center))
153
+ elif self.arrange == "top center":
154
+ (VGroup(*self.mobjects)
155
+ .arrange(DOWN, buff=self.arrange_buff)
156
+ .next_to(self.get_top(), DOWN, buff=0))
157
+ elif self.arrange == "top left":
158
+ (VGroup(*self.mobjects)
159
+ .arrange(DOWN, buff=self.arrange_buff, aligned_edge=LEFT)
160
+ .next_to(self.get_corner(LEFT, UP), DOWN+RIGHT, buff=0))
161
+ elif self.arrange == "top right":
162
+ (VGroup(*self.mobjects)
163
+ .arrange(DOWN, buff=self.arrange_buff, aligned_edge=RIGHT)
164
+ .next_to(self.get_corner(RIGHT, UP), DOWN+LEFT, buff=0))
165
+ elif self.arrange == "center left":
166
+ (VGroup(*self.mobjects)
167
+ .arrange(DOWN, buff=self.arrange_buff, aligned_edge=LEFT)
168
+ .next_to(self.get_left(), RIGHT, buff=0))
169
+ elif self.arrange == "center right":
170
+ (VGroup(*self.mobjects)
171
+ .arrange(DOWN, buff=self.arrange_buff, aligned_edge=RIGHT)
172
+ .next_to(self.get_right(), LEFT, buff=0))
173
+ else:
174
+ raise ValueError(f"Arrange {self.arrange!r} is not defined")
175
+
176
+ for cent, mo in zip(centers, self.mobjects):
177
+
178
+ if hasattr(mo, 'box_arrange'):
179
+ if mo.box_arrange == "none":
180
+ pass
181
+ elif mo.box_arrange == "center":
182
+ mo.move_to(self.center)
183
+ elif mo.box_arrange == "hcenter":
184
+ mo.set_x(self.center[0])
185
+ elif mo.box_arrange == "vcenter":
186
+ mo.set_y(self.center[1])
187
+ else:
188
+ ValueError(
189
+ f"'box arrange' {mo.box_arrange!r} is not defined")
190
+
191
+ mo.shift(cent)
192
+
193
+ def def_grid(self, grid, hspace=0.5, vspace=0.5,
194
+ width_ratios=None, height_ratios=None,
195
+ arrange="self") -> dict[str, Box]:
196
+ """
197
+ TODO(bersp): Handle errors (bad defined grid)
198
+ """
199
+
200
+ if arrange == "self":
201
+ arrange = self.arrange
202
+
203
+ grid = np.asarray(grid)
204
+ nrows, ncols = grid.shape
205
+
206
+ if width_ratios is None:
207
+ width_ratios = np.asarray([1]*ncols)/ncols
208
+ else:
209
+ width_ratios = np.asarray(width_ratios)
210
+ width_ratios = width_ratios/width_ratios.sum()
211
+
212
+ if height_ratios is None:
213
+ height_ratios = np.asarray([1]*nrows)/nrows
214
+ else:
215
+ height_ratios = np.asarray(height_ratios)
216
+ height_ratios = height_ratios/height_ratios.sum()
217
+
218
+ grid_widths = width_ratios * (self.width-hspace*(ncols-1))
219
+ grid_heights = height_ratios * (self.height-vspace*(nrows-1))
220
+
221
+ centers_x = []
222
+ cx = -self.width/2 - hspace
223
+ for gw in grid_widths:
224
+ cx += gw/2 + hspace
225
+ centers_x.append(cx)
226
+ cx += gw/2
227
+
228
+ centers_y = []
229
+ cy = self.height/2 + hspace
230
+ for gh in grid_heights:
231
+ cy -= gh/2 + hspace
232
+ centers_y.append(cy)
233
+ cy -= gh/2
234
+
235
+ x0, y0, _ = self.center
236
+ subgrid = np.zeros(shape=(nrows, ncols), dtype='object')
237
+ for row, (cy, gh) in enumerate(zip(centers_y, grid_heights)):
238
+ for col, (cx, gw) in enumerate(zip(centers_x, grid_widths)):
239
+ subgrid[row, col] = Box(center=np.array((x0+cx, y0+cy, 0)),
240
+ height=gh, width=gw, arrange=arrange)
241
+
242
+ self.grid = {}
243
+ for label in np.unique(grid):
244
+ self.grid[label] = Box.merge_boxes(
245
+ subgrid[np.where(grid == label)], arrange=arrange
246
+ )
247
+
248
+ return self.grid
249
+
250
+ def get_bbox_mo(self, color="BLACK") -> VMobject:
251
+ color = ColorManager().get_color(color)
252
+ r = (
253
+ Rectangle(height=self.height, width=self.width,
254
+ fill_color=color, fill_opacity=0.2)
255
+ .set_stroke(color=color, opacity=0.0)
256
+ .move_to(self.center)
257
+ .set_z_index(-1)
258
+ )
259
+
260
+ return r
261
+
262
+ def get_bbox_grid(self, color="RED") -> VMobject:
263
+ color = ColorManager().get_color(color)
264
+
265
+ if self.grid is None:
266
+ raise ValueError(f"{self!r} has no defined grid")
267
+
268
+ o = VGroup()
269
+ for g in self.grid.values():
270
+ o.add(g.get_bbox_mo(color=color))
271
+
272
+ return o
273
+
274
+ def shrink(self, *, left_gap=0, right_gap=0,
275
+ top_gap=0, bottom_gap=0) -> Box:
276
+ self.width = self.width - (left_gap + right_gap)
277
+ self.height = self.height - (top_gap + bottom_gap)
278
+ self.center = np.array([self.center[0] + (left_gap - right_gap)/2,
279
+ self.center[1] + (bottom_gap - top_gap)/2,
280
+ 0])
281
+ return self
282
+
283
+ # ---
284
+
285
+ def set_center(self, value):
286
+ self.center = value
287
+ return self
288
+
289
+ def set_width(self, value):
290
+ self.width = value
291
+ return self
292
+
293
+ def set_height(self, value):
294
+ self.height = value
295
+ return self
296
+
297
+ def set_arrange(self, value: str | None):
298
+ self.arrange = None if value == "none" else value
299
+ return self
300
+
301
+ def set_arrange_buff(self, value):
302
+ self.arrange_buff = value
303
+ return self
304
+
305
+ def null(self, value=True):
306
+ self.is_null = value
307
+ return self
308
+
309
+ # ---
310
+
311
+ def get_left(self):
312
+ return self.center + self.width/2*LEFT
313
+
314
+ def get_right(self):
315
+ return self.center + self.width/2*RIGHT
316
+
317
+ def get_top(self):
318
+ return self.center + self.height/2*UP
319
+
320
+ def get_bottom(self):
321
+ return self.center + self.height/2*DOWN
322
+
323
+ def get_corner(self, x_direction, y_direction):
324
+ return self.center+self.width/2*x_direction+self.height/2*y_direction
325
+
326
+ # ---
327
+
328
+ def __eq__(self, other_box):
329
+ if self.is_unique or other_box.is_unique:
330
+ return False
331
+ return (np.all(self.center == other_box.center) and
332
+ self.width == other_box.width and
333
+ self.height == other_box.height and
334
+ self.arrange == other_box.arrange and
335
+ self.arrange_buff == other_box.arrange_buff and
336
+ self.is_null == other_box.is_null)
337
+
338
+ def __repr__(self):
339
+ c = f"[{self.center[0]:.2g}, {self.center[1]:.2g}]"
340
+ return f"Box(center={c}, width={self.width:.2g}, height={self.height:.2g}, arrange={self.arrange}, is_null={self.is_null})"
341
+
342
+
343
+ class NamedBoxes:
344
+ def __init__(self, **boxes):
345
+ """
346
+ Parameters
347
+ ----------
348
+ **boxes : dict
349
+ Named boxes
350
+ """
351
+ for name, box in boxes.items():
352
+ self.add(name, box)
353
+
354
+ def add(self, name, box):
355
+ setattr(self, name, box)
356
+
357
+ def set_current_box(self, box_or_box_name):
358
+ """
359
+ TODO(bersp): Documentation.
360
+ TODO(bersp): Handle errors.
361
+ """
362
+
363
+ if isinstance(box_or_box_name, Box):
364
+ setattr(self, "active", box_or_box_name)
365
+ else:
366
+ setattr(self, "active", self.__dict__[box_or_box_name])
367
+
368
+ def remove_all_mobjects(self):
369
+ """Remove all mobjects from all boxes in the slide layout."""
370
+ for _, box in self.__dict__.items():
371
+ box.remove_all_mobjects()
372
+
373
+ def get_bbox_mo(self):
374
+ """Return a VGroup containing all the bounding boxes of the all boxes."""
375
+ o = VGroup()
376
+ for _, box in self.__dict__.items():
377
+ o += box.get_bbox_mo()
378
+ return o
379
+
380
+ def __repr__(self):
381
+ s = ''
382
+ for name, box in self.__dict__.items():
383
+ s += f"{name} -> {box}\n"
384
+
385
+ return s
@@ -0,0 +1,186 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+ import shutil
5
+ import uuid
6
+ from manim import VGroup, Rectangle
7
+ from xml.etree import ElementTree
8
+ from PIL import Image
9
+
10
+ from ..managers.color_manager import ColorManager
11
+ from ..base.ytext import Ytex
12
+ from ..utils.constants import UL, PX, SLIDE_X_RAD, SLIDE_Y_RAD
13
+
14
+
15
+ class ImageSvgBase(VGroup):
16
+ # TODO(bersp): Implement .rotate to imgs
17
+ def __init__(self, filename, width, height, draft_mode):
18
+
19
+ self.filename = filename
20
+ self.basename = os.path.basename(filename)
21
+ self.draft_mode = draft_mode
22
+
23
+ colors = ColorManager()
24
+
25
+ rec = (Rectangle(width=width, height=height)
26
+ .set_stroke(opacity=0)
27
+ .set_fill(color=colors.get_color("BLACK"), opacity=0.8))
28
+
29
+ if draft_mode:
30
+ tex_filename = filename.replace("_", r"\\_")
31
+ tex_filename = fr"\\texttt{{{tex_filename}}}"
32
+ super().__init__(
33
+ rec,
34
+ Ytex(tex_filename, color=colors.get_color("BLACK")).move_to(
35
+ rec).set(width=width*0.9)
36
+ )
37
+ else:
38
+ super().__init__(rec)
39
+
40
+ def _manim_to_svg_coords(self):
41
+ x0, y0 = self.get_corner(UL)[:2]
42
+ x0 = (SLIDE_X_RAD+x0)*PX
43
+ y0 = (SLIDE_Y_RAD-y0)*PX
44
+ w = self.width*PX
45
+ h = self.height*PX
46
+
47
+ return x0, y0, w, h
48
+
49
+ def _get_width_and_height(self, width, height,
50
+ file_width, file_height):
51
+
52
+ ERR = ValueError(
53
+ r"width or height must be numbers or percentages like '90%'"
54
+ )
55
+
56
+ porcentage_pattern = r"(^\d+(\.\d+)?)%$"
57
+
58
+ if isinstance(width, str):
59
+ m = re.match(porcentage_pattern, width)
60
+ if m:
61
+ self.width_units = "box"
62
+ width = float(m[1])/100
63
+ else:
64
+ raise ERR
65
+
66
+ if isinstance(height, str):
67
+ m = re.match(porcentage_pattern, height)
68
+ if m:
69
+ self.height_units = "box"
70
+ height = float(m[1])/100
71
+ else:
72
+ raise ERR
73
+
74
+ if not ((isinstance(width, (int, float)) or width is None) and
75
+ (isinstance(height, (int, float)) or height is None)):
76
+ raise ERR
77
+
78
+ if width is not None and height is None:
79
+ height = file_height/file_width*width
80
+ elif width is None and height is not None:
81
+ width = file_width/file_height*height
82
+ else:
83
+ raise ValueError(
84
+ "You have to specify either 'width' or 'height', and not both")
85
+
86
+ return width, height
87
+
88
+
89
+ class ImageSvg(ImageSvgBase):
90
+ def __init__(self, filename, width=None, height=None,
91
+ draft_mode=False, **_):
92
+
93
+ fw, fh = Image.open(filename).size
94
+ width, height = self._get_width_and_height(width, height, fw, fh)
95
+
96
+ super().__init__(filename, width, height, draft_mode)
97
+
98
+ def get_svg_str(self):
99
+ x0, y0, w, h = self._manim_to_svg_coords()
100
+ img_base64 = (
101
+ subprocess.run(["base64", self.filename],
102
+ stdout=subprocess.PIPE)
103
+ .stdout.decode("utf-8").replace("\n", "")
104
+ )
105
+ s = (f'<image width="{w}" height="{h}" x="{x0}" y="{y0}" '
106
+ f'href="data:image/png;base64,{img_base64}"/>')
107
+
108
+ return s
109
+
110
+
111
+ class ImagePDFSvg(ImageSvgBase):
112
+ def __init__(self, filename, width=None, height=None,
113
+ backend='poppler', draft_mode=False, **_):
114
+ if not shutil.which("inkscape"):
115
+ raise FileNotFoundError("Inkscape is required to use PDF images.")
116
+
117
+ svg_str_raw = self._get_svg_str_raw(filename, backend)
118
+ self.xml_tree = ElementTree.fromstring(svg_str_raw)
119
+
120
+ unique_prefix = f"{uuid.uuid4().hex[:8]}_"
121
+ self._rename_ids(self.xml_tree, unique_prefix)
122
+
123
+ fw, fh = self.xml_tree.get('width'), self.xml_tree.get('height')
124
+ if fw is not None and fh is not None:
125
+ fw = float(fw.replace("pt", ""))
126
+ fh = float(fh.replace("pt", ""))
127
+ else:
128
+ raise ValueError("An error occurred while parsing the PDF "
129
+ f"file {filename!r} to SVG.")
130
+
131
+ width, height = self._get_width_and_height(width, height, fw, fh)
132
+
133
+ super().__init__(filename, width, height, draft_mode)
134
+
135
+ def get_svg_str(self):
136
+ x0, y0, w, h = self._manim_to_svg_coords()
137
+
138
+ self.xml_tree.set("x", str(x0))
139
+ self.xml_tree.set("y", str(y0))
140
+ self.xml_tree.set("width", str(w))
141
+ self.xml_tree.set("height", str(h))
142
+
143
+ s = ElementTree.tostring(self.xml_tree, encoding='unicode')
144
+ return s
145
+
146
+ def _get_svg_str_raw(self, filename, backend):
147
+ assert backend == "poppler" or backend == "internal", (
148
+ "Backend must be 'poppler' or 'internal'"
149
+ )
150
+ opts = ["--export-plain-svg",
151
+ "--export-type=svg",
152
+ "--export-filename=-"]
153
+ if backend == "poppler":
154
+ opts.append("--pdf-poppler")
155
+ s = subprocess.run(["inkscape", *opts, filename],
156
+ stdout=subprocess.PIPE, stderr=open(os.devnull, "w")
157
+ ).stdout.decode("utf-8")
158
+
159
+ return s
160
+
161
+ def _rename_ids(self, root, prefix):
162
+ ids_map = {}
163
+ for elem in root.iter():
164
+ if 'id' in elem.attrib:
165
+ old_id = elem.get('id')
166
+ new_id = prefix + old_id
167
+ elem.set('id', new_id)
168
+ ids_map[old_id] = new_id
169
+
170
+ if not ids_map:
171
+ return
172
+
173
+ escaped_ids: list[str] = list(map(re.escape, ids_map.keys()))
174
+
175
+ pattern = re.compile(r"#(" + "|".join(escaped_ids) + r")\b")
176
+
177
+ def replace_id(m):
178
+ old = m.group(1)
179
+ return "#" + ids_map[old]
180
+
181
+ for elem in root.iter():
182
+ for attr_name in list(elem.attrib.keys()):
183
+ attr_value = elem.attrib[attr_name]
184
+ if "#" in attr_value: # Quick verification to not apply re if not necessary
185
+ new_attr_value = pattern.sub(replace_id, attr_value)
186
+ elem.set(attr_name, new_attr_value)