Skip to content

Commit 9358a62

Browse files
Fix #1039 (<span foreground=> tags) (#1058)
* refactor svg def to store element references instead of mobjects * Refactor svg defaults to parsing instead of cascading * Break up style attribute before cascading * add tag inheritance SVG test * black styling * MarkupText: Deprecate <color> tag and use <span foreground> instead * Typo in docstring * Update examples and docs for MarkupText * Fix errors in MarkupText examples * Fixed one more error in the docs * Improved documentation for MarkupText * underline_color is now supported * overline and overline_color are now working * strikethrough and strikethrough_color are now working * additional examples for those features * Fix for docs build * Use existing logger. * revisions from feedback set 1 * delete style from cascading attributes again (it is parsed but not cascaded) * Reinterpret stroke=none as stroke-width=0 and opacity=1 * Update use tag test to handle two more cases Co-authored-by: Philipp Imhof <[email protected]>
1 parent 8f3c372 commit 9358a62

File tree

6 files changed

+195
-70
lines changed

6 files changed

+195
-70
lines changed

manim/mobject/svg/style_utils.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,26 @@
99

1010
from typing import Dict, List
1111

12-
SUPPORTED_STYLING_ATTRIBUTES: List[str] = [
12+
13+
CASCADING_STYLING_ATTRIBUTES: List[str] = [
1314
"fill",
1415
"stroke",
15-
"style",
1616
"fill-opacity",
1717
"stroke-opacity",
1818
]
1919

2020

21+
# The default styling specifications for SVG images,
22+
# according to https://www.w3.org/TR/SVG/painting.html
23+
# (ctrl-F for "initial")
24+
SVG_DEFAULT_ATTRIBUTES: Dict[str, str] = {
25+
"fill": "black",
26+
"fill-opacity": "1",
27+
"stroke": "none",
28+
"stroke-opacity": "1",
29+
}
30+
31+
2132
def cascade_element_style(
2233
element: MinidomElement, inherited: Dict[str, str]
2334
) -> Dict[str, str]:
@@ -46,11 +57,29 @@ def cascade_element_style(
4657

4758
style = inherited.copy()
4859

49-
for attr in SUPPORTED_STYLING_ATTRIBUTES:
60+
# cascade the regular elements.
61+
for attr in CASCADING_STYLING_ATTRIBUTES:
5062
entry = element.getAttribute(attr)
5163
if entry:
5264
style[attr] = entry
5365

66+
# the style attribute should be handled separately in order to
67+
# break it up nicely. furthermore, style takes priority over other
68+
# attributes in the same element.
69+
style_specs = element.getAttribute("style")
70+
if style_specs:
71+
for style_spec in style_specs.split(";"):
72+
try:
73+
key, value = style_spec.split(":")
74+
except ValueError as e:
75+
if not style_spec:
76+
# there was just a stray semicolon at the end, producing an emptystring
77+
pass
78+
else:
79+
raise e
80+
else:
81+
style[key] = value
82+
5483
return style
5584

5685

@@ -91,6 +120,26 @@ def parse_color_string(color_spec: str) -> str:
91120
return hex_color
92121

93122

123+
def fill_default_values(svg_style: Dict) -> None:
124+
"""
125+
Fill in the default values for properties of SVG elements,
126+
if they are not currently set in the style dictionary.
127+
128+
Parameters
129+
----------
130+
svg_style : :class:`dict`
131+
Style dictionary with SVG property names. Some may be missing.
132+
133+
Returns
134+
-------
135+
:class:`dict`
136+
Style attributes; none are missing.
137+
"""
138+
for key in SVG_DEFAULT_ATTRIBUTES:
139+
if key not in svg_style:
140+
svg_style[key] = SVG_DEFAULT_ATTRIBUTES[key]
141+
142+
94143
def parse_style(svg_style: Dict[str, str]) -> Dict:
95144
"""Convert a dictionary of SVG attributes to Manim VMobject keyword arguments.
96145
@@ -106,22 +155,7 @@ def parse_style(svg_style: Dict[str, str]) -> Dict:
106155
"""
107156

108157
manim_style = {}
109-
110-
# style attributes trump other element-level attributes,
111-
# see https://www.w3.org/TR/SVG11/styling.html section 6.4, search "priority"
112-
# so overwrite the other attribute dictionary values.
113-
if "style" in svg_style:
114-
for style_spec in svg_style["style"].split(";"):
115-
try:
116-
key, value = style_spec.split(":")
117-
except ValueError as e:
118-
if not style_spec:
119-
# there was just a stray semicolon at the end, producing an emptystring
120-
pass
121-
else:
122-
raise e
123-
else:
124-
svg_style[key] = value
158+
fill_default_values(svg_style)
125159

126160
if "fill-opacity" in svg_style:
127161
manim_style["fill_opacity"] = float(svg_style["fill-opacity"])
@@ -138,7 +172,12 @@ def parse_style(svg_style: Dict[str, str]) -> Dict:
138172

139173
if "stroke" in svg_style:
140174
if svg_style["stroke"] == "none":
141-
manim_style["stroke_opacity"] = 0
175+
# In order to not break animations.creation.Write,
176+
# we interpret no stroke as stroke-width of zero and
177+
# color the same as the fill color, if it exists.
178+
manim_style["stroke_width"] = 0
179+
if "fill_color" in manim_style:
180+
manim_style["stroke_color"] = manim_style["fill_color"]
142181
else:
143182
manim_style["stroke_color"] = parse_color_string(svg_style["stroke"])
144183

manim/mobject/svg/svg_mobject.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def __init__(
7777
fill_opacity=1.0,
7878
**kwargs,
7979
):
80-
self.def_id_to_mobject = {}
80+
self.def_map = {}
8181
self.file_name = file_name or self.file_name
8282
self.ensure_valid_file()
8383
self.should_center = should_center
@@ -125,17 +125,7 @@ def generate_points(self):
125125
"""
126126
doc = minidom_parse(self.file_path)
127127
for svg in doc.getElementsByTagName("svg"):
128-
mobjects = self.get_mobjects_from(
129-
svg,
130-
# these are the default styling specifications for SVG images,
131-
# according to https://www.w3.org/TR/SVG/painting.html, ctrl-F for "initial"
132-
{
133-
"fill": "black",
134-
"fill-opacity": "1",
135-
"stroke": "none",
136-
"stroke-opacity": "1",
137-
},
138-
)
128+
mobjects = self.get_mobjects_from(svg, {})
139129
if self.unpack_groups:
140130
self.add(*mobjects)
141131
else:
@@ -182,7 +172,9 @@ def get_mobjects_from(
182172
elif element.tagName in ["g", "svg", "symbol", "defs"]:
183173
result += it.chain(
184174
*[
185-
self.get_mobjects_from(child, style, within_defs or is_defs)
175+
self.get_mobjects_from(
176+
child, style, within_defs=within_defs or is_defs
177+
)
186178
for child in element.childNodes
187179
]
188180
)
@@ -191,8 +183,8 @@ def get_mobjects_from(
191183
if temp != "":
192184
result.append(self.path_string_to_mobject(temp, style))
193185
elif element.tagName == "use":
194-
# note, style is not passed down to "use" elements
195-
result += self.use_to_mobjects(element)
186+
# note, style is calcuated in a different way for `use` elements.
187+
result += self.use_to_mobjects(element, style)
196188
elif element.tagName == "rect":
197189
result.append(self.rect_to_mobject(element, style))
198190
elif element.tagName == "circle":
@@ -210,7 +202,9 @@ def get_mobjects_from(
210202
result = [VGroup(*result)]
211203

212204
if within_defs and element.hasAttribute("id"):
213-
self.def_id_to_mobject[element.getAttribute("id")] = result
205+
# it seems wasteful to throw away the actual element,
206+
# but I'd like the parsing to be as similar as possible
207+
self.def_map[element.getAttribute("id")] = (style, element)
214208
if is_defs:
215209
# defs shouldn't be part of the result tree, only the id dictionary.
216210
return []
@@ -235,7 +229,9 @@ def path_string_to_mobject(self, path_string: str, style: dict):
235229
"""
236230
return SVGPathMobject(path_string, **parse_style(style))
237231

238-
def use_to_mobjects(self, use_element: MinidomElement) -> List[VMobject]:
232+
def use_to_mobjects(
233+
self, use_element: MinidomElement, local_style: Dict
234+
) -> List[VMobject]:
239235
"""Converts a SVG <use> element to a collection of VMobjects.
240236
241237
Parameters
@@ -244,22 +240,34 @@ def use_to_mobjects(self, use_element: MinidomElement) -> List[VMobject]:
244240
An SVG <use> element which represents nodes that should be
245241
duplicated elsewhere.
246242
243+
local_style : :class:`Dict`
244+
The styling using SVG property names at the point the element is `<use>`d.
245+
Not all values are applied; styles defined when the element is specified in
246+
the `<def>` tag cannot be overriden here.
247+
247248
Returns
248249
-------
249250
List[VMobject]
250-
A collection of VMobjects that are copies of the defined objects
251+
A collection of VMobjects that are a copy of the defined object
251252
"""
252253

253254
# Remove initial "#" character
254255
ref = use_element.getAttribute("xlink:href")[1:]
255256

256257
try:
257-
return [i.copy() for i in self.def_id_to_mobject[ref]]
258+
def_style, def_element = self.def_map[ref]
258259
except KeyError:
259260
warning_text = f"{self.file_name} contains a reference to id #{ref}, which is not recognized"
260261
warnings.warn(warning_text)
261262
return []
262263

264+
# In short, the def-ed style overrides the new style,
265+
# in cases when the def-ed styled is defined.
266+
style = local_style.copy()
267+
style.update(def_style)
268+
269+
return self.get_mobjects_from(def_element, style)
270+
263271
def attribute_to_float(self, attr):
264272
"""A helper method which converts the attribute to float.
265273
@@ -385,20 +393,21 @@ def rect_to_mobject(self, rect_element: MinidomElement, style: dict):
385393

386394
corner_radius = float(corner_radius)
387395

396+
parsed_style = parse_style(style)
397+
parsed_style["stroke_width"] = stroke_width
398+
388399
if corner_radius == 0:
389400
mob = Rectangle(
390401
width=self.attribute_to_float(rect_element.getAttribute("width")),
391402
height=self.attribute_to_float(rect_element.getAttribute("height")),
392-
stroke_width=stroke_width,
393-
**parse_style(style),
403+
**parsed_style,
394404
)
395405
else:
396406
mob = RoundedRectangle(
397407
width=self.attribute_to_float(rect_element.getAttribute("width")),
398408
height=self.attribute_to_float(rect_element.getAttribute("height")),
399-
stroke_width=stroke_width,
400409
corner_radius=corner_radius,
401-
**parse_style(style),
410+
**parsed_style,
402411
)
403412

404413
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))

0 commit comments

Comments
 (0)