247 lines
No EOL
8.5 KiB
Python
247 lines
No EOL
8.5 KiB
Python
# Fork of the v1.1 Style
|
|
# Adds some instance checks to reduce number of inits that need to be called
|
|
# Uses dicts instead of OrderedDicts, which are faster
|
|
|
|
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
|
|
|
|
# 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-1301, USA.
|
|
|
|
import inkex
|
|
|
|
import inspect
|
|
from functools import lru_cache
|
|
|
|
|
|
def count_callers():
|
|
caller_frame = inspect.stack()[2]
|
|
filename = caller_frame.filename
|
|
line_number = caller_frame.lineno
|
|
lstr = f"{filename} at line {line_number}"
|
|
global callinfo
|
|
try:
|
|
callinfo
|
|
except:
|
|
callinfo = dict()
|
|
if lstr in callinfo:
|
|
callinfo[lstr] += 1
|
|
else:
|
|
callinfo[lstr] = 1
|
|
inkex.utils.debug(lstr)
|
|
|
|
|
|
class Style0(dict):
|
|
"""A list of style directives"""
|
|
|
|
color_props = ("stroke", "fill", "stop-color", "flood-color", "lighting-color")
|
|
opacity_props = ("stroke-opacity", "fill-opacity", "opacity", "stop-opacity")
|
|
unit_props = "stroke-width"
|
|
|
|
# We modify Style so that it has two versions: one without the callback
|
|
# (Style0) and one with (Style0cb). That way, when no callback is needed,
|
|
# we do not incur extra overhead by overloading __setitem__, __delitem__, etc.
|
|
def __new__(cls, style=None, callback=None, **kw):
|
|
if cls != Style0 and issubclass(
|
|
cls, Style0
|
|
): # Don't treat subclasses' arguments as callback
|
|
return dict.__new__(cls)
|
|
elif callback is not None:
|
|
instance = dict.__new__(Style0cb)
|
|
instance.__init__(style, callback, **kw)
|
|
return instance
|
|
else:
|
|
return dict.__new__(cls)
|
|
|
|
def __init__(self, style=None, **kw):
|
|
# Either a string style or kwargs (with dashes as underscores).
|
|
if style is None:
|
|
if kw:
|
|
style = ((k.replace("_", "-"), v) for k, v in kw.items())
|
|
else:
|
|
return
|
|
elif isinstance(style, str):
|
|
style = self.parse_str(style)
|
|
# Order raw dictionaries so tests can be made reliable
|
|
# if isinstance(style, dict) and not isinstance(style, inkex.OrderedDict):
|
|
# style = [(name, style[name]) for name in sorted(style)]
|
|
# Should accept dict, Style, parsed string, list etc.
|
|
# dict.__init__(self,style)
|
|
self.update(style)
|
|
|
|
@staticmethod
|
|
@lru_cache(maxsize=None)
|
|
def parse_str(style):
|
|
"""Create a dictionary from the value of an inline style attribute"""
|
|
if style is None:
|
|
style = ""
|
|
ret = []
|
|
for directive in style.split(";"):
|
|
if ":" in directive:
|
|
(name, value) = directive.split(":", 1)
|
|
# FUTURE: Parse value here for extra functionality
|
|
ret.append((name.strip().lower(), value.strip()))
|
|
return ret
|
|
|
|
def __str__(self):
|
|
"""Format an inline style attribute from a dictionary"""
|
|
return ';'.join(
|
|
[f"{key}:{value}" for key, value in self.items()]
|
|
)
|
|
|
|
def to_str(self, sep=";"):
|
|
"""Convert to string using a custom delimiter"""
|
|
return sep.join(
|
|
[f"{key}:{value}" for key, value in self.items()]
|
|
)
|
|
|
|
def __hash__(self):
|
|
return hash(tuple(self.items()))
|
|
# return hash(self.to_str())
|
|
|
|
def __add__(self, other):
|
|
"""Add two styles together to get a third, composing them"""
|
|
# ret = self.copy()
|
|
# ret.update(other)
|
|
ret = dict.__new__(type(self))
|
|
ret.update(self)
|
|
ret.update(other)
|
|
return ret
|
|
|
|
def add3(self,other1,other2):
|
|
ret = dict.__new__(type(self))
|
|
ret.update(self)
|
|
ret.update(other1)
|
|
ret.update(other2)
|
|
return ret
|
|
|
|
# A shallow copy that does not call __init__
|
|
def copy(self):
|
|
new_instance = dict.__new__(type(self))
|
|
new_instance.update(self)
|
|
return new_instance
|
|
|
|
def __iadd__(self, other):
|
|
"""Add style to this style, the same as style.update(dict)"""
|
|
self.update(other)
|
|
return self
|
|
|
|
def __sub__(self, other):
|
|
"""Remove keys and return copy"""
|
|
ret = self.copy()
|
|
ret.__isub__(other)
|
|
return ret
|
|
|
|
def __isub__(self, other):
|
|
"""Remove keys from this style, list of keys or other style dictionary"""
|
|
for key in other:
|
|
self.pop(key, None)
|
|
return self
|
|
|
|
# def __eq__(self, other):
|
|
# """Not equals, prefer to overload 'in' but that doesn't seem possible"""
|
|
# if not isinstance(other, Style0):
|
|
# other = Style0(other)
|
|
# return dict.__eq__(self,other)
|
|
# # Inkex uses dict comparison, not OrderedDict
|
|
# # for arg in set(self) | set(other):
|
|
# # if self.get(arg, None) != other.get(arg, None):
|
|
# # return False
|
|
# # return True
|
|
|
|
__ne__ = lambda self, other: not self.__eq__(other)
|
|
|
|
# def update(self, other):
|
|
# if not (isinstance(other, Style0)):
|
|
# other = Style0(other)
|
|
# super().update(other)
|
|
|
|
def get_color(self, name="fill"):
|
|
"""Get the color AND opacity as one Color object"""
|
|
color = inkex.Color(self.get(name, "none"))
|
|
return color.to_rgba(self.get(name + "-opacity", 1.0))
|
|
|
|
def set_color(self, color, name="fill"):
|
|
"""Sets the given color AND opacity as rgba to the fill or stroke style properties."""
|
|
color = inkex.Color(color)
|
|
if color.space == "rgba":
|
|
self[name + "-opacity"] = color.alpha
|
|
self[name] = str(color.to_rgb())
|
|
|
|
def update_urls(self, old_id, new_id):
|
|
"""Find urls in this style and replace them with the new id"""
|
|
for name, value in self.items():
|
|
if value == f"url(#{old_id})":
|
|
self[name] = f"url(#{new_id})"
|
|
|
|
def interpolate_prop(self, other, fraction, prop, svg=None):
|
|
"""Interpolate specific property."""
|
|
a1 = self[prop]
|
|
a2 = other.get(prop, None)
|
|
if a2 is None:
|
|
val = a1
|
|
else:
|
|
if prop in self.color_props:
|
|
if isinstance(a1, inkex.Color):
|
|
val = a1.interpolate(inkex.Color(a2), fraction)
|
|
elif a1.startswith("url(") or a2.startswith("url("):
|
|
# gradient requires changes to the whole svg
|
|
# and needs to be handled externally
|
|
val = a1
|
|
else:
|
|
val = inkex.Color(a1).interpolate(inkex.Color(a2), fraction)
|
|
elif prop in self.opacity_props:
|
|
val = inkex.interpcoord(float(a1), float(a2), fraction)
|
|
elif prop in self.unit_props:
|
|
val = inkex.interpunit(a1, a2, fraction)
|
|
else:
|
|
val = a1
|
|
return val
|
|
|
|
def interpolate(self, other, fraction):
|
|
"""Interpolate all properties."""
|
|
style = Style0()
|
|
for prop, value in self.items():
|
|
style[prop] = self.interpolate_prop(other, fraction, prop)
|
|
return style
|
|
|
|
|
|
class Style0cb(Style0):
|
|
def __init__(self, style=None, callback=None, **kw):
|
|
# This callback is set twice because this is 'pre-initial' data (no callback)
|
|
self.callback = None
|
|
super().__init__(style, **kw)
|
|
self.callback = callback
|
|
|
|
# A shallow copy that does not call __init__
|
|
def copy(self):
|
|
new_instance = type(self).__new__(type(self))
|
|
for k, v in self.items():
|
|
dict.__setitem__(new_instance, k, v)
|
|
new_instance.callback = self.callback
|
|
return new_instance
|
|
|
|
def update(self, other):
|
|
super().update(other)
|
|
if self.callback is not None:
|
|
self.callback(self)
|
|
|
|
def __delitem__(self, key):
|
|
super().__delitem__(key)
|
|
if self.callback is not None:
|
|
self.callback(self)
|
|
|
|
def __setitem__(self, key, value):
|
|
super().__setitem__(key, value)
|
|
if self.callback is not None:
|
|
self.callback(self) |