#!/usr/bin/env python
# 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
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,
BaseStyleValue,
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:
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:
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(style["stroke-dasharray"], "none")
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")
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))
stroke = objects[3].specified_style()("stroke")
self.assertEqual(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 URL format"):
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"), doc.getElementById("Arrow1Lend"))
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, Exception]] = [
(r"opacity: abc", ValueError),
(r"fill: #GHI", ColorError),
(r"stroke: url(#missing)", ValueError),
(r"fill: ", ColorError),
(r"font-variant: blue", ValueError),
]
for decl, exceptiontype in tests:
with self.assertRaises(exceptiontype):
value = BaseStyleValue.factory(declaration=decl)
_ = value.parse_value(doc)
self.assertEqual(
BaseStyleValue.factory_errorhandled(element=doc, declaration=decl), None
)
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 font-variant",
),
("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_basestylevalue(self):
"""Create BaseStyleValue's directly and work on them"""
val1 = BaseStyleValue.factory("fill: red;")
self.assertEqual(val1.parse_value(), Color("red"))
# Compare the style
self.assertNotEqual(val1, "fill: red;")
# Create a rule with an invalid declaration
with self.assertRaises(ValueError):
_ = BaseStyleValue.parse_declaration("fill=red;")
# Try to apply a shorthand to the wrong style
val2 = BaseStyleValue.factory("font: 12pt Verdana")
style = Style("fill: context-fill;")
copy = style.copy()
val2.apply_shorthand(style)
self.assertEqual(style, copy)
# Set a value to the wrong key
with self.assertRaises(ValueError):
style["stroke"] = BaseStyleValue.factory("font: 12pt Verdana")
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 = """
"""
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 = """"""
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 = """"""
doc = etree.fromstring(content, parser=SVG_PARSER)
group = doc.getElementById("test")
self.assertEqual(group.specified_style()("fill"), Color("black"))
self.assertEqual(group.specified_style()("bla"), None)
def test_direct_child_and_import(self):
content = """"""
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", None),
("1.111", [1.111, 1.111]),
("1px, 2px, 3px", [1, 2, 3, 1, 2, 3]),
("", None),
("1 -2", None),
(None, 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}"
)