dots/config/inkscape/extensions/org.inkscape.extension.30306/scientific_inkscape/styles0.py
2026-06-05 13:11:08 +02:00

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)