525 lines
20 KiB
Python
525 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
#
|
|
# Copyright (C) 2021 Jonathan Neuhauser, jonathan.neuhauser@outlook.com
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA.
|
|
#
|
|
"""
|
|
Some more complicated styling tests, including inheritance and shorthand attributes
|
|
"""
|
|
|
|
from lxml import etree
|
|
from typing import List, Tuple
|
|
|
|
import pytest
|
|
from inkex.styles import Style
|
|
from inkex.colors import Color
|
|
from inkex.tester import TestCase
|
|
from inkex.tester.svg import svg_file
|
|
from inkex import (
|
|
SvgDocumentElement,
|
|
BaseElement,
|
|
ColorError,
|
|
RadialGradient,
|
|
Stop,
|
|
PathElement,
|
|
)
|
|
from inkex import SVG_PARSER
|
|
|
|
|
|
class StyleInheritanceTests(TestCase):
|
|
"""Some test cases for css attribute handling"""
|
|
|
|
def test_style_sheet_1(self):
|
|
"""File from https://commons.wikimedia.org/wiki/File:Test_only.svg, public domain
|
|
note that Inkscape fails the same test: https://gitlab.com/inkscape/inbox/-/issues/1929
|
|
"""
|
|
doc: SvgDocumentElement = svg_file(
|
|
self.data_file("svg", "style_inheritance.svg")
|
|
)
|
|
|
|
circles: List[BaseElement] = doc.xpath("//svg:circle")
|
|
for circle in circles:
|
|
self.assertEqual(
|
|
circle.get_computed_style("fill"),
|
|
Color("red"),
|
|
circle.getparent().get_id(),
|
|
)
|
|
|
|
style = circle.specified_style()
|
|
self.assertEqual(style("fill"), Color("red"), circle.getparent().get_id())
|
|
|
|
rects: List[BaseElement] = doc.xpath("//svg:rect")
|
|
for rect in rects:
|
|
self.assertEqual(
|
|
rect.get_computed_style("fill"),
|
|
Color("blue"),
|
|
rect.getparent().get_id(),
|
|
)
|
|
|
|
style = rect.specified_style()
|
|
self.assertEqual(style("fill"), Color("blue"))
|
|
|
|
def test_style_sheet_2(self):
|
|
"""This is the unit test styling-css-04-f.svg from
|
|
https://www.w3.org/Graphics/SVG/Test/20061213/htmlObjectHarness/full-styling-css-04-f.html
|
|
Note that the "good" preview image attached on the site is wrong per the explanation
|
|
"""
|
|
doc: SvgDocumentElement = svg_file(
|
|
self.data_file("svg", "styling-css-04-f.svg")
|
|
)
|
|
|
|
rects: List[BaseElement] = doc.xpath("//svg:rect")
|
|
|
|
results = {
|
|
"A": "blue",
|
|
"B": "green",
|
|
"C": "orange",
|
|
"D": "gold",
|
|
"E": "purple",
|
|
"F": "red",
|
|
}
|
|
for rect in rects:
|
|
ident = rect.get_id()
|
|
if len(ident) != 2:
|
|
continue
|
|
result = results[ident[0]]
|
|
|
|
style = rect.specified_style()
|
|
self.assertEqual(style("fill"), Color(result))
|
|
self.assertEqual(rect.get_computed_style("fill"), Color(result), ident)
|
|
self.assertEqual(style["stroke-dasharray"], "none")
|
|
self.assertEqual(rect.get_computed_style("stroke-dasharray"), [])
|
|
|
|
def test_current_color(self):
|
|
"""This is the unit test styling-inherit-01-b.svg from
|
|
https://www.w3.org/Graphics/SVG/Test/20061213/htmlObjectHarness/full-styling-inherit-01-b.html
|
|
"""
|
|
|
|
doc: SvgDocumentElement = svg_file(
|
|
self.data_file("svg", "styling-inherit-01-b.svg")
|
|
)
|
|
|
|
objects: List[BaseElement] = doc.xpath("//svg:rect|//svg:ellipse")
|
|
|
|
for counter, obj in zip(range(3), objects[:3]):
|
|
fill = obj.specified_style()("fill")
|
|
fill2 = obj.get_computed_style("fill")
|
|
assert fill == fill2
|
|
if counter == 0:
|
|
self.assertEqual(fill, Color("yellow"))
|
|
else:
|
|
if counter == 1:
|
|
result = "green"
|
|
else:
|
|
result = "#700"
|
|
self.assertIsInstance(fill, RadialGradient)
|
|
stops = [child for child in fill if isinstance(child, Stop)]
|
|
stop = stops[1]
|
|
self.assertEqual(stop.specified_style()("stop-color"), Color(result))
|
|
self.assertEqual(stop.get_computed_style("stop-color"), Color(result))
|
|
|
|
stroke = objects[3].specified_style()("stroke")
|
|
self.assertEqual(stroke, Color("red"))
|
|
|
|
self.assertEqual(objects[3].get_computed_style("stroke"), Color("red"))
|
|
|
|
def test_marker_style(self):
|
|
"""Check if markers are read and written correctly"""
|
|
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "markers.svg"))
|
|
elem = doc.getElementById("dimension")
|
|
style = elem.specified_style()
|
|
marker = style("marker-start")
|
|
self.assertEqual(marker, doc.getElementById("Arrow1Lstart"))
|
|
|
|
# replace marker
|
|
elem.style["marker-start"] = doc.getElementById("Arrow1Lend")
|
|
marker = elem.specified_style()("marker-start")
|
|
self.assertEqual(marker, doc.getElementById("Arrow1Lend"))
|
|
|
|
# write invalid attribute
|
|
with self.assertRaisesRegex(ValueError, "Invalid property value"):
|
|
elem.style["marker-start"] = "#url(test)"
|
|
|
|
# write invalid attribute, second attempt
|
|
with self.assertRaisesRegex(ValueError, "Invalid property value"):
|
|
elem.style["marker-start"] = "url('test)"
|
|
|
|
# write shorthand
|
|
elem.style["marker"] = doc.getElementById("Arrow1Lstart")
|
|
self.assertEqual(elem.style("marker-start"), doc.getElementById("Arrow1Lstart"))
|
|
self.assertEqual(elem.style("marker-mid"), doc.getElementById("Arrow1Lstart"))
|
|
self.assertEqual(elem.style("marker-end"), doc.getElementById("Arrow1Lstart"))
|
|
|
|
# write shorthand to empty
|
|
elem.style["marker"] = ""
|
|
self.assertEqual(elem.style("marker-start"), None)
|
|
|
|
def test_get_default(self):
|
|
"""Test if the default values are returned for missing attributes"""
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
elem = doc.getElementById("path6")
|
|
|
|
assert elem.style("stroke-dashoffset") == "0"
|
|
assert elem.style("font") == ""
|
|
|
|
def parse_style_and_compare(self, tests: List[Tuple[str, dict]]):
|
|
"""Parses a style and compares the output to a dictionary of attributes"""
|
|
for shorthand, result in tests:
|
|
style = Style(shorthand)
|
|
for key, value in result.items():
|
|
self.assertEqual(str(style(key)), value)
|
|
|
|
def test_font_shorthand(self):
|
|
"""Test whether shorthand properties are applied correctly"""
|
|
tests: List[Tuple[str, dict]] = [
|
|
("font: ", {"font-size": "medium"}),
|
|
(
|
|
r"font: 12px/14px sans-serif",
|
|
{
|
|
"font-size": "12px",
|
|
"line-height": "14px",
|
|
"font-family": "sans-serif",
|
|
},
|
|
),
|
|
(
|
|
r"font: 80% sans-serif",
|
|
{"font-size": "80%", "font-family": "sans-serif"},
|
|
),
|
|
(
|
|
r'font: x-large/110% "New Century Schoolbook", serif',
|
|
{
|
|
"font-size": "x-large",
|
|
"line-height": "110%",
|
|
"font-family": '"New Century Schoolbook", serif',
|
|
},
|
|
),
|
|
(
|
|
r"font: semi-condensed bold italic large Palatino, serif",
|
|
{
|
|
"font-weight": "bold",
|
|
"font-style": "italic",
|
|
"font-size": "large",
|
|
"font-family": "Palatino, serif",
|
|
"font-stretch": "semi-condensed",
|
|
},
|
|
),
|
|
(
|
|
r"font: normal small-caps 120%/120% fantasy",
|
|
{
|
|
"font-weight": "normal",
|
|
"font-style": "normal",
|
|
"font-variant": "small-caps",
|
|
"font-size": "120%",
|
|
"line-height": "120%",
|
|
"font-family": "fantasy",
|
|
},
|
|
),
|
|
]
|
|
self.parse_style_and_compare(tests)
|
|
|
|
def test_shorthand_overwrites(self):
|
|
"""Test whether shorthands correctly follow precedence: only overwrite rules which are
|
|
defined before and not important"""
|
|
tests: List[Tuple[str, dict]] = [
|
|
(
|
|
"""font-size: large;
|
|
font-family: Verdana !important;
|
|
font: bold 12px/14px sans-serif;
|
|
font-weight: normal;""",
|
|
{
|
|
"font-size": "12px",
|
|
"font-family": "Verdana",
|
|
"line-height": "14px",
|
|
"font-weight": "normal",
|
|
},
|
|
)
|
|
]
|
|
self.parse_style_and_compare(tests)
|
|
|
|
def test_gradient_parsing(self):
|
|
"""Test if the style correctly outputs Gradient objects"""
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
elem = doc.getElementById("path6")
|
|
style = elem.style
|
|
grad = style("stroke")
|
|
self.assertEqual(grad, doc.getElementById("linearGradient855"))
|
|
|
|
def test_attribute_set(self):
|
|
"""Tests if we can set attributes with parsed values"""
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
|
|
elem = doc.getElementById("path6")
|
|
style = elem.style
|
|
|
|
tests = [
|
|
(
|
|
"stroke",
|
|
doc.getElementById("linearGradient847"),
|
|
"url(#linearGradient847)",
|
|
),
|
|
("fill", Color("red"), "red"),
|
|
("stroke", None, "none"),
|
|
("opacity", 0.5, "0.5"),
|
|
("opacity", 1.2, "1"),
|
|
("opacity", -2, "0"),
|
|
("font-variant", "small-caps", "small-caps"),
|
|
]
|
|
for attr, value, result in tests:
|
|
style[attr] = value
|
|
self.assertEqual(style[attr], result)
|
|
self.assertEqual(elem.specified_style()[attr], result)
|
|
|
|
def test_opacity_clip(self):
|
|
"""Test if opacity clipping works"""
|
|
style = Style()
|
|
tests = [
|
|
("opacity", "0.5", 0.5),
|
|
("opacity", "1.2", 1),
|
|
("opacity", "-2", 0),
|
|
("opacity", "50%", 0.5),
|
|
]
|
|
for attr, value, result in tests:
|
|
style[attr] = value
|
|
self.assertEqual(style(attr), result)
|
|
|
|
def test_style_parsing_error(self):
|
|
"""Test if bad attribute data raises an exception during parsing"""
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
tests: List[Tuple[str, str, Exception]] = [
|
|
(r"opacity", "abc", ValueError),
|
|
(r"fill", "#GHI", ColorError),
|
|
(r"stroke", "url(#missing)", ValueError),
|
|
(r"font-variant", "blue", ValueError),
|
|
]
|
|
|
|
s = doc.style
|
|
for key, value, exceptiontype in tests:
|
|
with self.assertRaises(exceptiontype):
|
|
s[key] = value
|
|
|
|
def test_attribute_set_invalid(self):
|
|
"""Test if bad attribute data raises an exception when setting it on a style"""
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
|
|
elem = doc.getElementById("path6")
|
|
tests = [
|
|
("fill", "nocolor", "Unknown color format"),
|
|
("opacity", Style(), "Value must be number"),
|
|
(
|
|
"font-variant",
|
|
"red",
|
|
"Value 'red' is invalid for the property",
|
|
),
|
|
("stroke", "url(#missing)", "Paint server not found"),
|
|
]
|
|
style = elem.style
|
|
for attr, value, errormsg in tests:
|
|
with self.assertRaisesRegex(Exception, errormsg):
|
|
style[attr] = value
|
|
|
|
def test_gradient_id_fallback(self):
|
|
"""Test if the gradient fallback (color after nonexistent url) works"""
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
sty = Style(element=doc)
|
|
sty["stroke"] = "url(#nonexistent) red"
|
|
self.assertEqual(sty("stroke"), Color("red"))
|
|
|
|
def test_compare_styles(self):
|
|
"""Check style comparison"""
|
|
st1 = Style("fill: blue; stroke: red")
|
|
st2 = Style("fill: orange; stroke: red")
|
|
self.assertNotEqual(st1, st2)
|
|
st2["fill"] = "blue"
|
|
self.assertEqual(st1, st2)
|
|
st1["font-size"] = 1
|
|
self.assertNotEqual(st1, st2)
|
|
|
|
def test_style_bad_interfacing(self):
|
|
"""Check a few ways to wrongly interface the Style class"""
|
|
style = Style("fill: red;")
|
|
|
|
# call a missing key that is unknown and therefore has no default
|
|
with self.assertRaises(KeyError):
|
|
_ = style("favourite-test")
|
|
|
|
# call the add_inherited method with a non-style argument
|
|
style2 = style.add_inherited("fill: blue")
|
|
self.assertEqual(style, style2)
|
|
|
|
# set the importance on a value that doesn't exist
|
|
with self.assertRaises(KeyError):
|
|
style.set_importance("stroke", True)
|
|
|
|
def test_style_exchange(self):
|
|
doc: SvgDocumentElement = svg_file(self.data_file("svg", "interp_shapes.svg"))
|
|
|
|
elem = doc.getElementById("path6")
|
|
style = elem.style
|
|
|
|
copystyle = style.copy()
|
|
self.assertIsNone(copystyle.callback)
|
|
copystyle["new-attribute"] = "test"
|
|
self.assertNotIn("new-attribute", elem.style)
|
|
|
|
elem.style = copystyle
|
|
|
|
copystyle["new-attribute"] = "test"
|
|
self.assertEqual(elem.style("new-attribute"), "test")
|
|
# callback is set after accessing the element
|
|
self.assertIsNotNone(elem.style.callback)
|
|
# copystyle["new-attribute2"] = "test"
|
|
# self.assertEqual(elem.style("new-attribute2"), "test")
|
|
|
|
def test_stop_opacity_inheritance(self):
|
|
# subtest of pservers-grad-18b SVG1.1 unit test
|
|
content = """ <svg><g id="test-body-content" font-family="SVGFreeSansASCII,sans-serif" font-size="18">
|
|
<g id="g0" stop-color="#f48" stop-opacity="0.5" color="yellow">
|
|
<linearGradient id="MyGradient1" stop-color="inherit">
|
|
<stop offset="0" stop-color="green" stop-opacity="1"/>
|
|
<stop offset="1" stop-color="inherit" stop-opacity="1"/>
|
|
</linearGradient>
|
|
</g></g></svg>
|
|
"""
|
|
doc = etree.fromstring(content, parser=SVG_PARSER)
|
|
grad = doc.getElementById("MyGradient1")
|
|
self.assertEqual(
|
|
grad[0].specified_style()("stop-opacity"), 1
|
|
) # assert that stop opacity is overwritten
|
|
self.assertEqual(
|
|
grad[1].specified_style()("stop-opacity"), 1
|
|
) # assert that stop opacity is not inherited by default
|
|
|
|
def test_inheritance_second_attribute(self):
|
|
"""Check that the second attribute is also correctly inherited"""
|
|
content = """<svg><g fill="red" font-size="18"><g font-size="20" id="test"></g></g></svg>"""
|
|
doc = etree.fromstring(content, parser=SVG_PARSER)
|
|
group = doc.getElementById("test")
|
|
self.assertEqual(group.specified_style()("font-size"), 20)
|
|
|
|
def test_inherit_fallback(self):
|
|
content = """<svg><g fill="inherit" style="bla: inherit"><g fill="inherit" id="test" style="bla:inherit"/></g></svg>"""
|
|
doc = etree.fromstring(content, parser=SVG_PARSER)
|
|
group = doc.getElementById("test")
|
|
self.assertEqual(group.specified_style()("fill"), Color("black"))
|
|
self.assertEqual(group.get_computed_style("fill"), Color("black"))
|
|
self.assertEqual(group.specified_style()("bla"), None)
|
|
self.assertEqual(group.get_computed_style("bla"), None)
|
|
|
|
def test_direct_child_and_import(self):
|
|
content = """<svg xmlns="http://www.w3.org/2000/svg">
|
|
<style type="text/css"><![CDATA[
|
|
@import url("test.css");
|
|
g > ellipse
|
|
{
|
|
fill: red;
|
|
}
|
|
@import url("test.css");
|
|
]]></style>
|
|
<g><ellipse id="test"></ellipse></g></svg>"""
|
|
doc = etree.fromstring(content, parser=SVG_PARSER)
|
|
ellipse = doc.getElementById("test")
|
|
self.assertEqual(ellipse.specified_style()("fill"), Color("red"))
|
|
|
|
def test_dasharray(self):
|
|
"""test parsing of dasharray"""
|
|
elem = PathElement()
|
|
style = elem.style
|
|
tests = [
|
|
("1 2 3 4", [1, 2, 3, 4]),
|
|
("1 2,3 4.5", [1, 2, 3, 4.5]),
|
|
("1;2", []),
|
|
("1.111", [1.111, 1.111]),
|
|
("1px, 2px, 3px", [1, 2, 3, 1, 2, 3]),
|
|
("", []),
|
|
("1 -2", []),
|
|
(None, []),
|
|
([1, 2, 3], [1, 2, 3, 1, 2, 3]),
|
|
]
|
|
for value, result in tests:
|
|
style["stroke-dasharray"] = value
|
|
setvalue = style("stroke-dasharray")
|
|
if result is None:
|
|
self.assertEqual(result, setvalue, f"got {setvalue}, original: {value}")
|
|
else:
|
|
self.assertAlmostTuple(
|
|
result, setvalue, msg=f"Expected {result}, got {setvalue}"
|
|
)
|
|
|
|
def test_dasharray_mutable(self):
|
|
elem = PathElement()
|
|
style = elem.style
|
|
sd = "stroke-dasharray"
|
|
style[sd] = [1, 2, 3, 4]
|
|
style(sd).extend([5, 6])
|
|
self.assertAlmostTuple(style(sd), [1, 2, 3, 4, 5, 6])
|
|
|
|
def test_recursion_currentcolor(self):
|
|
svg = """<svg viewBox="0 0 210 297" xmlns="http://www.w3.org/2000/svg">
|
|
<g
|
|
fill="currentColor">
|
|
<path d="M 21.797956,55.871 37.966447,36.549261 52.393685,54.300881"/>
|
|
</g>
|
|
</svg>"""
|
|
doc = etree.fromstring(svg, parser=SVG_PARSER)
|
|
path = doc[0][0]
|
|
assert path.get_computed_style("fill") == Color("black")
|
|
assert path.specified_style()("fill") == Color("black")
|
|
|
|
def test_rgb_color(self):
|
|
elem = PathElement()
|
|
elem.style["fill"] = "rgb(255, 0, 0)"
|
|
self.assertEqual(elem.specified_style()("fill"), Color("rgb(255, 0, 0)"))
|
|
self.assertEqual(elem.get_computed_style("fill"), Color("rgb(255, 0, 0)"))
|
|
|
|
def test_rgba_color(self):
|
|
elem = PathElement()
|
|
elem.style["fill"] = "rgba(255, 0, 0, 0.5)"
|
|
self.assertEqual(elem.specified_style()("fill"), Color("rgba(255, 0, 0, 0.5)"))
|
|
self.assertEqual(elem.get_computed_style("fill"), Color("rgba(255, 0, 0, 0.5)"))
|
|
|
|
def test_hsl_color(self):
|
|
elem = PathElement()
|
|
elem.style["fill"] = "hsl(175, 75, 50)"
|
|
self.assertEqual(elem.specified_style()("fill"), Color("hsl(175, 75, 50)"))
|
|
self.assertEqual(elem.get_computed_style("fill"), Color("hsl(175, 75, 50)"))
|
|
|
|
def test_hsla_color(self):
|
|
elem = PathElement()
|
|
elem.style["fill"] = "hsla(175, 75, 50, 0.5)"
|
|
self.assertEqual(
|
|
elem.specified_style()("fill"), Color("hsla(175, 75, 50, 0.5)")
|
|
)
|
|
self.assertEqual(
|
|
elem.get_computed_style("fill"), Color("hsla(175, 75, 50, 0.5)")
|
|
)
|
|
|
|
|
|
def test_overwrite():
|
|
style = Style(
|
|
"""marker-start: url(#id1) !important;
|
|
marker-mid: None;
|
|
marker: url(#id2);
|
|
marker-end: url(#id3);
|
|
"""
|
|
)
|
|
assert style["marker-start"] == "url(#id1)"
|
|
assert style.get_importance("marker-start")
|
|
assert style["marker-mid"] == "url(#id2)"
|
|
assert not style.get_importance("marker-mid")
|
|
assert style["marker-end"] == "url(#id3)"
|
|
assert not style.get_importance("marker-end")
|