bundle: update (2026-01-18)

This commit is contained in:
2026-01-18 04:47:51 +00:00
parent 822898e4a3
commit 7597f2f156
223 changed files with 31167 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
# coding=utf-8
"""
This describes the core API for the inkex core modules.
This provides the basis from which you can develop your inkscape extension.
"""
# pylint: disable=wildcard-import
import sys
from .extensions import *
from .utils import AbortExtension, DependencyError, Boolean, errormsg
from .styles import *
from .paths import Path, CubicSuperPath # Path commands are not exported
from .colors import Color, ColorError, ColorIdError, is_color
from .colors.spaces import *
from .transforms import *
from .elements import *
# legacy proxies
from .deprecated import Effect
from .deprecated import localize
from .deprecated import debug
# legacy functions
from .deprecated import are_near_relative
from .deprecated import unittouu
MIN_VERSION = (3, 7)
if sys.version_info < MIN_VERSION:
sys.exit("Inkscape extensions require Python 3.7 or greater.")
__version__ = "1.4.0" # Version number for inkex; may differ from Inkscape version.

View File

@@ -0,0 +1,567 @@
# coding=utf-8
#
# Copyright (c) 2018 - Martin Owens <doctormo@gmail.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-1301, USA.
#
"""
The ultimate base functionality for every Inkscape extension.
"""
import io
import os
import re
import sys
import copy
from typing import (
Dict,
List,
Tuple,
Type,
Optional,
Callable,
Any,
Union,
IO,
TYPE_CHECKING,
cast,
)
from argparse import ArgumentParser, Namespace
from lxml import etree
from .utils import filename_arg, AbortExtension, ABORT_STATUS, errormsg, do_nothing
from .elements._parser import load_svg
from .elements._utils import NSS
from .localization import localize
if TYPE_CHECKING:
from .elements._svg import SvgDocumentElement
from .elements._base import BaseElement
class InkscapeExtension:
"""
The base class extension, provides argument parsing and basic
variable handling features.
"""
multi_inx = False # Set to true if this class is used by multiple inx files.
extra_nss = {} # type: Dict[str, str]
# Provide a unique value to allow detection of no argument specified
# for `output` parameter of `run()`, not even `None`; this has to be an io
# type for type checking purposes:
output_unspecified = io.StringIO("")
def __init__(self):
# type: () -> None
NSS.update(self.extra_nss)
self.file_io = None # type: Optional[IO]
self.options = Namespace()
self.document = None # type: Union[None, bytes, str, etree.element]
self.arg_parser = ArgumentParser(description=self.__doc__)
self.arg_parser.add_argument(
"input_file",
nargs="?",
metavar="INPUT_FILE",
type=filename_arg,
help="Filename of the input file (default is stdin)",
default=None,
)
self.arg_parser.add_argument(
"--output",
type=str,
default=None,
help="Optional output filename for saving the result (default is stdout).",
)
self.add_arguments(self.arg_parser)
localize()
def add_arguments(self, pars):
# type: (ArgumentParser) -> None
"""Add any extra arguments to your extension handle, use:
def add_arguments(self, pars):
pars.add_argument("--num-cool-things", type=int, default=3)
pars.add_argument("--pos-in-doc", type=str, default="doobry")
"""
# No extra arguments by default so super is not required
def parse_arguments(self, args):
# type: (List[str]) -> None
"""Parse the given arguments and set 'self.options'"""
self.options = self.arg_parser.parse_args(args)
def arg_method(self, prefix="method"):
# type: (str) -> Callable[[str], Callable[[Any], Any]]
"""Used by add_argument to match a tab selection with an object method
pars.add_argument("--tab", type=self.arg_method(), default="foo")
...
self.options.tab(arguments)
...
.. code-block:: python
.. def method_foo(self, arguments):
.. # do something
"""
def _inner(value):
name = f"""{prefix}_{value.strip('"').lower()}""".replace("-", "_")
try:
return getattr(self, name)
except AttributeError as error:
if name.startswith("_"):
return do_nothing
raise AbortExtension(f"Can not find method {name}") from error
return _inner
@staticmethod
def arg_number_ranges():
"""Parses a number descriptor. e.g:
``1,2,4-5,7,9-`` is parsed to ``1, 2, 4, 5, 7, 9, 10, ..., lastvalue``
.. versionadded:: 1.2
Usage:
.. code-block:: python
# in add_arguments()
pars.add_argument("--pages", type=self.arg_number_ranges(), default=1-)
# later on, pages is then a list of ints
pages = self.options.pages(lastvalue)
"""
def _inner(value):
def method(pages, lastvalue, startvalue=1):
# replace ranges, such as -3, 10- with startvalue,2,3,10..lastvalue
pages = re.sub(
r"(\d+|)\s?-\s?(\d+|)",
lambda m: (
",".join(
map(
str,
range(
int(m.group(1) or startvalue),
int(m.group(2) or lastvalue) + 1,
),
)
)
if not (m.group(1) or m.group(2)) == ""
else ""
),
pages,
)
pages = map(int, re.findall(r"(\d+)", pages))
pages = tuple({i for i in pages if i <= lastvalue})
return pages
return lambda lastvalue, startvalue=1: method(
value, lastvalue, startvalue=startvalue
)
return _inner
@staticmethod
def arg_class(options: List[Type]) -> Callable[[str], Any]:
"""Used by add_argument to match an option with a class
Types to choose from are given by the options list
.. versionadded:: 1.2
Usage:
.. code-block:: python
pars.add_argument("--class", type=self.arg_class([ClassA, ClassB]),
default="ClassA")
"""
def _inner(value: str):
name = value.strip('"')
for i in options:
if name == i.__name__:
return i
raise AbortExtension(f"Can not find class {name}")
return _inner
def debug(self, msg):
# type: (str) -> None
"""Write a debug message"""
errormsg(f"DEBUG<{type(self).__name__}> {msg}\n")
@staticmethod
def msg(msg):
# type: (str) -> None
"""Write a non-error message"""
errormsg(msg)
def run(self, args=None, output=output_unspecified):
# type: (Optional[List[str]], Union[str, IO]) -> None
"""Main entrypoint for any Inkscape Extension"""
try:
if args is None:
args = sys.argv[1:]
self.parse_arguments(args)
if self.options.input_file is None:
self.options.input_file = sys.stdin
elif "DOCUMENT_PATH" not in os.environ:
os.environ["DOCUMENT_PATH"] = self.options.input_file
self.bin_stdout = None
if self.options.output is None:
# If no output was specified, attempt to extract a binary
# output from stdout, and if that doesn't seem possible,
# punt and try whatever stream stdout is:
if output is InkscapeExtension.output_unspecified:
output = sys.stdout
if "b" not in getattr(output, "mode", "") and not isinstance(
output, (io.RawIOBase, io.BufferedIOBase)
):
if hasattr(output, "buffer"):
output = output.buffer # type: ignore
elif hasattr(output, "fileno"):
self.bin_stdout = os.fdopen(
output.fileno(), "wb", closefd=False
)
output = self.bin_stdout
self.options.output = output
self.load_raw()
self.save_raw(self.effect())
except AbortExtension as err:
errormsg(str(err))
sys.exit(ABORT_STATUS)
finally:
self.clean_up()
def load_raw(self):
# type: () -> None
"""Load the input stream or filename, save everything to self"""
if isinstance(self.options.input_file, str):
# pylint: disable=consider-using-with
self.file_io = open(self.options.input_file, "rb")
document = self.load(self.file_io)
else:
document = self.load(self.options.input_file)
self.document = document
def save_raw(self, ret):
# type: (Any) -> None
"""Save to the output stream, use everything from self"""
if self.has_changed(ret):
if isinstance(self.options.output, str):
with open(self.options.output, "wb") as stream:
self.save(stream)
else:
if sys.platform == "win32" and not "PYTEST_CURRENT_TEST" in os.environ:
# When calling an extension from within Inkscape on Windows,
# Python thinks that the output stream is seekable
# (https://gitlab.com/inkscape/inkscape/-/issues/3273)
self.options.output.seekable = lambda self: False
def seek_replacement(offset: int, whence: int = 0):
raise AttributeError(
"We can't seek in the stream passed by Inkscape on Windows"
)
def tell_replacement():
raise AttributeError(
"We can't tell in the stream passed by Inkscape on Windows"
)
# Some libraries (e.g. ZipFile) don't query seekable, but check for an error
# on seek
self.options.output.seek = seek_replacement
self.options.output.tell = tell_replacement
self.save(self.options.output)
def load(self, stream):
# type: (IO) -> str
"""Takes the input stream and creates a document for parsing"""
raise NotImplementedError(f"No input handle for {self.name}")
def save(self, stream):
# type: (IO) -> None
"""Save the given document to the output file"""
raise NotImplementedError(f"No output handle for {self.name}")
def effect(self):
# type: () -> Any
"""Apply some effects on the document or local context"""
raise NotImplementedError(f"No effect handle for {self.name}")
def has_changed(self, ret): # pylint: disable=no-self-use
# type: (Any) -> bool
"""Return true if the output should be saved"""
return ret is not False
def clean_up(self):
# type: () -> None
"""Clean up any open handles and other items"""
if hasattr(self, "bin_stdout"):
if self.bin_stdout is not None:
self.bin_stdout.close()
if self.file_io is not None:
self.file_io.close()
@classmethod
def svg_path(cls, default=None):
# type: (Optional[str]) -> Optional[str]
"""
Return the folder the svg is contained in.
Returns None if there is no file.
.. versionchanged:: 1.1
A default path can be given which is returned in case no path to the
SVG file can be determined.
"""
path = cls.document_path()
if path:
return os.path.dirname(path)
if default:
return default
return path # Return None or '' for context
@classmethod
def ext_path(cls):
# type: () -> str
"""Return the folder the extension script is in"""
return os.path.dirname(sys.modules[cls.__module__].__file__ or "")
@classmethod
def get_resource(cls, name, abort_on_fail=True):
# type: (str, bool) -> str
"""Return the full filename of the resource in the extension's dir
.. versionadded:: 1.1"""
filename = cls.absolute_href(name, cwd=cls.ext_path())
if abort_on_fail and not os.path.isfile(filename):
raise AbortExtension(f"Could not find resource file: {filename}")
return filename
@classmethod
def document_path(cls):
# type: () -> Optional[str]
"""Returns the saved location of the document
* Normal return is a string containing the saved location
* Empty string means the document was never saved
* 'None' means this version of Inkscape doesn't support DOCUMENT_PATH
DO NOT READ OR WRITE TO THE DOCUMENT FILENAME!
* Inkscape may have not written the latest changes, leaving you reading old
data.
* Inkscape will not respect anything you write to the file, causing data loss.
.. versionadded:: 1.1
"""
return os.environ.get("DOCUMENT_PATH", None)
@classmethod
def absolute_href(cls, filename, default="~/", cwd=None):
# type: (str, str, Optional[str]) -> str
"""
Process the filename such that it's turned into an absolute filename
with the working directory being the directory of the loaded svg.
User's home folder is also resolved. So '~/a.png` will be `/home/bob/a.png`
Default is a fallback working directory to use if the svg's filename is not
available.
.. versionchanged:: 1.1
If you set default to None, then the user will be given errors if
there's no working directory available from Inkscape.
"""
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
if cwd is None:
cwd = cls.svg_path(default)
if cwd is None:
raise AbortExtension(
"Can not use relative path, Inkscape isn't telling us the "
"current working directory."
)
if cwd == "":
raise AbortExtension(
"The SVG must be saved before you can use relative paths."
)
filename = os.path.join(cwd, filename)
return os.path.realpath(os.path.expanduser(filename))
@property
def name(self):
# type: () -> str
"""Return a fixed name for this extension"""
return type(self).__name__
if TYPE_CHECKING:
_Base = InkscapeExtension
else:
_Base = object
class TempDirMixin(_Base): # pylint: disable=abstract-method
"""
Provide a temporary directory for extensions to stash files.
"""
dir_suffix = ""
dir_prefix = "inktmp"
def __init__(self, *args, **kwargs):
self.tempdir = None
self._tempdir = None
super().__init__(*args, **kwargs)
def load_raw(self):
# type: () -> None
"""Create the temporary directory"""
# pylint: disable=import-outside-toplevel
from tempfile import TemporaryDirectory
# Need to hold a reference to the Directory object or else it might get GC'd
self._tempdir = TemporaryDirectory( # pylint: disable=consider-using-with
prefix=self.dir_prefix, suffix=self.dir_suffix
)
self.tempdir = os.path.realpath(self._tempdir.name)
super().load_raw()
def clean_up(self):
# type: () -> None
"""Delete the temporary directory"""
self.tempdir = None
# if the file does not exist, _tempdir is never set.
if self._tempdir is not None:
self._tempdir.cleanup()
super().clean_up()
class SvgInputMixin(_Base): # pylint: disable=too-few-public-methods, abstract-method
"""
Expects the file input to be an svg document and will parse it.
"""
# Select all objects if none are selected
select_all: Tuple[Type["BaseElement"], ...] = ()
def __init__(self):
super().__init__()
self.arg_parser.add_argument(
"--id",
action="append",
type=str,
dest="ids",
default=[],
help="id attribute of object to manipulate",
)
self.arg_parser.add_argument(
"--selected-nodes",
action="append",
type=str,
dest="selected_nodes",
default=[],
help="id:subpath:position of selected nodes, if any",
)
def load(self, stream):
# type: (IO) -> etree
"""Load the stream as an svg xml etree and make a backup"""
document = load_svg(stream)
self.original_document = copy.deepcopy(document)
self.svg: SvgDocumentElement = document.getroot()
self.svg.selection.set(*self.options.ids)
if not self.svg.selection and self.select_all:
self.svg.selection = self.svg.descendants().filter(*self.select_all)
return document
class SvgOutputMixin(_Base): # pylint: disable=too-few-public-methods, abstract-method
"""
Expects the output document to be an svg document and will write an etree xml.
A template can be specified to kick off the svg document building process.
"""
template = """<svg viewBox="0 0 {width} {height}"
width="{width}{unit}" height="{height}{unit}"
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
</svg>"""
@classmethod
def get_template(cls, **kwargs):
"""
Opens a template svg document for building, the kwargs
MUST include all the replacement values in the template, the
default template has 'width' and 'height' of the document.
"""
kwargs.setdefault("unit", "")
return load_svg(str(cls.template.format(**kwargs)))
def save(self, stream):
# type: (IO) -> None
"""Save the svg document to the given stream"""
if isinstance(self.document, (bytes, str)):
document = self.document
elif "Element" in type(self.document).__name__:
# isinstance can't be used here because etree is broken
doc = cast(etree, self.document)
document = doc.getroot().tostring()
else:
raise ValueError(
f"Unknown type of document: {type(self.document).__name__} can not"
+ "save."
)
try:
stream.write(document)
except TypeError:
# we hope that this happens only when document needs to be encoded
stream.write(document.encode("utf-8")) # type: ignore
class SvgThroughMixin(SvgInputMixin, SvgOutputMixin): # pylint: disable=abstract-method
"""
Combine the input and output svg document handling (usually for effects).
"""
def has_changed(self, ret): # pylint: disable=unused-argument
# type: (Any) -> bool
"""Return true if the svg document has changed"""
original = etree.tostring(self.original_document)
result = etree.tostring(self.document)
return original != result

View File

@@ -0,0 +1,582 @@
# coding=utf-8
#
# Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
# 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.
#
# pylint: disable=invalid-name,too-many-locals
#
"""
Bezier calculations
"""
import cmath
import math
import numpy
from .transforms import DirectedLineSegment
from .localization import inkex_gettext as _
# bez = ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))
def pointdistance(point_a, point_b):
"""The straight line distance between two points"""
return math.sqrt(
((point_b[0] - point_a[0]) ** 2) + ((point_b[1] - point_a[1]) ** 2)
)
def between_point(point_a, point_b, time=0.5):
"""Returns the point between point a and point b"""
return point_a[0] + time * (point_b[0] - point_a[0]), point_a[1] + time * (
point_b[1] - point_a[1]
)
def percent_point(point_a, point_b, percent=50.0):
"""Returns between_point but takes percent instead of 0.0-1.0"""
return between_point(point_a, point_b, percent / 100.0)
def root_wrapper(root_a, root_b, root_c, root_d):
"""Get the Cubic function, moic formular of roots, simple root"""
if root_a:
# Monics formula, see
# http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
mono_a, mono_b, mono_c = (root_b / root_a, root_c / root_a, root_d / root_a)
m = 2.0 * mono_a**3 - 9.0 * mono_a * mono_b + 27.0 * mono_c
k = mono_a**2 - 3.0 * mono_b
n = m**2 - 4.0 * k**3
w1 = -0.5 + 0.5 * cmath.sqrt(-3.0)
w2 = -0.5 - 0.5 * cmath.sqrt(-3.0)
if n < 0:
m1 = pow(complex((m + cmath.sqrt(n)) / 2), 1.0 / 3)
n1 = pow(complex((m - cmath.sqrt(n)) / 2), 1.0 / 3)
else:
if m + math.sqrt(n) < 0:
m1 = -pow(-(m + math.sqrt(n)) / 2, 1.0 / 3)
else:
m1 = pow((m + math.sqrt(n)) / 2, 1.0 / 3)
if m - math.sqrt(n) < 0:
n1 = -pow(-(m - math.sqrt(n)) / 2, 1.0 / 3)
else:
n1 = pow((m - math.sqrt(n)) / 2, 1.0 / 3)
return (
-1.0 / 3 * (mono_a + m1 + n1),
-1.0 / 3 * (mono_a + w1 * m1 + w2 * n1),
-1.0 / 3 * (mono_a + w2 * m1 + w1 * n1),
)
if root_b:
det = root_c**2.0 - 4.0 * root_b * root_d
if det:
return (
(-root_c + cmath.sqrt(det)) / (2.0 * root_b),
(-root_c - cmath.sqrt(det)) / (2.0 * root_b),
)
return (-root_c / (2.0 * root_b),)
if root_c:
return (1.0 * (-root_d / root_c),)
return ()
def bezlenapprx(sp1, sp2):
"""Return the aproximate length between two beziers"""
return (
pointdistance(sp1[1], sp1[2])
+ pointdistance(sp1[2], sp2[0])
+ pointdistance(sp2[0], sp2[1])
)
def cspbezsplit(sp1, sp2, time=0.5):
"""Split a cubic bezier at the time period"""
m1 = tpoint(sp1[1], sp1[2], time)
m2 = tpoint(sp1[2], sp2[0], time)
m3 = tpoint(sp2[0], sp2[1], time)
m4 = tpoint(m1, m2, time)
m5 = tpoint(m2, m3, time)
m = tpoint(m4, m5, time)
return [[sp1[0][:], sp1[1][:], m1], [m4, m, m5], [m3, sp2[1][:], sp2[2][:]]]
def cspbezsplitatlength(sp1, sp2, length=0.5, tolerance=0.001):
"""Split a cubic bezier at length"""
bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
time = beziertatlength(bez, length, tolerance)
return cspbezsplit(sp1, sp2, time)
def cspseglength(sp1, sp2, tolerance=0.001):
"""Get cubic bezier segment length"""
bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
return bezierlength(bez, tolerance)
def csplength(csp):
"""Get cubic bezier length"""
total = 0
lengths = []
for sp in csp:
lengths.append([])
for i in range(1, len(sp)):
l = cspseglength(sp[i - 1], sp[i])
lengths[-1].append(l)
total += l
return lengths, total
def bezierparameterize(bez):
"""Return the bezier parameter size
Converts the bezier parametrisation from the default form
P(t) = (1-t)³ P_1 + 3(1-t)²t P_2 + 3(1-t)t² P_3 + t³ x_4
to the a form which can be differentiated more easily
P(t) = a t³ + b t² + c t + P0
Args:
bez (List[Tuple[float, float]]): the Bezier curve. The elements of the list the
coordinates of the points (in this order): Start point, Start control point,
End control point, End point.
Returns:
Tuple[float, float, float, float, float, float, float, float]:
the values ax, ay, bx, by, cx, cy, x0, y0
"""
((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) = bez
# parametric bezier
x0 = bx0
y0 = by0
cx = 3 * (bx1 - x0)
bx = 3 * (bx2 - bx1) - cx
ax = bx3 - x0 - cx - bx
cy = 3 * (by1 - y0)
by = 3 * (by2 - by1) - cy
ay = by3 - y0 - cy - by
return ax, ay, bx, by, cx, cy, x0, y0
def linebezierintersect(arg_a, bez):
"""Where a line and bezier intersect"""
((lx1, ly1), (lx2, ly2)) = arg_a
# parametric line
dd = lx1
cc = lx2 - lx1
bb = ly1
aa = ly2 - ly1
if aa:
coef1 = cc / aa
coef2 = 1
else:
coef1 = 1
coef2 = aa / cc
ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
# cubic intersection coefficients
a = coef1 * ay - coef2 * ax
b = coef1 * by - coef2 * bx
c = coef1 * cy - coef2 * cx
d = coef1 * (y0 - bb) - coef2 * (x0 - dd)
roots = root_wrapper(a, b, c, d)
retval = []
for i in roots:
if isinstance(i, complex) and i.imag == 0:
i = i.real
if not isinstance(i, complex) and 0 <= i <= 1:
retval.append(bezierpointatt(bez, i))
return retval
def bezierpointatt(bez, t):
"""Get coords at the given time point along a bezier curve"""
ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
x = ax * (t**3) + bx * (t**2) + cx * t + x0
y = ay * (t**3) + by * (t**2) + cy * t + y0
return x, y
def bezierslopeatt(bez, t):
"""Get slope at the given time point along a bezier curve
The slope is computed as (dx, dy) where dx = df_x(t)/dt and dy = df_y(t)/dt.
Note that for lines P1=P2 and P3=P4, so the slope at the end points is dx=dy=0
(slope not defined).
Args:
bez (List[Tuple[float, float]]): the Bezier curve. The elements of the list the
coordinates of the points (in this order): Start point, Start control point,
End control point, End point.
t (float): time in the interval [0, 1]
Returns:
Tuple[float, float]: x and y increment
"""
ax, ay, bx, by, cx, cy, _, _ = bezierparameterize(bez)
dx = 3 * ax * (t**2) + 2 * bx * t + cx
dy = 3 * ay * (t**2) + 2 * by * t + cy
return dx, dy
def beziertatslope(bez, d):
"""Reverse; get time from slope along a bezier curve"""
ax, ay, bx, by, cx, cy, _, _ = bezierparameterize(bez)
(dy, dx) = d
# quadratic coefficients of slope formula
if dx:
slope = 1.0 * (dy / dx)
a = 3 * ay - 3 * ax * slope
b = 2 * by - 2 * bx * slope
c = cy - cx * slope
elif dy:
slope = 1.0 * (dx / dy)
a = 3 * ax - 3 * ay * slope
b = 2 * bx - 2 * by * slope
c = cx - cy * slope
else:
return []
roots = root_wrapper(0, a, b, c)
retval = []
for i in roots:
if isinstance(i, complex) and i.imag == 0:
i = i.real
if not isinstance(i, complex) and 0 <= i <= 1:
retval.append(i)
return retval
def tpoint(p1, p2, t):
"""Linearly interpolate between p1 and p2.
t = 0.0 returns p1, t = 1.0 returns p2.
:return: Interpolated point
:rtype: tuple
:param p1: First point as sequence of two floats
:param p2: Second point as sequence of two floats
:param t: Number between 0.0 and 1.0
:type t: float
"""
x1, y1 = p1
x2, y2 = p2
return x1 + t * (x2 - x1), y1 + t * (y2 - y1)
def beziersplitatt(bez, t):
"""Split bezier at given time"""
((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) = bez
m1 = tpoint((bx0, by0), (bx1, by1), t)
m2 = tpoint((bx1, by1), (bx2, by2), t)
m3 = tpoint((bx2, by2), (bx3, by3), t)
m4 = tpoint(m1, m2, t)
m5 = tpoint(m2, m3, t)
m = tpoint(m4, m5, t)
return ((bx0, by0), m1, m4, m), (m, m5, m3, (bx3, by3))
def addifclose(bez, l, error=0.001):
"""Gravesen, Add if the line is closed, in-place addition to array l"""
box = 0
for i in range(1, 4):
box += pointdistance(bez[i - 1], bez[i])
chord = pointdistance(bez[0], bez[3])
if (box - chord) > error:
first, second = beziersplitatt(bez, 0.5)
addifclose(first, l, error)
addifclose(second, l, error)
else:
l[0] += (box / 2.0) + (chord / 2.0)
# balfax, balfbx, balfcx, balfay, balfby, balfcy = 0, 0, 0, 0, 0, 0
def balf(t, args):
"""Bezier Arc Length Function"""
ax, bx, cx, ay, by, cy = args
retval = (ax * (t**2) + bx * t + cx) ** 2 + (ay * (t**2) + by * t + cy) ** 2
return math.sqrt(retval)
def simpson(start, end, maxiter, tolerance, bezier_args):
"""Calculate the length of a bezier curve using Simpson's algorithm:
http://steve.hollasch.net/cgindex/curves/cbezarclen.html
Args:
start (int): Start time (between 0 and 1)
end (int): End time (between start time and 1)
maxiter (int): Maximum number of iterations. If not a power of 2, the algorithm
will behave like the value is set to the next power of 2.
tolerance (float): maximum error ratio
bezier_args (list): arguments as computed by bezierparametrize()
Returns:
float: the appoximate length of the bezier curve
"""
n = 2
multiplier = (end - start) / 6.0
endsum = balf(start, bezier_args) + balf(end, bezier_args)
interval = (end - start) / 2.0
asum = 0.0
bsum = balf(start + interval, bezier_args)
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
est0 = 2.0 * est1
# print(multiplier, endsum, interval, asum, bsum, est1, est0)
while n < maxiter and abs(est1 - est0) > tolerance:
n *= 2
multiplier /= 2.0
interval /= 2.0
asum += bsum
bsum = 0.0
est0 = est1
for i in range(1, n, 2):
bsum += balf(start + (i * interval), bezier_args)
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
# print(multiplier, endsum, interval, asum, bsum, est1, est0)
return est1
def bezierlength(bez, tolerance=0.001, time=1.0):
"""Get length of bezier curve"""
ax, ay, bx, by, cx, cy, _, _ = bezierparameterize(bez)
return simpson(0.0, time, 4096, tolerance, [3 * ax, 2 * bx, cx, 3 * ay, 2 * by, cy])
def beziertatlength(bez, l=0.5, tolerance=0.001):
"""Get bezier curve time at the length specified"""
curlen = bezierlength(bez, tolerance, 1.0)
time = 1.0
tdiv = time
targetlen = l * curlen
diff = curlen - targetlen
while abs(diff) > tolerance:
tdiv /= 2.0
if diff < 0:
time += tdiv
else:
time -= tdiv
curlen = bezierlength(bez, tolerance, time)
diff = curlen - targetlen
return time
def maxdist(bez):
"""Get maximum distance within bezier curve"""
seg = DirectedLineSegment(bez[0], bez[3])
return max(seg.distance_to_point(*bez[1]), seg.distance_to_point(*bez[2]))
def cspsubdiv(csp, flat):
"""Sub-divide cubic sub-paths"""
for sp in csp:
subdiv(sp, flat)
def subdiv(sp, flat, i=1):
"""sub divide bezier curve"""
while i < len(sp):
p0 = sp[i - 1][1]
p1 = sp[i - 1][2]
p2 = sp[i][0]
p3 = sp[i][1]
bez = (p0, p1, p2, p3)
mdist = maxdist(bez)
if mdist <= flat:
i += 1
else:
one, two = beziersplitatt(bez, 0.5)
sp[i - 1][2] = one[1]
sp[i][0] = two[2]
p = [one[2], one[3], two[1]]
sp[i:1] = [p]
def csparea(csp):
r"""Get total area of cubic superpath.
.. hint::
The results may be slightly inaccurate for paths containing arcs due
to the loss of accuracy during arc -> cubic bezier conversion.
The function works as follows: For each subpath,
#. compute the area of the polygon created by the path's vertices:
For a line with coordinates :math:`(x_0, y_0)` and :math:`(x_1, y_1)`, the area
of the trapezoid of its projection on the x axis is given by
.. math::
\frac{1}{2} (y_1 + y_0) (x_1 - x_0)
Summing the contribution of all lines of the polygon yields the polygon's area
(lines from left to right have a positive contribution, while those right-to
left have a negative area contribution, canceling out the computed area not
inside the polygon), so we find (setting :math:`x_{0} = x_N` etc.):
.. math::
A = \frac{1}{2} * \sum_{i=1}^N (x_i y_i - x_{i-1} y_{i-1} + x_i y_{i-1}
- x_{i-1} y_{i})
The first two terms cancel out in the summation over all points, and the second
two terms can be regrouped as
.. math::
A = \frac{1}{2} * \sum_{i=1}^N x_i (y_{i+1} -y_{i-1})
#. The contribution by the bezier curve is considered: We compute
the integral :math:`\int_{x(t=0)}^{x(t=1)} y dx`, i.e. the area between the x
axis and the curve, where :math:`y = y(t)` (the Bezier curve). By substitution
:math:`dx = x'(t) dt`, performing the integration and
subtracting the trapezoid we already considered above, we find (with control
points :math:`(x_{c1}, y_{c1})` and :math:`(x_{c2}, y_{c2})`)
.. math::
\Delta A &= \int_0^1 y(t) x'(t) dt - \frac{1}{2} (y_1 + y_0) (x_1 - x_0) \\
&= \frac{3}{20} \cdot \begin{pmatrix}
& y_0(& & 2x_{c1} & + x_{c2} & -3x_1&) \\
+ & y_{c1}(& -2x_0 & & + x_{c2} &+ x_1&) \\
+ & y_{c2}(& -x_0 & -x_{c1} & & + 2x_1&) \\
+ & y_1(& 3x_0 & - x_{c1} & -2 x_{c2} &&)
\end{pmatrix}
This is computed for every bezier and added to the area. Again, this is a signed
area: convex beziers have a positive area and concave ones a negative area
contribution.
"""
MAT_AREA = numpy.array(
[[0, 2, 1, -3], [-2, 0, 1, 1], [-1, -1, 0, 2], [3, -1, -2, 0]]
)
area = 0.0
for sp in csp:
if len(sp) < 2:
continue
for x, coord in enumerate(sp): # calculate polygon area
area += 0.5 * sp[x - 1][1][0] * (coord[1][1] - sp[x - 2][1][1])
for i in range(1, len(sp)): # add contribution from cubic Bezier
# EXPLANATION: https://github.com/Pomax/BezierInfo-2/issues/238#issue-554619801
vec_x = numpy.array(
[sp[i - 1][1][0], sp[i - 1][2][0], sp[i][0][0], sp[i][1][0]]
)
vec_y = numpy.array(
[sp[i - 1][1][1], sp[i - 1][2][1], sp[i][0][1], sp[i][1][1]]
)
vex = numpy.matmul(vec_x, MAT_AREA)
area += 0.15 * numpy.matmul(vex, vec_y.T)
return -area
def cspcofm(csp):
r"""Get center of area / gravity for a cubic superpath.
.. hint::
The results may be slightly inaccurate for paths containing arcs due
to the loss of accuracy during arc -> cubic bezier conversion.
The function works similar to :func:`csparea`, only the computations are a bit more
difficult. Again all subpaths are considered. The total center of mass is given by
.. math::
C_y = \frac{1}{A} \int_A y dA
The integral can be expressed as a weighted sum; first, the contributions
of the polygon created by the path's nodes is computed. Second, we compute the
contribution of the Bezier curve; this is again done by an integral from which
the weighted CofM of the trapezoid between end points and horizontal axis is
removed. For the integrals, we have
.. math::
A * C_{y,bez} &= \int_A y dA = \int_{x(t=0)}^{y(t=1)} \int_{0}^{y(x)} y dy dx \\
&= \int_{x(t=0)}^{y(t=1)} \frac 12 y(x)^2 dx
= \int_0^1 \frac 12 y(t)^2 x'(t) dt \\
A * C_{x,bez} &= \int_A x dA = \int_{x(t=0)}^{y(t=1)} x \int_{0}^{y(x)} dy dx \\
&= \int_{x(t=0)}^{y(t=1)} x y(x) dx = \int_0^1 x(t) y(t) x'(t) dt
from which the trapezoids are removed, in case of the y-CofM this amounts to
.. math::
\frac{y_0}{2} (x_1-x_0)y_0 + \left(y_0 + \frac 13 (y_1 - y_0)\right)
\cdot \frac 12 (y_1 - y_0) (x_1 - x_0)
"""
MAT_COFM_0 = numpy.array(
[[0, 35, 10, -45], [-35, 0, 12, 23], [-10, -12, 0, 22], [45, -23, -22, 0]]
)
MAT_COFM_1 = numpy.array(
[[0, 15, 3, -18], [-15, 0, 9, 6], [-3, -9, 0, 12], [18, -6, -12, 0]]
)
MAT_COFM_2 = numpy.array(
[[0, 12, 6, -18], [-12, 0, 9, 3], [-6, -9, 0, 15], [18, -3, -15, 0]]
)
MAT_COFM_3 = numpy.array(
[[0, 22, 23, -45], [-22, 0, 12, 10], [-23, -12, 0, 35], [45, -10, -35, 0]]
)
area = csparea(csp)
xc = 0.0
yc = 0.0
if abs(area) < 1.0e-8:
raise ValueError(_("Area is zero, cannot calculate Center of Mass"))
for sp in csp:
for x, coord in enumerate(sp): # calculate polygon moment
xc += (
sp[x - 1][1][1]
* (sp[x - 2][1][0] - coord[1][0])
* (sp[x - 2][1][0] + sp[x - 1][1][0] + coord[1][0])
/ 6
)
yc += (
sp[x - 1][1][0]
* (coord[1][1] - sp[x - 2][1][1])
* (sp[x - 2][1][1] + sp[x - 1][1][1] + coord[1][1])
/ 6
)
for i in range(1, len(sp)): # add contribution from cubic Bezier
vec_x = numpy.array(
[sp[i - 1][1][0], sp[i - 1][2][0], sp[i][0][0], sp[i][1][0]]
)
vec_y = numpy.array(
[sp[i - 1][1][1], sp[i - 1][2][1], sp[i][0][1], sp[i][1][1]]
)
def _mul(MAT, vec_x=vec_x, vec_y=vec_y):
return numpy.matmul(numpy.matmul(vec_x, MAT), vec_y.T)
vec_t = numpy.array(
[_mul(MAT_COFM_0), _mul(MAT_COFM_1), _mul(MAT_COFM_2), _mul(MAT_COFM_3)]
)
xc += numpy.matmul(vec_x, vec_t.T) / 280
yc += numpy.matmul(vec_y, vec_t.T) / 280
return -xc / area, -yc / area

View File

@@ -0,0 +1,49 @@
# coding=utf-8
"""
The color module allows for the parsing and printing of CSS colors in an SVG document.
Support formats are currently:
1. #RGB #RRGGBB #RGBA #RRGGBBAA formats
2. Named colors such as 'red'
3. icc-color(...) which is specific to SVG 1.1
4. rgb(...) and rgba(...) from CSS Color Module 3
5. hsl(...) and hsla(...) from CSS Color Module 3
6. hwb(...) from CSS Color Module 4, but encoded internally as hsv
7. device-cmyk(...) from CSS Color Module 4
Each color space has it's own class, such as ColorRGB. Each space will parse multiple
formats, for example ColorRGB supports hex and rgb CSS module formats.
Each color object is a list of numbers, each number is a channel in that color space
with alpha channel being held in it's own property which may be a unit number or None.
The numbers a color stores are typically in the range defined in the CSS module
specification so for example RGB, all the numbers are between 0-255 while for hsl
the hue channel is between 0-360 and the saturation and lightness are between 0-100.
To get normalised numbers you can use to the `to_units` function to get everything 0-1
Each Color space type has a name value which can be used to identify the color space,
if this is more useful than checking the class type. Either can be used when converting
the color values between spaces.
A color object may be converted into a different space by using the
`color.to(other_space)` function, which will return a new color object in the requested
space.
There are three special cases.
1. ColorNamed is a type of ColorRGB which will preferentially print the name instead
of the hex value if one is available.
2. ColorNone is a special value which indicates the keyword `none` and does not
allow any values or alpha.
3. ColorCMS can not be converted to other color spaces and contains a `fallback_color`
to access the RGB fallback if it was provided.
"""
from .color import Color, ColorError, ColorIdError
from .utils import is_color
from .spaces import *

View File

@@ -0,0 +1,295 @@
# coding=utf-8
#
# Copyright (C) 2020 Martin Owens
# 2021 Jonathan Neuhauser
#
# 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.
#
"""
Basic color controls
"""
from typing import Dict, Optional, Tuple, Union
from .converters import Converters
Number = Union[int, float]
def round_by_type(kind, number):
"""Round a number to zero or five decimal places depending on it's type"""
return kind(round(number, kind == float and 5 or 0))
class ColorError(KeyError):
"""Specific color parsing error"""
class ColorIdError(ColorError):
"""Special color error for gradient and color stop ids"""
class Color(list):
"""A parsed color object which could be in many color spaces, the default is sRGB
Can be constructed from valid CSS color attributes, as well as
tuple/list + color space. Percentage values are supported.
"""
_spaces: Dict[str, type] = {}
name: Optional[str] = None
# A list of known channels
channels: Tuple[str, ...] = ()
# A list of scales for converting css color values to known qantities
scales: Tuple[
Union[Tuple[Number, Number, bool], Tuple[Number, Number]], ...
] = () # Min (int/float), Max (int/float), [wrap around (bool:False)]
# If alpha is not specified, this is the default for most color types.
default_alpha = 1.0
def __init_subclass__(cls):
if not cls.name:
return # It is a base class
# Add space to a dictionary of available color spaces
cls._spaces[cls.name] = cls
Converters.add_space(cls)
def __new__(cls, value=None, alpha=None, arg=None):
if not cls.name:
if value is None:
return super().__new__(cls._spaces["none"])
if isinstance(value, int):
return super().__new__(cls._spaces["rgb"])
if isinstance(value, str):
# String from xml or css attributes
for space in cls._spaces.values():
if space.can_parse(value.lower()):
return super().__new__(space, value)
if isinstance(value, Color):
return super().__new__(type(value), value)
if isinstance(value, (list, tuple)):
from ..deprecated.main import _deprecated
_deprecated(
"Anonymous lists of numbers for colors no longer default to rgb"
)
return super().__new__(cls._spaces["rgb"], value)
return super().__new__(cls, value, alpha=alpha, arg=arg)
def __init__(self, values, alpha=None, arg=None):
super().__init__()
if not self.name:
raise ColorError(f"Not a known color value: '{values}' {arg}")
if not isinstance(values, (list, tuple)):
raise ColorError(
f"Colors must be constructed with a list of values: '{values}'"
)
if alpha is not None and not isinstance(alpha, float):
raise ColorError("Color alpha property must be a float number")
if alpha is None and self.channels and len(values) == len(self.channels) + 1:
alpha = values.pop()
if isinstance(values, Color):
alpha = values.alpha
if self.channels and len(values) != len(self.channels):
raise ColorError(
f"You must have {len(self.channels)} channels for a {self.name} color"
)
self[:] = values
self.alpha = alpha
def __hash__(self):
"""Allow colors to be hashable"""
return tuple(self + [self.alpha, self.name]).__hash__()
def __str__(self):
raise NotImplementedError(
f"Color space {self.name} can not be printed to a string."
)
def __int__(self):
raise NotImplementedError(
f"Color space {self.name} can not be converted to a number."
)
def __getitem__(self, index):
"""Get the color value"""
space = self.name
if (
isinstance(index, slice)
and index.start is not None
and not isinstance(index.start, int)
):
# We support the format `value = color["space_name":index]` here
space = self._spaces[index.start]
index = int(index.stop)
# Allow regular slicing to fall through more freely than setitem
if space == self.name:
return super().__getitem__(index)
if not isinstance(index, int):
raise ColorError(f"Unknown color getter definition: '{index}'")
return self.to(space)[
index
] # Note: this calls Color.__getitem__ function again
def __setitem__(self, index, value):
"""Set the color value in place, limits setter to specific color space"""
space = self.name
if isinstance(index, slice):
# Support the format color[:] = [list of numbers] here
if index.start is None and index.stop is None:
super().__setitem__(
index, (self.constrain(ind, val) for ind, val in enumerate(value))
)
return
# We support the format `color["space_name":index] = value` here
space = self._spaces[index.start]
index = int(index.stop)
if not isinstance(index, int):
raise ColorError(f"Unknown color setter definition: '{index}'")
# Setting a channel in the existing space
if space == self.name:
super().__setitem__(index, self.constrain(index, value))
else:
# Set channel is another space, convert back and forth
values = self.to(space)
values[index] = value # Note: this calls Color.__setitem__ function again
self[:] = values.to(self.name)
def to(self, space): # pylint: disable=invalid-name
"""Get this color but in a specific color space"""
if space in self._spaces.values():
space = space.name
if space not in self._spaces:
raise AttributeError(
f"Unknown color space {space} when converting from {self.name}"
)
if not hasattr(type(self), f"to_{space}"):
setattr(
type(self),
f"to_{space}",
Converters.find_converter(type(self), self._spaces[space]),
)
return getattr(self, f"to_{space}")()
def __getattr__(self, name):
if name.startswith("to_") and name.count("_") == 1:
return lambda: self.to(name.split("_")[-1])
raise AttributeError(f"Can not find attribute {type(self).__name__}.{name}")
@property
def effective_alpha(self):
"""Get the alpha as set, or tell me what it would be by default"""
if self.alpha is None:
return self.default_alpha
return self.alpha
def get_values(self, alpha=True):
"""Returns all values, including alpha as a list"""
if alpha:
return list(self + [self.effective_alpha])
return list(self)
@classmethod
def to_units(cls, *values):
"""Convert the color values into floats scales from 0.0 to 1.0"""
return [cls.scale_down(ind, val) for ind, val in enumerate(values)]
@classmethod
def from_units(cls, *values):
"""Convert float values to the scales expected and return a new instance"""
return [cls.scale_up(ind, val) for ind, val in enumerate(values)]
@classmethod
def can_parse(cls, string): # pylint: disable=unused-argument
"""Returns true if this string can be parsed for this color type"""
return False
@classmethod
def scale_up(cls, index, value):
"""Convert from float 0.0 to 1.0 to an int used in css"""
(min_value, max_value) = cls.scales[index][:2]
return cls.constrain(
index, (value * (max_value - min_value)) + min_value
) # See inkscape/src/colors/spaces/base.h:SCALE_UP
@classmethod
def scale_down(cls, index, value):
"""Convert from int, often 0 to 255 to a float 0.0 to 1.0"""
(min_value, max_value) = cls.scales[index][:2]
return (cls.constrain(index, value) - min_value) / (
max_value - min_value
) # See inkscape/src/colors/spaces/base.h:SCALE_DOWN
@classmethod
def constrain(cls, index, value):
"""Constrains the value to the css scale"""
scale = cls.scales[index]
if len(scale) == 3 and scale[2] is True:
if value == scale[1]:
return value
return round_by_type(
type(scale[0]), value % scale[1]
) # Wrap around value (i.e. hue)
return min(max(round_by_type(type(scale[0]), value), scale[0]), scale[1])
def interpolate(self, other, fraction):
"""Interpolate two colours by the given fraction
.. versionadded:: 1.1"""
from ..tween import ColorInterpolator # pylint: disable=import-outside-toplevel
try:
other = other.to(type(self))
except ColorError:
raise ColorError("Can not convert color in interpolation.")
return ColorInterpolator(self, other).interpolate(fraction)
class AlphaNotAllowed:
"""Mixin class to indicate that alpha values are not permitted on this color space"""
alpha = property(
lambda self: None,
lambda self, value: None,
)
def get_values(self, alpha=False):
return super().get_values(False)

View File

@@ -0,0 +1,122 @@
# coding=utf-8
#
# Copyright (C) 2018-2024 Martin Owens
#
# 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.
#
"""
Basic color errors and common functions
"""
from collections import defaultdict
from typing import Dict, List, Callable
ConverterFunc = Callable[[float], List[float]]
class Converters:
"""
Record how colors can be converted between different spaces and provides
a way to path-find between multiple step conversions.
"""
links: Dict[str, Dict[str, ConverterFunc]] = defaultdict(dict)
chains: Dict[str, List[List[str]]] = {}
@classmethod
def add_space(cls, color_cls):
"""
Records the stated links between this class and other color spaces
"""
for name, func in color_cls.__dict__.items():
if not name.startswith("convert_"):
continue
_, direction, space = name.split("_", 2)
from_name = color_cls.name if direction == "to" else space
to_name = color_cls.name if direction == "from" else space
if from_name != to_name:
if not isinstance(func, staticmethod):
raise TypeError(f"Method '{name}' must be a static method.")
cls.links[from_name][to_name] = func.__func__
@classmethod
def get_chain(cls, source, target):
"""
Get a chain of conversions between two color spaces, if possible.
"""
def build_chains(chains, space):
new_chains = []
for chain in chains:
for hop in cls.links[space]:
if hop not in chain:
new_chains += build_chains([chain + [hop]], hop)
return chains + new_chains
if source not in cls.chains:
cls.chains[source] = build_chains([[source]], source)
chosen = None
for chain in cls.chains[source] or ():
if chain[-1] == target and (not chosen or len(chain) < len(chosen)):
chosen = chain
return chosen
@classmethod
def find_converter(cls, source, target):
"""
Find a way to convert from source to target using any conversion functions.
Will hop from one space to another if needed.
"""
func = None
# Passthough
if source == target:
return lambda self: self
if func is None:
chain = cls.get_chain(source.name, target.name)
if chain:
return cls.generate_converter(chain, source, target)
# Returning a function means we only run this function once, even when not found
def _error(self):
raise NotImplementedError(
f"Color space {source} can not be converted to {target}."
)
return _error
@classmethod
def generate_converter(cls, chain, source_cls, target_cls):
"""
Put together a function that can do every step of the chain of conversions
"""
# Build a list of functions to run
funcs = [cls.links[a][b] for a, b in zip(chain, chain[1:])]
funcs.insert(0, source_cls.to_units)
funcs.append(target_cls.from_units)
def _inner(values):
if hasattr(values, "alpha") and values.alpha is not None:
values = list(values) + [values.alpha]
for func in funcs:
values = func(*values)
return target_cls(values)
return _inner

View File

@@ -0,0 +1,11 @@
"""
Each color space that this module supports such have one file in this module.
"""
from .cmyk import ColorDeviceCMYK
from .cms import ColorCMS
from .hsl import ColorHSL
from .hsv import ColorHSV
from .named import ColorNamed
from .none import ColorNone
from .rgb import ColorRGB

View File

@@ -0,0 +1,95 @@
# coding=utf-8
#
# Copyright (C) 2024 Martin Owens
#
# 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.
#
"""
SVG icc-color parser
"""
from ..color import Color, AlphaNotAllowed, ColorError, round_by_type
from .css import CssColor
from .rgb import ColorRGB
class ColorCMS(CssColor, AlphaNotAllowed):
"""
Parse and print SVG icc-color objects into their values and the fallback RGB
"""
name = "cms"
css_func = "icc-color"
channels = ()
scales = ()
def __init__(self, values, icc_profile=None, fallback=None):
if isinstance(values, str):
if values.strip().startswith("#") and " " in values:
fallback, values = values.split(" ", 1)
fallback = Color(fallback)
icc_profile, values = self.parse_css_color(values)
if icc_profile is None:
raise ColorError("CMS Color requires an icc color profile name.")
self.icc_profile = icc_profile
self.fallback_rgb = fallback
super().__init__(values)
def __str__(self) -> str:
values = self.css_join.join([f"{v:g}" for v in self.get_css_values()])
fallback = str(ColorRGB(self.fallback_rgb)) + " " if self.fallback_rgb else ""
return f"{fallback}{self.css_func}({self.icc_profile}, {values})"
@classmethod
def can_parse(cls, string: str) -> bool:
# Custom detection because of RGB fallback prefix
return "icc-color" in string.replace("(", " ").split()
@classmethod
def constrain(cls, index, value):
return min(max(round_by_type(float, value), 0.0), 1.0)
@classmethod
def scale_up(cls, index, value):
return value # All cms values are already 0.0 to 1.0
@classmethod
def scale_down(cls, index, value):
return value # All cms values are already 0.0 to 1.0
@staticmethod
def convert_to_rgb(*data):
"""Catch attempted conversions to rgb"""
raise NotImplementedError("Can not convert to RGB from icc color")
@staticmethod
def convert_from_rgb(*data):
"""Catch attempted conversions from rgb"""
raise NotImplementedError("Can not convert from RGB to icc color")
# This is research code for a future developer to use. We already use PIL and this will
# allow icc colors to be converted in python. This isn't needed right now, so this work
# will be left undone.
# @staticmethod
# def convert_to_rgb():
# from PIL import Image, ImageCms
# pixel = Image.fromarray([[int(r * 255), int(g * 255), int(b * 255)]], 'RGB')
# transform = ImageCms.buildTransform(sRGB_profile, self.this_profile, "RGB",
# self.this_profile_mode, self.this_rendering_intent, 0)
# transform.apply_in_place(pixel)
# return [p / 255 for p in pixel[0]]

View File

@@ -0,0 +1,81 @@
# coding=utf-8
#
# Copyright (C) 2024 Martin Owens
#
# 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.
#
# pylint: disable=W0223
"""
DeviceCMYK Color Space
"""
from .css import CssColorModule4
class ColorDeviceCMYK(CssColorModule4):
"""
Parse the device-cmyk CSS Color Module 4 format.
Note that this format is NOT true CMYK as you might expect in a printer and
is instead is an aproximation of the intended ink levels if this was converted
into a real CMYK color profile using a color management system.
"""
name = "cmyk"
channels = ("cyan", "magenta", "yellow", "black")
scales = ((0, 100), (0, 100), (0, 100), (0, 100), (0.0, 1.0))
css_either_prefix = "device-cmyk"
cyan = property(
lambda self: self[0], lambda self, value: self.__setitem__(0, value)
)
magenta = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
yellow = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
black = property(
lambda self: self[3], lambda self, value: self.__setitem__(3, value)
)
@staticmethod
def convert_to_rgb(cyan, magenta, yellow, black, *alpha):
"""
Convert a set of Device-CMYK identities into RGB
"""
white = 1.0 - black
return [
1.0 - min((1.0, cyan * white + black)),
1.0 - min((1.0, magenta * white + black)),
1.0 - min((1.0, yellow * white + black)),
] + list(alpha)
@staticmethod
def convert_from_rgb(red, green, blue, *alpha):
"""
Convert RGB into Device-CMYK
"""
white = max((red, green, blue))
black = 1.0 - white
return [
# Each channel is it's color chart oposite (cyan->red)
# with a bit of white removed.
(white and (1.0 - red - black) / white or 0.0),
(white and (1.0 - green - black) / white or 0.0),
(white and (1.0 - blue - black) / white or 0.0),
black,
] + list(alpha)

View File

@@ -0,0 +1,139 @@
# coding=utf-8
#
# Copyright (C) 2018-2024 Martin Owens
#
# 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.
#
# pylint: disable=W0223
"""
Parsing CSS elements from colors
"""
from typing import Optional, Union
from ..color import Color, ColorError, ColorIdError
class CssColor(Color):
"""
A Color which is always parsed and printed from a css format.
"""
# A list of css prefixes which ar valid for this space
css_noalpha_prefix: Optional[str] = None
css_alpha_prefix: Optional[str] = None
css_either_prefix: Optional[str] = None
# Some CSS formats require commas, others do not
css_join: str = ", "
css_join_alpha: str = ", "
css_func = "color"
def __str__(self):
values = self.css_join.join([f"{v:g}" for v in self.get_css_values()])
prefix = self.css_noalpha_prefix or self.css_either_prefix
if self.alpha is not None:
# Alpha is stored as a percent for clarity
alpha = int(self.alpha * 100)
values += self.css_join_alpha + f"{alpha}%"
if not self.css_either_prefix:
prefix = self.css_alpha_prefix
if prefix is None:
raise ColorError(f"Can't encode color {self.name} into CSS color format.")
return f"{prefix}({values})"
@classmethod
def can_parse(cls, string: str):
string = string.replace(" ", "")
if "(" not in string or ")" not in string:
return False
for prefix in (
cls.css_noalpha_prefix,
cls.css_alpha_prefix,
cls.css_either_prefix,
):
if prefix and (prefix + "(" in string or "color(" + prefix in string):
return True
return False
def __init__(self, value, alpha=None):
if isinstance(value, str):
prefix, values = self.parse_css_color(value)
has_alpha = (
self.channels is not None and len(values) == len(self.channels) + 1
)
if prefix == self.css_noalpha_prefix or (
prefix == self.css_either_prefix and not has_alpha
):
super().__init__(values)
elif prefix == self.css_alpha_prefix or (
prefix == self.css_either_prefix and has_alpha
):
super().__init__(values, values.pop())
else:
raise ColorError(f"Could not parse {self.name} css color: '{value}'")
else:
super().__init__(value, alpha=alpha)
@classmethod
def parse_css_color(cls, value):
"""Parse a css string into a list of values and it's color space prefix"""
prefix, values = value.lower().strip().strip(")").split("(")
# Some css formats use commas, others do not
if "," in cls.css_join:
values = values.replace(",", " ")
if "/" in cls.css_join_alpha:
values = values.replace("/", " ")
# Split values by spaces
values = values.split()
prefix = prefix.strip()
if prefix == cls.css_func:
prefix = values.pop(0)
if prefix == "url":
raise ColorIdError("Can not parse url as if it was a color.")
return prefix, [cls.parse_css_value(i, v) for i, v in enumerate(values)]
def get_css_values(self):
"""Return a list of values used for css string output"""
return self
@classmethod
def parse_css_value(cls, index, value) -> Union[int, float]:
"""Parse a CSS value such as 100%, 360 or 0.4"""
if cls.scales and index >= len(cls.scales):
raise ValueError("Can't add any more values to color.")
if isinstance(value, str):
value = value.strip()
if value.endswith("%"):
value = float(value.strip("%")) / 100
elif "." in value:
value = float(value)
else:
value = int(value)
if isinstance(value, float) and value <= 1.0:
value = cls.scale_up(index, value)
return cls.constrain(index, value)
class CssColorModule4(CssColor):
"""Tweak the css parser for CSS Module Four formating"""
css_join = " "
css_join_alpha = " / "

View File

@@ -0,0 +1,107 @@
# coding=utf-8
#
# Copyright (C) 2024 Martin Owens
#
# 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.
#
# pylint: disable=W0223
"""
HSL Color Space
"""
from .css import CssColor
class ColorHSL(CssColor):
"""
Parse the HSL CSS Module Module 3 format.
"""
name = "hsl"
channels = ("hue", "saturation", "lightness")
scales = ((0, 360, True), (0, 100), (0, 100), (0.0, 1.0))
css_noalpha_prefix = "hsl"
css_alpha_prefix = "hsla"
hue = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
saturation = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
lightness = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
@staticmethod
def convert_from_rgb(red, green, blue, alpha=None):
"""RGB to HSL colour conversion"""
rgb_max = max(red, green, blue)
rgb_min = min(red, green, blue)
delta = rgb_max - rgb_min
hsl = [0.0, 0.0, (rgb_max + rgb_min) / 2.0]
if delta != 0:
if hsl[2] <= 0.5:
hsl[1] = delta / (rgb_max + rgb_min)
else:
hsl[1] = delta / (2 - rgb_max - rgb_min)
if red == rgb_max:
hsl[0] = (green - blue) / delta
elif green == rgb_max:
hsl[0] = 2.0 + (blue - red) / delta
elif blue == rgb_max:
hsl[0] = 4.0 + (red - green) / delta
hsl[0] /= 6.0
if hsl[0] < 0:
hsl[0] += 1
if hsl[0] > 1:
hsl[0] -= 1
if alpha is not None:
hsl.append(alpha)
return hsl
@staticmethod
def convert_to_rgb(hue, sat, light, *alpha):
"""HSL to RGB Color Conversion"""
if sat == 0:
return [light, light, light] # Gray
if light < 0.5:
val2 = light * (1 + sat)
else:
val2 = light + sat - light * sat
val1 = 2 * light - val2
ret = [
_hue_to_rgb(val1, val2, hue * 6 + 2.0),
_hue_to_rgb(val1, val2, hue * 6),
_hue_to_rgb(val1, val2, hue * 6 - 2.0),
]
return ret + list(alpha)
def _hue_to_rgb(val1, val2, hue):
if hue < 0:
hue += 6.0
if hue > 6:
hue -= 6.0
if hue < 1:
return val1 + (val2 - val1) * hue
if hue < 3:
return val2
if hue < 4:
return val1 + (val2 - val1) * (4 - hue)
return val1

View File

@@ -0,0 +1,88 @@
# coding=utf-8
#
# Copyright (C) 2024 Jonathan Neuhauser
# 2024 Martin Owens
#
# 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.
#
# pylint: disable=W0223
"""
HSV Color Space
"""
from .css import CssColorModule4
class ColorHSV(CssColorModule4):
"""
Parse the HWB CSS Color Module 4 format and retain as HSV values.
"""
name = "hsv"
channels = ("hue", "saturation", "value")
scales = ((0, 360, True), (0, 100), (0, 100), (0.0, 1.0))
# We use HWB to store HSV as this makes the most sense to Inkscape
css_either_prefix = "hwb"
hue = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
saturation = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
value = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
@classmethod
def parse_css_color(cls, value):
"""Parsing HWB as if it was HSV for css input"""
prefix, values = super().parse_css_color(value)
# See https://en.wikipedia.org/wiki/HWB_color_model#Converting_to_and_from_HSV
values[1] /= 100
values[2] /= 100
scale = values[1] + values[2]
if scale > 1.0:
values[1] /= scale
values[2] /= scale
values[1] = int(
(values[2] == 1.0 and 0.0 or (1.0 - (values[1] / (1.0 - values[2])))) * 100
)
values[2] = int((1.0 - values[2]) * 100)
return prefix, values
def get_css_values(self):
"""Convert our HSV values into HWB for css output"""
values = list(self)
values[1] = (100 - values[1]) * (values[2] / 100)
values[2] = 100 - values[2]
return values
@staticmethod
def convert_to_hsl(hue, saturation, value, *alpha):
"""Conversion according to
https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
.. versionadded:: 1.5"""
lum = value * (1 - saturation / 2)
sat = 0 if lum in (0, 1) else (value - lum) / min(lum, 1 - lum)
return [hue, sat, lum] + list(alpha)
@staticmethod
def convert_from_hsl(hue, saturation, lightness, *alpha):
"""Convertion according to Inkscape C++ codebase
.. versionadded:: 1.5"""
val = lightness + saturation * min(lightness, 1 - lightness)
sat = 0 if val == 0 else 2 * (1 - lightness / val)
return [hue, sat, val] + list(alpha)

View File

@@ -0,0 +1,236 @@
# coding=utf-8
#
# Copyright (C) 2024, Martin Owens
#
# 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.
#
"""
CSS Named colors
"""
from typing import Dict
from ..color import Color
from .rgb import ColorRGB
_COLORS = {
"aliceblue": "#f0f8ff",
"antiquewhite": "#faebd7",
"aqua": "#00ffff",
"aquamarine": "#7fffd4",
"azure": "#f0ffff",
"beige": "#f5f5dc",
"bisque": "#ffe4c4",
"black": "#000000",
"blanchedalmond": "#ffebcd",
"blue": "#0000ff",
"blueviolet": "#8a2be2",
"brown": "#a52a2a",
"burlywood": "#deb887",
"cadetblue": "#5f9ea0",
"chartreuse": "#7fff00",
"chocolate": "#d2691e",
"coral": "#ff7f50",
"cornflowerblue": "#6495ed",
"cornsilk": "#fff8dc",
"crimson": "#dc143c",
"cyan": "#00ffff",
"darkblue": "#00008b",
"darkcyan": "#008b8b",
"darkgoldenrod": "#b8860b",
"darkgray": "#a9a9a9",
"darkgreen": "#006400",
"darkgrey": "#a9a9a9",
"darkkhaki": "#bdb76b",
"darkmagenta": "#8b008b",
"darkolivegreen": "#556b2f",
"darkorange": "#ff8c00",
"darkorchid": "#9932cc",
"darkred": "#8b0000",
"darksalmon": "#e9967a",
"darkseagreen": "#8fbc8f",
"darkslateblue": "#483d8b",
"darkslategray": "#2f4f4f",
"darkslategrey": "#2f4f4f",
"darkturquoise": "#00ced1",
"darkviolet": "#9400d3",
"deeppink": "#ff1493",
"deepskyblue": "#00bfff",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1e90ff",
"firebrick": "#b22222",
"floralwhite": "#fffaf0",
"forestgreen": "#228b22",
"fuchsia": "#ff00ff",
"gainsboro": "#dcdcdc",
"ghostwhite": "#f8f8ff",
"gold": "#ffd700",
"goldenrod": "#daa520",
"gray": "#808080",
"grey": "#808080",
"green": "#008000",
"greenyellow": "#adff2f",
"honeydew": "#f0fff0",
"hotpink": "#ff69b4",
"indianred": "#cd5c5c",
"indigo": "#4b0082",
"ivory": "#fffff0",
"khaki": "#f0e68c",
"lavender": "#e6e6fa",
"lavenderblush": "#fff0f5",
"lawngreen": "#7cfc00",
"lemonchiffon": "#fffacd",
"lightblue": "#add8e6",
"lightcoral": "#f08080",
"lightcyan": "#e0ffff",
"lightgoldenrodyellow": "#fafad2",
"lightgray": "#d3d3d3",
"lightgreen": "#90ee90",
"lightgrey": "#d3d3d3",
"lightpink": "#ffb6c1",
"lightsalmon": "#ffa07a",
"lightseagreen": "#20b2aa",
"lightskyblue": "#87cefa",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#b0c4de",
"lightyellow": "#ffffe0",
"lime": "#00ff00",
"limegreen": "#32cd32",
"linen": "#faf0e6",
"magenta": "#ff00ff",
"maroon": "#800000",
"mediumaquamarine": "#66cdaa",
"mediumblue": "#0000cd",
"mediumorchid": "#ba55d3",
"mediumpurple": "#9370db",
"mediumseagreen": "#3cb371",
"mediumslateblue": "#7b68ee",
"mediumspringgreen": "#00fa9a",
"mediumturquoise": "#48d1cc",
"mediumvioletred": "#c71585",
"midnightblue": "#191970",
"mintcream": "#f5fffa",
"mistyrose": "#ffe4e1",
"moccasin": "#ffe4b5",
"navajowhite": "#ffdead",
"navy": "#000080",
"oldlace": "#fdf5e6",
"olive": "#808000",
"olivedrab": "#6b8e23",
"orange": "#ffa500",
"orangered": "#ff4500",
"orchid": "#da70d6",
"palegoldenrod": "#eee8aa",
"palegreen": "#98fb98",
"paleturquoise": "#afeeee",
"palevioletred": "#db7093",
"papayawhip": "#ffefd5",
"peachpuff": "#ffdab9",
"peru": "#cd853f",
"pink": "#ffc0cb",
"plum": "#dda0dd",
"powderblue": "#b0e0e6",
"purple": "#800080",
"rebeccapurple": "#663399",
"red": "#ff0000",
"rosybrown": "#bc8f8f",
"royalblue": "#4169e1",
"saddlebrown": "#8b4513",
"salmon": "#fa8072",
"sandybrown": "#f4a460",
"seagreen": "#2e8b57",
"seashell": "#fff5ee",
"sienna": "#a0522d",
"silver": "#c0c0c0",
"skyblue": "#87ceeb",
"slateblue": "#6a5acd",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#fffafa",
"springgreen": "#00ff7f",
"steelblue": "#4682b4",
"tan": "#d2b48c",
"teal": "#008080",
"thistle": "#d8bfd8",
"tomato": "#ff6347",
"turquoise": "#40e0d0",
"violet": "#ee82ee",
"wheat": "#f5deb3",
"white": "#ffffff",
"whitesmoke": "#f5f5f5",
"yellow": "#ffff00",
"yellowgreen": "#9acd32",
}
class ColorNamed(ColorRGB):
"""
Parse specific named colors, fall back to RGB parsing if it fails.
"""
_color_names: Dict[ColorRGB, str] = {}
_name_colors: Dict[str, ColorRGB] = {}
name = "named"
def __init__(self, name, alpha=None):
if isinstance(name, str):
super().__init__(self.name_colors()[name.lower().strip()])
else:
super().__init__(name, alpha=alpha)
@classmethod
def color_names(cls):
"""Cache a list of color names"""
if not cls._color_names:
cls._color_names = {
value: name for name, value in cls.name_colors().items()
}
return cls._color_names
@classmethod
def name_colors(cls):
"""Cache a list of color objects"""
if not cls._name_colors:
cls._name_colors = {name: Color(value) for name, value in _COLORS.items()}
return cls._name_colors
def __str__(self):
return self.color_names().get(self, super().__str__())
def __hash__(self):
"""Allow named colors to match rgb colors"""
return tuple(self + [self.alpha, super().name]).__hash__()
@classmethod
def can_parse(cls, string: str):
"""If the string is one of the color names, we can parse it"""
return string in cls.name_colors()
@staticmethod
def convert_to_rgb(*data):
"""Converting to RGB is transparent, already in RGB"""
return data
@staticmethod
def convert_from_rgb(*data):
"""Converting from RGB is transparent, the store is RGB"""
return data
def to_rgb(self):
"""Prevent masking by ColorRGB of to_rgb method"""
return ColorRGB(list(self), alpha=self.alpha)

View File

@@ -0,0 +1,55 @@
# coding=utf-8
#
# Copyright (C) 2021 Jonathan Neuhauser
# 2020 Martin Owens
#
# 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.
#
# pylint: disable=W0223
"""
An empty color for 'none'
"""
from ..color import Color, AlphaNotAllowed
class ColorNone(Color, AlphaNotAllowed):
"""A special color for 'none' colors"""
name = "none"
# Override opacity since none can not have opacity
default_alpha = 0.0
def __init__(self, value=None):
pass
def __str__(self) -> str:
return "none"
@classmethod
def can_parse(cls, string: str) -> bool:
"""Returns true if this is the word 'none'"""
return string == "none"
@staticmethod
def convert_to_rgb(*_):
"""Converting to RGB means transparent black"""
return [0, 0, 0, 0]
@staticmethod
def convert_from_rgb(*_):
"""Converting from RGB means throwing out all data"""
return []

View File

@@ -0,0 +1,105 @@
# coding=utf-8
#
# Copyright (C) 2024, Martin Owens
#
# 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.
#
"""
RGB Colors
"""
from ..color import ColorError
from .css import CssColor
class ColorRGB(CssColor):
"""
Parse multiple versions of RGB from CSS module and standard hex formats.
"""
name = "rgb"
channels = ("red", "green", "blue")
scales = ((0, 255), (0, 255), (0, 255), (0.0, 1.0))
css_noalpha_prefix = "rgb"
css_alpha_prefix = "rgba"
red = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
green = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
blue = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
@classmethod
def can_parse(cls, string: str) -> bool:
return "icc" not in string and (
string.startswith("#")
or string.lstrip("-").isdigit()
or super().can_parse(string)
)
def __init__(self, value, alpha=None):
# Not CSS, but inkscape, some old color values stores as 32bit int strings
if isinstance(value, str) and value.lstrip("-").isdigit():
value = int(value)
if isinstance(value, int):
super().__init__(
[
((value >> 24) & 255), # red
((value >> 16) & 255), # green
((value >> 8) & 255), # blue
((value & 255) / 255.0),
]
) # opacity
elif isinstance(value, str) and value.startswith("#") and " " not in value:
if len(value) == 4: # (css: #rgb -> #rrggbb)
# pylint: disable=consider-using-f-string
value = "#{1}{1}{2}{2}{3}{3}".format(*value)
elif len(value) == 5: # (css: #rgba -> #rrggbbaa)
# pylint: disable=consider-using-f-string
value = "#{1}{1}{2}{2}{3}{3}{4}{4}".format(*value)
# Convert hex to integers
try:
values = [int(value[i : i + 2], 16) for i in range(1, len(value), 2)]
if len(values) == 4:
values[3] /= 255
super().__init__(values)
except ValueError as error:
raise ColorError(f"Bad RGB hex color value '{value}'") from error
else:
super().__init__(value, alpha=alpha)
def __str__(self) -> str:
if self.alpha is not None:
return super().__str__()
if len(self) < len(self.channels):
raise ColorError(
f"Incorrect number of channels for Color Space {self.name}"
)
# Always hex values when outputting color
return "#{0:02x}{1:02x}{2:02x}".format(*(int(v) for v in self)) # pylint: disable=consider-using-f-string
def __int__(self) -> int:
return (
(self[0] << 24)
+ (self[1] << 16)
+ (self[2] << 8)
+ int((self.alpha or 1.0) * 255)
)

View File

@@ -0,0 +1,31 @@
# coding=utf-8
#
# Copyright (C) 2018-2024 Martin Owens
#
# 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.
#
"""
Utilities for color support
"""
from .color import Color, ColorError
def is_color(color):
"""Determine if it is a color that we can use. If not, leave it unchanged."""
try:
return bool(Color(color))
except ColorError:
return False

View File

@@ -0,0 +1,347 @@
# coding=utf-8
#
# Copyright (C) 2019 Martin Owens
#
# 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.
#
"""
This API provides methods for calling Inkscape to execute a given
Inkscape command. This may be needed for various compiling options
(e.g., png), running other extensions or performing other options only
available via the shell API.
Best practice is to avoid using this API except when absolutely necessary,
since it is resource-intensive to invoke a new Inkscape instance.
However, in any circumstance when it is necessary to call Inkscape, it
is strongly recommended that you do so through this API, rather than calling
it yourself, to take advantage of the security settings and testing functions.
"""
import os
import re
import sys
from shutil import which as warlock
from subprocess import Popen, PIPE
from tempfile import TemporaryDirectory
from typing import List
from lxml.etree import ElementTree
from .elements import SvgDocumentElement
INKSCAPE_EXECUTABLE_NAME = os.environ.get("INKSCAPE_COMMAND")
if INKSCAPE_EXECUTABLE_NAME is None:
if sys.platform == "win32":
# prefer inkscape.exe over inkscape.com which spawns a command window
INKSCAPE_EXECUTABLE_NAME = "inkscape.exe"
else:
INKSCAPE_EXECUTABLE_NAME = "inkscape"
class CommandNotFound(IOError):
"""Command is not found"""
class ProgramRunError(ValueError):
"""A specialized ValueError that is raised when a call to an external command fails.
It stores additional information about a failed call to an external program.
If only the ``program`` parameter is given, it is interpreted as the error message.
Otherwise, the error message is compiled from all constructor parameters."""
program: str
"""The absolute path to the called executable"""
returncode: int
"""Return code of the program call"""
stderr: str
"""stderr stream output of the call"""
stdout: str
"""stdout stream output of the call"""
arguments: List
"""Arguments of the call"""
def __init__(self, program, returncode=None, stderr=None, stdout=None, args=None):
self.program = program
self.returncode = returncode
self.stderr = stderr
self.stdout = stdout
self.arguments = args
super().__init__(str(self))
def __str__(self):
if self.returncode is None:
return self.program
return (
f"Return Code: {self.returncode}: {self.stderr}\n{self.stdout}"
f"\nargs: {self.args}"
)
def which(program):
"""
Attempt different methods of trying to find if the program exists.
"""
if os.path.isabs(program) and os.path.isfile(program):
return program
# On Windows, shutil.which may give preference to .py files in the current directory
# (such as pdflatex.py), e.g. if .PY is in pathext, because the current directory is
# prepended to PATH. This can be suppressed by explicitly appending the current
# directory.
try:
if sys.platform == "win32":
prog = warlock(program, path=os.environ["PATH"] + ";" + os.curdir)
if prog:
return prog
except ImportError:
pass
try:
# Python3 only version of which
prog = warlock(program)
if prog:
return prog
except ImportError:
pass # python2
# There may be other methods for doing a `which` command for other
# operating systems; These should go here as they are discovered.
raise CommandNotFound(f"Can not find the command: '{program}'")
def write_svg(svg, *filename):
"""Writes an svg to the given filename"""
filename = os.path.join(*filename)
if os.path.isfile(filename):
return filename
with open(filename, "wb") as fhl:
if isinstance(svg, SvgDocumentElement):
svg = ElementTree(svg)
if hasattr(svg, "write"):
# XML document
svg.write(fhl)
elif isinstance(svg, bytes):
fhl.write(svg)
else:
raise ValueError("Not sure what type of SVG data this is.")
return filename
def to_arg(arg, oldie=False):
"""Convert a python argument to a command line argument"""
if isinstance(arg, (tuple, list)):
(arg, val) = arg
arg = "-" + arg
if len(arg) > 2 and not oldie:
arg = "-" + arg
if val is True:
return arg
if val is False:
return None
return f"{arg}={str(val)}"
return str(arg)
def to_args(prog, *positionals, **arguments):
"""Compile arguments and keyword arguments into a list of strings which Popen will
understand.
:param prog:
Program executable prepended to the output.
:type first: ``str``
:Arguments:
* (``str``) -- String added as given
* (``tuple``) -- Ordered version of Keyword Arguments, see below
:Keyword Arguments:
* *name* (``str``) --
Becomes ``--name="val"``
* *name* (``bool``) --
Becomes ``--name``
* *name* (``list``) --
Becomes ``--name="val1"`` ...
* *n* (``str``) --
Becomes ``-n=val``
* *n* (``bool``) --
Becomes ``-n``
:return: Returns a list of compiled arguments ready for Popen.
:rtype: ``list[str]``
"""
args = [prog]
oldie = arguments.pop("oldie", False)
for arg, value in arguments.items():
arg = arg.replace("_", "-").strip()
if isinstance(value, tuple):
value = list(value)
elif not isinstance(value, list):
value = [value]
for val in value:
args.append(to_arg((arg, val), oldie))
args += [to_arg(pos, oldie) for pos in positionals if pos is not None]
# Filter out empty non-arguments
return [arg for arg in args if arg is not None]
def to_args_sorted(prog, *positionals, **arguments):
"""same as :func:`to_args`, but keyword arguments are sorted beforehand
.. versionadded:: 1.2"""
return to_args(prog, *positionals, **dict(sorted(arguments.items())))
def _call(program, *args, **kwargs):
stdin = kwargs.pop("stdin", None)
if isinstance(stdin, str):
stdin = stdin.encode("utf-8")
inpipe = PIPE if stdin else None
args = to_args(which(program), *args, **kwargs)
kwargs = {}
if sys.platform == "win32":
kwargs["creationflags"] = 0x08000000 # create no console window
with Popen(
args,
shell=False, # Never have shell=True
stdin=inpipe, # StdIn not used (yet)
stdout=PIPE, # Grab any output (return it)
stderr=PIPE, # Take all errors, just incase
**kwargs,
) as process:
(stdout, stderr) = process.communicate(input=stdin)
if process.returncode == 0:
return stdout
raise ProgramRunError(program, process.returncode, stderr, stdout, args)
def call(program, *args, **kwargs):
"""
Generic caller to open any program and return its stdout::
stdout = call('executable', arg1, arg2, dash_dash_arg='foo', d=True, ...)
Will raise :class:`ProgramRunError` if return code is not 0.
Keyword arguments:
return_binary: Should stdout return raw bytes (default: False)
.. versionadded:: 1.1
stdin: The string or bytes containing the stdin (default: None)
All other arguments converted using :func:`to_args` function.
"""
# We use this long input because it's less likely to conflict with --binary=
binary = kwargs.pop("return_binary", False)
stdout = _call(program, *args, **kwargs)
# Convert binary to string when we wish to have strings we do this here
# so the mock tests will also run the conversion (always returns bytes)
if not binary and isinstance(stdout, bytes):
return stdout.decode(sys.stdout.encoding or "utf-8")
return stdout
def inkscape(svg_file, *args, **kwargs):
"""
Call Inkscape with the given svg_file and the given arguments, see call().
Returns the stdout of the call.
.. versionchanged:: 1.3
If the "actions" kwargs parameter is passed, it is checked whether the length of
the action string might lead to issues with the Windows CLI call character
limit. In this case, Inkscape is called in `--shell`
mode and the actions are fed in via stdin. This avoids violating the character
limit for command line arguments on Windows, which results in errors like this:
`[WinError 206] The filename or extension is too long`.
This workaround is also possible when calling Inkscape with long arguments
to `--export-id` and `--query-id`, by converting the call to the appropriate
action sequence. The stdout is cleaned to resemble non-interactive mode.
"""
os.environ["SELF_CALL"] = "true"
actions = kwargs.get("actions", None)
strip_stdout = False
# Keep some safe margin to the 8191 character limit.
if actions is not None and len(actions) > 7000:
args = args + ("--shell",)
kwargs["stdin"] = actions
kwargs.pop("actions")
strip_stdout = True
stdout = call(INKSCAPE_EXECUTABLE_NAME, svg_file, *args, **kwargs)
if strip_stdout:
split = re.split(r"\n> ", stdout)
if len(split) > 1:
if "\n" in split[1]:
stdout = "\n".join(split[1].split("\n")[1:])
else:
stdout = ""
return stdout
def inkscape_command(svg, select=None, actions=None, *args, **kwargs):
"""
Executes Inkscape batch actions with the given <svg> input and returns a new <svg>.
inkscape_command('<svg...>', [select=...], [actions=...], [...])
"""
with TemporaryDirectory(prefix="inkscape-command") as tmpdir:
svg_file = write_svg(svg, tmpdir, "input.svg")
select = ("select", select) if select else None
inkscape(
svg_file,
select,
batch_process=True,
export_overwrite=True,
actions=actions,
*args,
**kwargs,
)
with open(svg_file, "rb") as fhl:
return fhl.read()
def take_snapshot(svg, dirname, name="snapshot", ext="png", dpi=96, **kwargs):
"""
Take a snapshot of the given svg file.
Resulting filename is yielded back, after generator finishes, the
file is deleted so you must deal with the file inside the for loop.
"""
svg_file = write_svg(svg, dirname, name + ".svg")
ext_file = os.path.join(dirname, name + "." + str(ext).lower())
inkscape(
svg_file, export_dpi=dpi, export_filename=ext_file, export_type=ext, **kwargs
)
return ext_file
def is_inkscape_available():
"""Return true if the Inkscape executable is available."""
try:
return bool(which(INKSCAPE_EXECUTABLE_NAME))
except CommandNotFound:
return False

View File

@@ -0,0 +1,3 @@
"""CSS Processing module"""
from .compiler import CSSCompiler

View File

@@ -0,0 +1,483 @@
# coding=utf-8
#
# Copyright (C) 2023 - 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-1301, USA.
#
"""CSS evaluation logic, forked from cssselect2 (rewritten without eval, targeted to
our data structure). CSS selectors are compiled into boolean evaluator functions.
All HTML-specific code has been removed, and we don't duplicate the tree data structure
but work on the normal tree."""
import re
from lxml import etree
from typing import Union, List
from tinycss2.nth import parse_nth
from . import parser
from .parser import SelectorError
# http://dev.w3.org/csswg/selectors/#whitespace
split_whitespace = re.compile("[^ \t\r\n\f]+").findall
def ascii_lower(string): # from webencodings
r"""Transform (only) ASCII letters to lower case: A-Z is mapped to a-z."""
return string.encode("utf8").lower().decode("utf8")
# pylint: disable=protected-access,comparison-with-callable,invalid-name,bad-super-call
# pylint: disable=unnecessary-lambda-assignment
## Iterators without comments.
def iterancestors(element):
"""Iterate over ancestors but ignore comments."""
for e in element.iterancestors():
if isinstance(e, etree._Comment):
continue
yield e
def iterdescendants(element):
"""Iterate over descendants but ignore comments"""
for e in element.iterdescendants():
if isinstance(e, etree._Comment):
continue
yield e
def itersiblings(element, preceding=False):
"""Iterate over descendants but ignore comments"""
for e in element.itersiblings(preceding=preceding):
if isinstance(e, etree._Comment):
continue
yield e
def iterchildren(element):
"""Iterate over children but ignore comments"""
for e in element.iterchildren():
if isinstance(e, etree._Comment):
continue
yield e
def getprevious(element):
"""Get the previous non-comment element"""
for e in itersiblings(element, preceding=True):
return e
return None
def getnext(element):
"""Get the next non-comment element"""
for e in itersiblings(element, preceding=False):
return e
return None
def FALSE(_el):
"""Always returns 0"""
return 0
def TRUE(_el):
"""Always returns 1"""
return 1
class BooleanCompiler:
def __init__(self) -> None:
self._func_map = {
parser.CombinedSelector: self._compile_combined,
parser.CompoundSelector: self._compile_compound,
parser.NegationSelector: self._compile_negation,
parser.RelationalSelector: self._compile_relational,
parser.MatchesAnySelector: self._compile_any,
parser.SpecificityAdjustmentSelector: self._compile_any,
parser.LocalNameSelector: self._compile_local_name,
parser.NamespaceSelector: self._compile_namespace,
parser.ClassSelector: self._compile_class,
parser.IDSelector: self._compile_id,
parser.AttributeSelector: self._compile_attribute,
parser.PseudoClassSelector: self._compile_pseudoclass,
parser.FunctionalPseudoClassSelector: self._compile_functional_pseudoclass,
}
def _compile_combined(self, selector: parser.CombinedSelector):
left_inside = self.compile_node(selector.left)
if left_inside == FALSE:
return FALSE # 0 and x == 0
if left_inside == TRUE:
# 1 and x == x, but the element matching 1 still needs to exist.
if selector.combinator in (" ", ">"):
left = lambda el: el.getparent() is not None
elif selector.combinator in ("~", "+"):
left = lambda el: getprevious(el) is not None
else:
raise SelectorError("Unknown combinator", selector.combinator)
elif selector.combinator == " ":
left = lambda el: any((left_inside(e)) for e in el.ancestors())
elif selector.combinator == ">":
left = lambda el: el.getparent() is not None and left_inside(el.getparent())
elif selector.combinator == "+":
left = lambda el: getprevious(el) is not None and left_inside(
getprevious(el)
)
elif selector.combinator == "~":
left = lambda el: any(
(left_inside(e)) for e in itersiblings(el, preceding=True)
)
else:
raise SelectorError("Unknown combinator", selector.combinator)
right = self.compile_node(selector.right)
if right == FALSE:
return FALSE # 0 and x == 0
if right == TRUE:
return left # 1 and x == x
# Evaluate combinators right to left
return lambda el: right(el) and left(el)
def _compile_compound(self, selector: parser.CompoundSelector):
sub_expressions = [
expr
for expr in map(self.compile_node, selector.simple_selectors)
if expr != TRUE
]
if len(sub_expressions) == 1:
return sub_expressions[0]
if FALSE in sub_expressions:
return FALSE
if sub_expressions:
return lambda e: all(expr(e) for expr in sub_expressions)
return TRUE # all([]) == True
def _compile_negation(self, selector: parser.NegationSelector):
sub_expressions = [
expr
for expr in [
self.compile_node(selector.parsed_tree)
for selector in selector.selector_list
]
if expr != TRUE
]
if not sub_expressions:
return FALSE
return lambda el: not any(expr(el) for expr in sub_expressions)
@staticmethod
def _get_subexpr(expression, relative_selector):
"""Helper function for RelationalSelector"""
if relative_selector.combinator == " ":
return lambda el: any(expression(e) for e in iterdescendants(el))
if relative_selector.combinator == ">":
return lambda el: any(expression(e) for e in iterchildren(el))
if relative_selector.combinator == "+":
return lambda el: expression(next(itersiblings(el)))
if relative_selector.combinator == "~":
return lambda el: any(expression(e) for e in itersiblings(el))
raise SelectorError(
f"Unknown relational selector '{relative_selector.combinator}'"
)
def _compile_relational(self, selector: parser.RelationalSelector):
sub_expr = []
for relative_selector in selector.selector_list:
expression = self.compile_node(relative_selector.selector.parsed_tree)
if expression == FALSE:
continue
sub_expr.append(self._get_subexpr(expression, relative_selector))
return lambda el: any(expr(el) for expr in sub_expr)
def _compile_any(
self,
selector: Union[
parser.MatchesAnySelector, parser.SpecificityAdjustmentSelector
],
):
sub_expressions = [
expr
for expr in [
self.compile_node(selector.parsed_tree)
for selector in selector.selector_list
]
if expr != FALSE
]
if not sub_expressions:
return FALSE
return lambda el: any(expr(el) for expr in sub_expressions)
def _compile_local_name(self, selector: parser.LocalNameSelector):
return lambda el: el.TAG == selector.local_name
def _compile_namespace(self, selector: parser.NamespaceSelector):
return lambda el: el.NAMESPACE == selector.namespace
def _compile_class(self, selector: parser.ClassSelector):
return lambda el: selector.class_name in el.classes
def _compile_id(self, selector: parser.IDSelector):
return lambda el: super(etree.ElementBase, el).get("id", None) == selector.ident # type: ignore
def _compile_attribute(self, selector: parser.AttributeSelector):
if selector.namespace is not None:
if selector.namespace:
key_func = lambda el: (
f"{{{selector.namespace}}}{selector.name}"
if el.NAMESPACE != selector.namespace
else selector.name
)
else:
key_func = lambda el: selector.name
value = selector.value
if selector.case_sensitive is False:
value = value.lower()
attribute_value = (
lambda el: super(etree.ElementBase, el)
.get(key_func(el), "") # type: ignore
.lower()
)
else:
attribute_value = lambda el: super(etree.ElementBase, el).get( # type: ignore
key_func(el), ""
)
if selector.operator is None:
return lambda el: key_func(el) in el.attrib
if selector.operator == "=":
return lambda el: (
key_func(el) in el.attrib and attribute_value(el) == value
)
if selector.operator == "~=":
return (
FALSE
if len(value.split()) != 1 or value.strip() != value
else lambda el: value in split_whitespace(attribute_value(el))
)
if selector.operator == "|=":
return lambda el: (
key_func(el) in el.attrib
and (
attribute_value(el) == value
or attribute_value(el).startswith(value + "-")
)
)
if selector.operator == "^=":
if value:
return lambda el: attribute_value(el).startswith(value)
return FALSE
if selector.operator == "$=":
return (
(lambda el: attribute_value(el).endswith(value)) if value else FALSE
)
if selector.operator == "*=":
return (lambda el: value in attribute_value(el)) if value else FALSE
raise SelectorError("Unknown attribute operator", selector.operator)
# In any namespace
raise NotImplementedError # TODO
def _compile_pseudoclass(self, selector: parser.PseudoClassSelector):
if selector.name in ("link", "any-link", "local-link"):
def ancestors_or_self(el):
yield el
yield from iterancestors(el)
return lambda el: any(
e.TAG == "a" and super(etree.ElementBase, e).get("href", "") != "" # type: ignore
for e in ancestors_or_self(el)
)
if selector.name in (
"visited",
"hover",
"active",
"focus",
"focus-within",
"focus-visible",
"target",
"target-within",
"current",
"past",
"future",
"playing",
"paused",
"seeking",
"buffering",
"stalled",
"muted",
"volume-locked",
"user-valid",
"user-invalid",
):
# Not applicable in a static context: never match.
return FALSE
if selector.name in ("enabled", "disabled", "checked"):
# Not applicable to SVG
return FALSE
if selector.name in ("root", "scope"):
return lambda el: el.getparent() is None
if selector.name == "first-child":
return lambda el: getprevious(el) is None
if selector.name == "last-child":
return lambda el: getnext(el) is None
if selector.name == "first-of-type":
return lambda el: all(
s.tag != el.tag for s in itersiblings(el, preceding=True)
)
if selector.name == "last-of-type":
return lambda el: all(s.tag != el.tag for s in itersiblings(el))
if selector.name == "only-child":
return lambda el: getnext(el) is None and getprevious(el) is None
if selector.name == "only-of-type":
return lambda el: all(s.tag != el.tag for s in itersiblings(el)) and all(
s.tag != el.tag for s in itersiblings(el, preceding=True)
)
if selector.name == "empty":
return lambda el: not list(el) and el.text is None
raise SelectorError("Unknown pseudo-class", selector.name)
def _compile_lang(self, selector: parser.FunctionalPseudoClassSelector):
langs = []
tokens = [
token
for token in selector.arguments
if token.type not in ("whitespace", "comment")
]
while tokens:
token = tokens.pop(0)
if token.type == "ident":
langs.append(token.lower_value)
elif token.type == "string":
langs.append(ascii_lower(token.value))
else:
raise SelectorError("Invalid arguments for :lang()")
if tokens:
token = tokens.pop(0)
if token.type != "ident" and token.value != ",":
raise SelectorError("Invalid arguments for :lang()")
def haslang(el, lang):
print(
el.get("lang"),
lang,
el.get("lang", "") == lang or el.get("lang", "").startswith(lang + "-"),
)
return el.get("lang", "").lower() == lang or el.get(
"lang", ""
).lower().startswith(lang + "-")
return lambda el: any(
haslang(el, lang) or any(haslang(el2, lang) for el2 in iterancestors(el))
for lang in langs
)
def _compile_functional_pseudoclass(
self, selector: parser.FunctionalPseudoClassSelector
):
if selector.name == "lang":
return self._compile_lang(selector)
nth: List[str] = []
selector_list: List[str] = []
current_list = nth
for argument in selector.arguments:
if argument.type == "ident" and argument.value == "of":
if current_list is nth:
current_list = selector_list
continue
current_list.append(argument)
if selector_list:
compiled = tuple(
self.compile_node(selector.parsed_tree)
for selector in parser.parse(selector_list)
)
test = lambda el: all(expr(el) for expr in compiled)
else:
test = TRUE
if selector.name == "nth-child":
count = lambda el: sum(
1 for e in itersiblings(el, preceding=True) if test(e)
)
elif selector.name == "nth-last-child":
count = lambda el: sum(1 for e in itersiblings(el) if test(e))
elif selector.name == "nth-of-type":
count = lambda el: sum(
1
for s in (e for e in itersiblings(el, preceding=True) if test(e))
if s.tag == el.tag
)
elif selector.name == "nth-last-of-type":
count = lambda el: sum(
1 for s in (e for e in itersiblings(el) if test(e)) if s.tag == el.tag
)
else:
raise SelectorError("Unknown pseudo-class", selector.name)
count_func = lambda el: count(el) if test(el) else float("nan")
result = parse_nth(nth)
if result is None:
raise SelectorError(f"Invalid arguments for :{selector.name}()")
a, b = result
# x is the number of siblings before/after the element
# Matches if a positive or zero integer n exists so that:
# x = a*n + b-1
# x = a*n + B
B = b - 1
if a == 0:
# x = B
return lambda el: count_func(el) == B
# n = (x - B) / a
def evaluator(el):
n, r = divmod(count_func(el) - B, a)
return r == 0 and n >= 0
return evaluator
def compile_node(self, selector):
"""Return a boolean expression, as a callable.
When evaluated in a context where the `el` variable is an
:class:`cssselect2.tree.Element` object, tells whether the element is a
subject of `selector`.
"""
try:
return self._func_map[selector.__class__](selector)
except KeyError as e:
raise TypeError(type(selector), selector) from e
CSSCompiler = BooleanCompiler()

View File

@@ -0,0 +1,548 @@
# Forked from cssselect2, 1.2.1, BSD License
"""Parse CSS declarations."""
from tinycss2 import parse_component_value_list
__all__ = ["parse"]
SUPPORTED_PSEUDO_ELEMENTS = {
# As per CSS Pseudo-Elements Module Level 4
"first-line",
"first-letter",
"prefix",
"postfix",
"selection",
"target-text",
"spelling-error",
"grammar-error",
"before",
"after",
"marker",
"placeholder",
"file-selector-button",
# As per CSS Generated Content for Paged Media Module
"footnote-call",
"footnote-marker",
# As per CSS Scoping Module Level 1
"content",
"shadow",
}
def parse(input, namespaces=None, forgiving=False, relative=False):
"""Yield tinycss2 selectors found in given ``input``.
:param input:
A string, or an iterable of tinycss2 component values.
"""
if isinstance(input, str):
input = parse_component_value_list(input)
tokens = TokenStream(input)
namespaces = namespaces or {}
try:
yield parse_selector(tokens, namespaces, relative)
except SelectorError as exception:
if forgiving:
return
raise exception
while 1:
next = tokens.next()
if next is None:
return
elif next == ",":
try:
yield parse_selector(tokens, namespaces, relative)
except SelectorError as exception:
if not forgiving:
raise exception
else:
if not forgiving:
raise SelectorError(next, f"unexpected {next.type} token.")
def parse_selector(tokens, namespaces, relative=False):
tokens.skip_whitespace_and_comment()
if relative:
peek = tokens.peek()
if peek in (">", "+", "~"):
initial_combinator = peek.value
tokens.next()
else:
initial_combinator = " "
tokens.skip_whitespace_and_comment()
result, pseudo_element = parse_compound_selector(tokens, namespaces)
while 1:
has_whitespace = tokens.skip_whitespace()
while tokens.skip_comment():
has_whitespace = tokens.skip_whitespace() or has_whitespace
selector = Selector(result, pseudo_element)
if relative:
selector = RelativeSelector(initial_combinator, selector)
if pseudo_element is not None:
return selector
peek = tokens.peek()
if peek is None or peek == ",":
return selector
elif peek in (">", "+", "~"):
combinator = peek.value
tokens.next()
elif has_whitespace:
combinator = " "
else:
return selector
compound, pseudo_element = parse_compound_selector(tokens, namespaces)
result = CombinedSelector(result, combinator, compound)
def parse_compound_selector(tokens, namespaces):
type_selectors = parse_type_selector(tokens, namespaces)
simple_selectors = type_selectors if type_selectors is not None else []
while 1:
simple_selector, pseudo_element = parse_simple_selector(tokens, namespaces)
if pseudo_element is not None or simple_selector is None:
break
simple_selectors.append(simple_selector)
if simple_selectors or (type_selectors, pseudo_element) != (None, None):
return CompoundSelector(simple_selectors), pseudo_element
peek = tokens.peek()
peek_type = peek.type if peek else "EOF"
raise SelectorError(peek, f"expected a compound selector, got {peek_type}")
def parse_type_selector(tokens, namespaces):
tokens.skip_whitespace()
qualified_name = parse_qualified_name(tokens, namespaces)
if qualified_name is None:
return None
simple_selectors = []
namespace, local_name = qualified_name
if local_name is not None:
simple_selectors.append(LocalNameSelector(local_name))
if namespace is not None:
simple_selectors.append(NamespaceSelector(namespace))
return simple_selectors
def parse_simple_selector(tokens, namespaces):
peek = tokens.peek()
if peek is None:
return None, None
if peek.type == "hash" and peek.is_identifier:
tokens.next()
return IDSelector(peek.value), None
elif peek == ".":
tokens.next()
next = tokens.next()
if next is None or next.type != "ident":
raise SelectorError(next, f"Expected a class name, got {next}")
return ClassSelector(next.value), None
elif peek.type == "[] block":
tokens.next()
attr = parse_attribute_selector(TokenStream(peek.content), namespaces)
return attr, None
elif peek == ":":
tokens.next()
next = tokens.next()
if next == ":":
next = tokens.next()
if next is None or next.type != "ident":
raise SelectorError(next, f"Expected a pseudo-element name, got {next}")
value = next.lower_value
if value not in SUPPORTED_PSEUDO_ELEMENTS:
raise SelectorError(
next, f"Expected a supported pseudo-element, got {value}"
)
return None, value
elif next is not None and next.type == "ident":
name = next.lower_value
if name in ("before", "after", "first-line", "first-letter"):
return None, name
else:
return PseudoClassSelector(name), None
elif next is not None and next.type == "function":
name = next.lower_name
if name in ("is", "where", "not", "has"):
return parse_logical_combination(next, namespaces, name), None
else:
return (FunctionalPseudoClassSelector(name, next.arguments), None)
else:
raise SelectorError(next, f"unexpected {next} token.")
else:
return None, None
def parse_logical_combination(matches_any_token, namespaces, name):
forgiving = True
relative = False
if name == "is":
selector_class = MatchesAnySelector
elif name == "where":
selector_class = SpecificityAdjustmentSelector
elif name == "not":
forgiving = False
selector_class = NegationSelector
elif name == "has":
relative = True
selector_class = RelationalSelector
selectors = [
selector
for selector in parse(
matches_any_token.arguments, namespaces, forgiving, relative
)
if selector.pseudo_element is None
]
return selector_class(selectors)
def parse_attribute_selector(tokens, namespaces):
tokens.skip_whitespace()
qualified_name = parse_qualified_name(tokens, namespaces, is_attribute=True)
if qualified_name is None:
next = tokens.next()
raise SelectorError(next, f"expected attribute name, got {next}")
namespace, local_name = qualified_name
tokens.skip_whitespace()
peek = tokens.peek()
if peek is None:
operator = None
value = None
elif peek in ("=", "~=", "|=", "^=", "$=", "*="):
operator = peek.value
tokens.next()
tokens.skip_whitespace()
next = tokens.next()
if next is None or next.type not in ("ident", "string"):
next_type = "None" if next is None else next.type
raise SelectorError(next, f"expected attribute value, got {next_type}")
value = next.value
else:
raise SelectorError(peek, f"expected attribute selector operator, got {peek}")
tokens.skip_whitespace()
next = tokens.next()
case_sensitive = None
if next is not None:
if next.type == "ident" and next.value.lower() == "i":
case_sensitive = False
elif next.type == "ident" and next.value.lower() == "s":
case_sensitive = True
else:
raise SelectorError(next, f"expected ], got {next.type}")
return AttributeSelector(namespace, local_name, operator, value, case_sensitive)
def parse_qualified_name(tokens, namespaces, is_attribute=False):
"""Return ``(namespace, local)`` for given tokens.
Can also return ``None`` for a wildcard.
The empty string for ``namespace`` means "no namespace".
"""
peek = tokens.peek()
if peek is None:
return None
if peek.type == "ident":
first_ident = tokens.next()
peek = tokens.peek()
if peek != "|":
namespace = "" if is_attribute else namespaces.get(None, None)
return namespace, (first_ident.value, first_ident.lower_value)
tokens.next()
namespace = namespaces.get(first_ident.value)
if namespace is None:
raise SelectorError(
first_ident, f"undefined namespace prefix: {first_ident.value}"
)
elif peek == "*":
next = tokens.next()
peek = tokens.peek()
if peek != "|":
if is_attribute:
raise SelectorError(next, f"expected local name, got {next.type}")
return namespaces.get(None, None), None
tokens.next()
namespace = None
elif peek == "|":
tokens.next()
namespace = ""
else:
return None
# If we get here, we just consumed '|' and set ``namespace``
next = tokens.next()
if next.type == "ident":
return namespace, (next.value, next.lower_value)
elif next == "*" and not is_attribute:
return namespace, None
else:
raise SelectorError(next, f"expected local name, got {next.type}")
class SelectorError(ValueError):
"""A specialized ``ValueError`` for invalid selectors."""
class TokenStream:
def __init__(self, tokens):
self.tokens = iter(tokens)
self.peeked = [] # In reversed order
def next(self):
if self.peeked:
return self.peeked.pop()
else:
return next(self.tokens, None)
def peek(self):
if not self.peeked:
self.peeked.append(next(self.tokens, None))
return self.peeked[-1]
def skip(self, skip_types):
found = False
while 1:
peek = self.peek()
if peek is None or peek.type not in skip_types:
break
self.next()
found = True
return found
def skip_whitespace(self):
return self.skip(["whitespace"])
def skip_comment(self):
return self.skip(["comment"])
def skip_whitespace_and_comment(self):
return self.skip(["comment", "whitespace"])
class Selector:
def __init__(self, tree, pseudo_element=None):
self.parsed_tree = tree
self.pseudo_element = pseudo_element
if pseudo_element is None:
#: Tuple of 3 integers: http://www.w3.org/TR/selectors/#specificity
self.specificity = tree.specificity
else:
a, b, c = tree.specificity
self.specificity = a, b, c + 1
def __repr__(self):
pseudo = f"::{self.pseudo_element}" if self.pseudo_element else ""
return f"{self.parsed_tree!r}{pseudo}"
class RelativeSelector:
def __init__(self, combinator, selector):
self.combinator = combinator
self.selector = selector
@property
def specificity(self):
return self.selector.specificity
@property
def pseudo_element(self):
return self.selector.pseudo_element
def __repr__(self):
return (
f"{self.selector!r}"
if self.combinator == " "
else f"{self.combinator} {self.selector!r}"
)
class CombinedSelector:
def __init__(self, left, combinator, right):
#: Combined or compound selector
self.left = left
# One of `` `` (a single space), ``>``, ``+`` or ``~``.
self.combinator = combinator
#: compound selector
self.right = right
@property
def specificity(self):
a1, b1, c1 = self.left.specificity
a2, b2, c2 = self.right.specificity
return a1 + a2, b1 + b2, c1 + c2
def __repr__(self):
return f"{self.left!r}{self.combinator}{self.right!r}"
class CompoundSelector:
def __init__(self, simple_selectors):
self.simple_selectors = simple_selectors
@property
def specificity(self):
if self.simple_selectors:
# zip(*foo) turns [(a1, b1, c1), (a2, b2, c2), ...]
# into [(a1, a2, ...), (b1, b2, ...), (c1, c2, ...)]
return tuple(
map(sum, zip(*(sel.specificity for sel in self.simple_selectors)))
)
else:
return 0, 0, 0
def __repr__(self):
return "".join(map(repr, self.simple_selectors))
class LocalNameSelector:
specificity = 0, 0, 1
def __init__(self, local_name):
self.local_name, self.lower_local_name = local_name
def __repr__(self):
return self.local_name
class NamespaceSelector:
specificity = 0, 0, 0
def __init__(self, namespace):
#: The namespace URL as a string,
#: or the empty string for elements not in any namespace.
self.namespace = namespace
def __repr__(self):
if self.namespace == "":
return "|"
else:
return f"{{{self.namespace}}}|"
class IDSelector:
specificity = 1, 0, 0
def __init__(self, ident):
self.ident = ident
def __repr__(self):
return f"#{self.ident}"
class ClassSelector:
specificity = 0, 1, 0
def __init__(self, class_name):
self.class_name = class_name
def __repr__(self):
return f".{self.class_name}"
class AttributeSelector:
specificity = 0, 1, 0
def __init__(self, namespace, name, operator, value, case_sensitive):
self.namespace = namespace
self.name, self.lower_name = name
#: A string like ``=`` or ``~=``, or None for ``[attr]`` selectors
self.operator = operator
#: A string, or None for ``[attr]`` selectors
self.value = value
#: ``True`` if case-sensitive, ``False`` if case-insensitive, ``None``
#: if depends on the document language
self.case_sensitive = case_sensitive
def __repr__(self):
namespace = "*|" if self.namespace is None else f"{{{self.namespace}}}"
case_sensitive = (
""
if self.case_sensitive is None
else f" {'s' if self.case_sensitive else 'i'}"
)
return f"[{namespace}{self.name}{self.operator}{self.value!r}{case_sensitive}]"
class PseudoClassSelector:
specificity = 0, 1, 0
def __init__(self, name):
self.name = name
def __repr__(self):
return ":" + self.name
class FunctionalPseudoClassSelector:
specificity = 0, 1, 0
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
def __repr__(self):
return f":{self.name}{tuple(self.arguments)!r}"
class NegationSelector:
def __init__(self, selector_list):
self.selector_list = selector_list
@property
def specificity(self):
if self.selector_list:
return max(selector.specificity for selector in self.selector_list)
else:
return (0, 0, 0)
def __repr__(self):
return f":not({', '.join(repr(sel) for sel in self.selector_list)})"
class RelationalSelector:
def __init__(self, selector_list):
self.selector_list = selector_list
@property
def specificity(self):
if self.selector_list:
return max(selector.specificity for selector in self.selector_list)
else:
return (0, 0, 0)
def __repr__(self):
return f":has({', '.join(repr(sel) for sel in self.selector_list)})"
class MatchesAnySelector:
def __init__(self, selector_list):
self.selector_list = selector_list
@property
def specificity(self):
if self.selector_list:
return max(selector.specificity for selector in self.selector_list)
else:
return (0, 0, 0)
def __repr__(self):
return f":is({', '.join(repr(sel) for sel in self.selector_list)})"
class SpecificityAdjustmentSelector:
def __init__(self, selector_list):
self.selector_list = selector_list
@property
def specificity(self):
return (0, 0, 0)
def __repr__(self):
return f":where({', '.join(repr(sel) for sel in self.selector_list)})"

View File

@@ -0,0 +1,4 @@
# coding=utf-8This directory contains compatibility layers for all the `simple` modules, such as `simplepath` and `simplestyle`
This directory IS NOT a module path, to denote this we are using a dash in the name and there is no '__init__.py'

View File

@@ -0,0 +1,46 @@
# coding=utf-8
#
# 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.
#
# pylint: disable=invalid-name,unused-argument
"""Deprecated bezmisc API"""
from inkex.deprecated import deprecate
from inkex import bezier
bezierparameterize = deprecate(bezier.bezierparameterize)
linebezierintersect = deprecate(bezier.linebezierintersect)
bezierpointatt = deprecate(bezier.bezierpointatt)
bezierslopeatt = deprecate(bezier.bezierslopeatt)
beziertatslope = deprecate(bezier.beziertatslope)
tpoint = deprecate(bezier.tpoint)
beziersplitatt = deprecate(bezier.beziersplitatt)
pointdistance = deprecate(bezier.pointdistance)
Gravesen_addifclose = deprecate(bezier.addifclose)
balf = deprecate(bezier.balf)
bezierlengthSimpson = deprecate(bezier.bezierlength)
beziertatlength = deprecate(bezier.beziertatlength)
bezierlength = bezierlengthSimpson
@deprecate
def Simpson(func, a, b, n_limit, tolerance):
"""bezier.simpson(a, b, n_limit, tolerance, balf_arguments)"""
raise AttributeError(
"""Because bezmisc.Simpson used global variables, it's not possible to
call the replacement code automatically. In fact it's unlikely you were
using the code or functionality you think you were since it's a highly
broken way of writing python."""
)

View File

@@ -0,0 +1,25 @@
# coding=utf-8
#
# 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.
#
# pylint: disable=invalid-name
"""Deprecated cspsubdiv API"""
from inkex.deprecated import deprecate
from inkex import bezier
maxdist = deprecate(bezier.maxdist)
cspsubdiv = deprecate(bezier.cspsubdiv)
subdiv = deprecate(bezier.subdiv)

View File

@@ -0,0 +1,52 @@
# coding=utf-8
#
# 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.
#
# pylint: disable=invalid-name
"""Deprecated cubic super path API"""
from inkex.deprecated import deprecate
from inkex import paths
@deprecate
def ArcToPath(p1, params):
return paths.arc_to_path(p1, params)
@deprecate
def CubicSuperPath(simplepath):
return paths.Path(simplepath).to_superpath()
@deprecate
def unCubicSuperPath(csp):
return paths.CubicSuperPath(csp).to_path().to_arrays()
@deprecate
def parsePath(d):
return paths.CubicSuperPath(paths.Path(d))
@deprecate
def formatPath(p):
return str(paths.Path(unCubicSuperPath(p)))
matprod = deprecate(paths.matprod)
rotmat = deprecate(paths.rotmat)
applymat = deprecate(paths.applymat)
norm = deprecate(paths.norm)

View File

@@ -0,0 +1,92 @@
# coding=utf-8
#
# 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.
#
# pylint: disable=invalid-name,missing-docstring
"""Deprecated ffgeom API"""
from collections import namedtuple
from inkex.deprecated import deprecate
from inkex.transforms import DirectedLineSegment as NewSeg
try:
NaN = float("NaN")
except ValueError:
PosInf = 1e300000
NaN = PosInf / PosInf
class Point(namedtuple("Point", "x y")):
__slots__ = ()
def __getitem__(self, key):
if isinstance(key, str):
key = "xy".index(key)
return super(Point, self).__getitem__(key)
class Segment(NewSeg):
@deprecate
def __init__(self, e0, e1):
"""inkex.transforms.DirectedLineSegment((x1, y1), (x2, y2))"""
if isinstance(e0, dict):
e0 = (e0["x"], e0["y"])
if isinstance(e1, dict):
e1 = (e1["x"], e1["y"])
super(Segment, self).__init__(e0, e1)
def __getitem__(self, key):
if key:
return {"x": self.x.maximum, "y": self.y.maximum}
return {"x": self.x.minimum, "y": self.y.minimum}
delta_x = lambda self: self.width
delta_y = lambda self: self.height
run = delta_x
rise = delta_y
def distanceToPoint(self, p):
return self.distance_to_point(p["x"], p["y"])
def perpDistanceToPoint(self, p):
return self.perp_distance(p["x"], p["y"])
def angle(self):
return super(Segment, self).angle
def length(self):
return super(Segment, self).length
def pointAtLength(self, length):
return self.point_at_length(length)
def pointAtRatio(self, ratio):
return self.point_at_ratio(ratio)
def createParallel(self, p):
self.parallel(p["x"], p["y"])
@deprecate
def intersectSegments(s1, s2):
"""transforms.Segment(s1).intersect(s2)"""
return Point(*s1.intersect(s2))
@deprecate
def dot(s1, s2):
"""transforms.Segment(s1).dot(s2)"""
return s1.dot(s2)

View File

@@ -0,0 +1,80 @@
# coding=utf-8
#
# Copyright (C) 2008 Stephen Silver
#
# 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
#
"""
Deprecated module for running SVG-generating commands in Inkscape extensions
"""
import os
import sys
import tempfile
from subprocess import Popen, PIPE
from inkex.deprecated import deprecate
def run(command_format, prog_name):
"""inkex.commands.call(...)"""
svgfile = tempfile.mktemp(".svg")
command = command_format % svgfile
msg = None
# ps2pdf may attempt to write to the current directory, which may not
# be writeable, so we switch to the temp directory first.
try:
os.chdir(tempfile.gettempdir())
except IOError:
pass
try:
proc = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
return_code = proc.wait()
out = proc.stdout.read()
err = proc.stderr.read()
if msg is None:
if return_code:
msg = "{} failed:\n{}\n{}\n".format(prog_name, out, err)
elif err:
sys.stderr.write(
"{} executed but logged the following error:\n{}\n{}\n".format(
prog_name, out, err
)
)
except Exception as inst:
msg = "Error attempting to run {}: {}".format(prog_name, str(inst))
# If successful, copy the output file to stdout.
if msg is None:
if os.name == "nt": # make stdout work in binary on Windows
import msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
try:
with open(svgfile, "rb") as fhl:
sys.stdout.write(fhl.read().decode(sys.stdout.encoding))
except IOError as inst:
msg = "Error reading temporary file: {}".format(str(inst))
try:
# Clean up.
os.remove(svgfile)
except (IOError, OSError):
pass
# Output error message (if any) and exit.
return msg

View File

@@ -0,0 +1,68 @@
# coding=utf-8
# COPYRIGHT
#
# pylint: disable=invalid-name
#
"""
Depreicated simplepath replacements with documentation
"""
import math
from inkex.deprecated import deprecate, DeprecatedDict
from inkex.transforms import Transform
from inkex.paths import Path
pathdefs = DeprecatedDict(
{
"M": ["L", 2, [float, float], ["x", "y"]],
"L": ["L", 2, [float, float], ["x", "y"]],
"H": ["H", 1, [float], ["x"]],
"V": ["V", 1, [float], ["y"]],
"C": [
"C",
6,
[float, float, float, float, float, float],
["x", "y", "x", "y", "x", "y"],
],
"S": ["S", 4, [float, float, float, float], ["x", "y", "x", "y"]],
"Q": ["Q", 4, [float, float, float, float], ["x", "y", "x", "y"]],
"T": ["T", 2, [float, float], ["x", "y"]],
"A": [
"A",
7,
[float, float, float, int, int, float, float],
["r", "r", "a", 0, "s", "x", "y"],
],
"Z": ["L", 0, [], []],
}
)
@deprecate
def parsePath(d):
"""element.path.to_arrays()"""
return Path(d).to_arrays()
@deprecate
def formatPath(a):
"""str(element.path) or str(Path(array))"""
return str(Path(a))
@deprecate
def translatePath(p, x, y):
"""Path(array).translate(x, y)"""
p[:] = Path(p).translate(x, y).to_arrays()
@deprecate
def scalePath(p, x, y):
"""Path(array).scale(x, y)"""
p[:] = Path(p).scale(x, y).to_arrays()
@deprecate
def rotatePath(p, a, cx=0, cy=0):
"""Path(array).rotate(angle_degrees, (center_x, center_y))"""
p[:] = Path(p).rotate(math.degrees(a), (cx, cy)).to_arrays()

View File

@@ -0,0 +1,55 @@
# coding=utf-8
# COPYRIGHT
"""DOCSTRING"""
import inkex
from inkex.colors.spaces.named import _COLORS as svgcolors
from inkex.deprecated import deprecate
@deprecate
def parseStyle(s):
"""dict(inkex.Style.parse_str(s))"""
return dict(inkex.Style.parse_str(s))
@deprecate
def formatStyle(a):
"""str(inkex.Style(a))"""
return str(inkex.Style(a))
@deprecate
def isColor(c):
"""inkex.colors.is_color(c)"""
return inkex.colors.is_color(c)
@deprecate
def parseColor(c):
"""inkex.Color(c).to_rgb()"""
return tuple(inkex.Color(c).to_rgb())
@deprecate
def formatColoria(a):
"""str(inkex.Color(a))"""
return str(inkex.ColorRGB(a))
@deprecate
def formatColorfa(a):
"""str(inkex.Color(a))"""
return str(inkex.ColorRGB([b * 255 for b in a]))
@deprecate
def formatColor3i(r, g, b):
"""str(inkex.Color((r, g, b)))"""
return str(inkex.ColorRGB((r, g, b)))
@deprecate
def formatColor3f(r, g, b):
"""str(inkex.Color((r, g, b)))"""
return str(inkex.ColorRGB((r * 255, g * 255, b * 255)))

View File

@@ -0,0 +1,122 @@
# coding=utf-8
#
# pylint: disable=invalid-name
#
"""
Depreicated simpletransform replacements with documentation
"""
import warnings
from inkex.deprecated import deprecate
from inkex.transforms import Transform, BoundingBox, cubic_extrema
from inkex.paths import Path
import inkex, cubicsuperpath
def _lists(mat):
return [list(row) for row in mat]
@deprecate
def parseTransform(transf, mat=None):
"""Transform(str).matrix"""
t = Transform(transf)
if mat is not None:
t = Transform(mat) @ t
return _lists(t.matrix)
@deprecate
def formatTransform(mat):
"""str(Transform(mat))"""
if len(mat) == 3:
warnings.warn("3x3 matrices not suported")
mat = mat[:2]
return str(Transform(mat))
@deprecate
def invertTransform(mat):
"""-Transform(mat)"""
return _lists((-Transform(mat)).matrix)
@deprecate
def composeTransform(mat1, mat2):
"""Transform(M1) * Transform(M2)"""
return _lists((Transform(mat1) @ Transform(mat2)).matrix)
@deprecate
def composeParents(node, mat):
"""elem.composed_transform() or elem.transform * Transform(mat)"""
return (node.transform @ Transform(mat)).matrix
@deprecate
def applyTransformToNode(mat, node):
"""elem.transform = Transform(mat) * elem.transform"""
node.transform = Transform(mat) @ node.transform
@deprecate
def applyTransformToPoint(mat, pt):
"""Transform(mat).apply_to_point(pt)"""
pt2 = Transform(mat).apply_to_point(pt)
# Apply in place as original method was modifying arrays in place.
# but don't do this in your code! This is not good code design.
pt[0] = pt2[0]
pt[1] = pt2[1]
@deprecate
def applyTransformToPath(mat, path):
"""Path(path).transform(mat)"""
return Path(path).transform(Transform(mat)).to_arrays()
@deprecate
def fuseTransform(node):
"""node.apply_transform()"""
return node.apply_transform()
@deprecate
def boxunion(b1, b2):
"""list(BoundingBox(b1) + BoundingBox(b2))"""
bbox = BoundingBox(b1[:2], b1[2:]) + BoundingBox(b2[:2], b2[2:])
return bbox.x.minimum, bbox.x.maximum, bbox.y.minimum, bbox.y.maximum
@deprecate
def roughBBox(path):
"""list(Path(path)).bounding_box())"""
bbox = Path(path).bounding_box()
return bbox.x.minimum, bbox.x.maximum, bbox.y.minimum, bbox.y.maximum
@deprecate
def refinedBBox(path):
"""list(Path(path)).bounding_box())"""
bbox = Path(path).bounding_box()
return bbox.x.minimum, bbox.x.maximum, bbox.y.minimum, bbox.y.maximum
@deprecate
def cubicExtrema(y0, y1, y2, y3):
"""from inkex.transforms import cubic_extrema"""
return cubic_extrema(y0, y1, y2, y3)
@deprecate
def computeBBox(aList, mat=[[1, 0, 0], [0, 1, 0]]):
"""sum([node.bounding_box() for node in aList])"""
return sum([node.bounding_box() for node in aList], None)
@deprecate
def computePointInNode(pt, node, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
"""(-Transform(node.transform * mat)).apply_to_point(pt)"""
return (-Transform(node.transform * mat)).apply_to_point(pt)

View File

@@ -0,0 +1,3 @@
from .main import *
from .meta import deprecate, _deprecated
from .deprecatedeffect import DeprecatedEffect, Effect

View File

@@ -0,0 +1,304 @@
# coding=utf-8
#
# Copyright (C) 2018 - Martin Owens <doctormo@mgail.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-1301, USA.
#
"""
Deprecation functionality for the pre-1.0 Inkex main effect class.
"""
#
# We ignore a lot of pylint warnings here:
#
# pylint: disable=invalid-name,unused-argument,missing-docstring,too-many-public-methods
#
import sys
import argparse
from argparse import ArgumentParser
from .. import utils
from .. import base
from ..base import SvgThroughMixin, InkscapeExtension
from ..localization import inkex_gettext as _
from .meta import _deprecated
class DeprecatedEffect:
"""An Inkscape effect, takes SVG in and outputs SVG, providing a deprecated layer"""
options = argparse.Namespace()
def __init__(self):
super().__init__()
self._doc_ids = None
self._args = None
# These are things we reference in the deprecated code, they are provided
# by the new effects code, but we want to keep this as a Mixin so these
# items will keep pylint happy and let use check our code as we write.
if not hasattr(self, "svg"):
from ..elements import SvgDocumentElement
self.svg = SvgDocumentElement()
if not hasattr(self, "arg_parser"):
self.arg_parser = ArgumentParser()
if not hasattr(self, "run"):
self.run = self.affect
@classmethod
def _deprecated(
cls, name, msg=_("{} is deprecated and should be removed"), stack=3
):
"""Give the user a warning about their extension using a deprecated API"""
_deprecated(
msg.format("Effect." + name, cls=cls.__module__ + "." + cls.__name__),
stack=stack,
)
@property
def OptionParser(self):
self._deprecated(
"OptionParser",
_(
"{} or `optparse` has been deprecated and replaced with `argparser`. "
"You must change `self.OptionParser.add_option` to "
"`self.arg_parser.add_argument`; the arguments are similar."
),
)
return self
def add_option(self, *args, **kw):
# Convert type string into type method as needed
if "type" in kw:
kw["type"] = {
"string": str,
"int": int,
"float": float,
"inkbool": utils.Boolean,
}.get(kw["type"])
if kw.get("action", None) == "store":
# Default store action not required, removed.
kw.pop("action")
args = [arg for arg in args if arg != ""]
self.arg_parser.add_argument(*args, **kw)
def effect(self):
self._deprecated(
"effect",
_(
"{} method is now a required method. It should "
"be created on {cls}, even if it does nothing."
),
)
@property
def current_layer(self):
self._deprecated(
"current_layer",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.get_current_layer()` instead."
),
)
return self.svg.get_current_layer()
@property
def view_center(self):
self._deprecated(
"view_center",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.get_center_position()` instead."
),
)
return self.svg.namedview.center
@property
def selected(self):
self._deprecated(
"selected",
_(
"{} is now a dict in the SvgDocumentElement class. "
"Use `self.svg.selected`."
),
)
return {elem.get("id"): elem for elem in self.svg.selected}
@property
def doc_ids(self):
self._deprecated(
"doc_ids",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.get_ids()` instead."
),
)
if self._doc_ids is None:
self._doc_ids = dict.fromkeys(self.svg.get_ids())
return self._doc_ids
def getselected(self):
self._deprecated("getselected", _("{} has been removed"))
def getElementById(self, eid):
self._deprecated(
"getElementById",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.getElementById(eid)` instead."
),
)
return self.svg.getElementById(eid)
def xpathSingle(self, xpath):
self._deprecated(
"xpathSingle",
_(
"{} is now a new method in the SvgDocumentElement class. "
"Use `self.svg.getElement(path)` instead."
),
)
return self.svg.getElement(xpath)
def getParentNode(self, node):
self._deprecated(
"getParentNode",
_("{} is no longer in use. Use the lxml `.getparent()` method instead."),
)
return node.getparent()
def getNamedView(self):
self._deprecated(
"getNamedView",
_(
"{} is now a property of the SvgDocumentElement class. "
"Use `self.svg.namedview` to access this element."
),
)
return self.svg.namedview
def createGuide(self, posX, posY, angle):
from ..elements import Guide
self._deprecated(
"createGuide",
_(
"{} is now a method of the namedview element object. "
"Use `self.svg.namedview.add(Guide().move_to(x, y, a))` instead."
),
)
return self.svg.namedview.add(Guide().move_to(posX, posY, angle))
def affect(self, args=sys.argv[1:], output=True): # pylint: disable=dangerous-default-value
# We need a list as the default value to preserve backwards compatibility
self._deprecated(
"affect", _("{} is now `Effect.run()`. The `output` argument has changed.")
)
self._args = args[-1:]
return self.run(args=args)
@property
def args(self):
self._deprecated("args", _("self.args[-1] is now self.options.input_file."))
return self._args
@property
def svg_file(self):
self._deprecated("svg_file", _("self.svg_file is now self.options.input_file."))
return self.options.input_file
def save_raw(self, ret):
# Derived class may implement "output()"
# Attention: 'cubify.py' implements __getattr__ -> hasattr(self, 'output')
# returns True
if hasattr(self.__class__, "output"):
self._deprecated("output", "Use `save()` or `save_raw()` instead.", stack=5)
return getattr(self, "output")()
return base.InkscapeExtension.save_raw(self, ret)
def uniqueId(self, old_id, make_new_id=True):
self._deprecated(
"uniqueId",
_(
"{} is now a method in the SvgDocumentElement class. "
" Use `self.svg.get_unique_id(old_id)` instead."
),
)
return self.svg.get_unique_id(old_id)
def getDocumentWidth(self):
self._deprecated(
"getDocumentWidth",
_(
"{} is now a property of the SvgDocumentElement class. "
"Use `self.svg.width` instead."
),
)
return self.svg.get("width")
def getDocumentHeight(self):
self._deprecated(
"getDocumentHeight",
_(
"{} is now a property of the SvgDocumentElement class. "
"Use `self.svg.height` instead."
),
)
return self.svg.get("height")
def getDocumentUnit(self):
self._deprecated(
"getDocumentUnit",
_(
"{} is now a property of the SvgDocumentElement class. "
"Use `self.svg.unit` instead."
),
)
return self.svg.unit
def unittouu(self, string):
self._deprecated(
"unittouu",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.unittouu(str)` instead."
),
)
return self.svg.unittouu(string)
def uutounit(self, val, unit):
self._deprecated(
"uutounit",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.uutounit(value, unit)` instead."
),
)
return self.svg.uutounit(val, unit)
def addDocumentUnit(self, value):
self._deprecated(
"addDocumentUnit",
_(
"{} is now a method in the SvgDocumentElement class. "
"Use `self.svg.add_unit(value)` instead."
),
)
return self.svg.add_unit(value)
class Effect(SvgThroughMixin, DeprecatedEffect, InkscapeExtension):
"""An Inkscape effect, takes SVG in and outputs SVG"""

View File

@@ -0,0 +1,237 @@
# coding=utf-8
#
# Copyright (C) 2018 - Martin Owens <doctormo@mgail.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-1301, USA.
#
"""
Provide some documentation to existing extensions about why they're failing.
"""
#
# We ignore a lot of pylint warnings here:
#
# pylint: disable=invalid-name,unused-argument,missing-docstring,too-many-public-methods
#
import os
import sys
import re
import warnings
import argparse
import cssselect
from ..transforms import Transform
from .. import utils
from .. import units
from ..elements._base import BaseElement, ShapeElement
from ..elements._selected import ElementList
from .meta import deprecate, _deprecated
from ..styles import ConditionalStyle, Style
from ..colors import Color
warnings.simplefilter("default")
# To load each of the deprecated sub-modules (the ones without a namespace)
# we will add the directory to our pythonpath so older scripts can find them
INKEX_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
SIMPLE_DIR = os.path.join(INKEX_DIR, "deprecated-simple")
if os.path.isdir(SIMPLE_DIR):
sys.path.append(SIMPLE_DIR)
class DeprecatedDict(dict):
@deprecate
def __getitem__(self, key):
return super().__getitem__(key)
@deprecate
def __iter__(self):
return super().__iter__()
# legacy inkex members
class lazyproxy:
"""Proxy, use as decorator on a function with provides the wrapped object.
The decorated function is called when a member is accessed on the proxy.
"""
def __init__(self, getwrapped):
"""
:param getwrapped: Callable which returns the wrapped object
"""
self._getwrapped = getwrapped
def __getattr__(self, name):
return getattr(self._getwrapped(), name)
def __call__(self, *args, **kwargs):
return self._getwrapped()(*args, **kwargs)
@lazyproxy
def localize():
_deprecated("inkex.localize was moved to inkex.localization.localize.", stack=3)
from ..localization import localize as wrapped
return wrapped
def are_near_relative(a, b, eps):
_deprecated(
"inkex.are_near_relative was moved to inkex.units.are_near_relative", stack=2
)
return units.are_near_relative(a, b, eps)
def debug(what):
_deprecated("inkex.debug was moved to inkex.utils.debug.", stack=2)
return utils.debug(what)
# legacy inkex members <= 0.48.x
def unittouu(string):
_deprecated(
"inkex.unittouu is now a method in the SvgDocumentElement class. "
"Use `self.svg.unittouu(str)` instead.",
stack=2,
)
return units.convert_unit(string, "px")
# optparse.Values.ensure_value
def ensure_value(self, attr, value):
_deprecated("Effect().options.ensure_value was removed.", stack=2)
if getattr(self, attr, None) is None:
setattr(self, attr, value)
return getattr(self, attr)
argparse.Namespace.ensure_value = ensure_value # type: ignore
@deprecate
def zSort(inNode, idList):
"""self.svg.get_z_selected()"""
sortedList = []
theid = inNode.get("id")
if theid in idList:
sortedList.append(theid)
for child in inNode:
if len(sortedList) == len(idList):
break
sortedList += zSort(child, idList)
return sortedList
# This can't be handled as a mixin class because of circular importing.
def description(self, value):
"""Use elem.desc = value"""
self.desc = value
BaseElement.description = deprecate(description, "1.1")
def composed_style(element: ShapeElement):
"""Calculate the final styles applied to this element
This function has been deprecated in favor of BaseElement.specified_style()"""
return element.specified_style()
ShapeElement.composed_style = deprecate(composed_style, "1.2")
def paint_order(selection: ElementList):
"""Use :func:`rendering_order`"""
return selection.rendering_order()
ElementList.paint_order = deprecate(paint_order, "1.2") # type: ignore
def transform_imul(self, matrix):
"""Use @= operator instead"""
return self.__imatmul__(matrix)
def transform_mul(self, matrix):
"""Use @ operator instead"""
return self.__matmul__(matrix)
Transform.__imul__ = deprecate(transform_imul, "1.2") # type: ignore
Transform.__mul__ = deprecate(transform_mul, "1.2") # type: ignore
def to_xpath(self):
"""Depending on whether you need to apply the rule to an invididual element
or find all matches in a subtree, use
.. code::
style.matches(element)
style.all_matches(subtree)
"""
return "|".join(self.to_xpaths())
def to_xpaths(self):
"""Depending on whether you need to apply the rule to an invididual element
or find all matches in a subtree, use
.. code::
style.matches(element)
style.all_matches(subtree)
"""
result = []
for rule in self.rules:
ret = (
cssselect.HTMLTranslator().selector_to_xpath(cssselect.parse(str(rule))[0])
+ " "
)
ret = re.compile(r"(::|\/)([a-z]+)(?=\W)(?!-)").sub(r"\1svg:\2", ret)
result.append(ret.strip())
return result
ConditionalStyle.to_xpath = deprecate(to_xpath, "1.4") # type: ignore
ConditionalStyle.to_xpaths = deprecate(to_xpaths, "1.4") # type: ignore
def apply_shorthands(self):
"""Apply all shorthands in this style. Shorthands are now simplified automatically,
so this method does nothing"""
Style.apply_shorthands = deprecate(apply_shorthands, "1.4") # type: ignore
def to_rgba(self, alpha=1.0):
"""
Opacity is now controlled via alpha property regardless of color space being used.
"""
ret = self.to_rgb()
ret.alpha = float(alpha)
return ret
Color.to_rgba = deprecate(to_rgba, "1.5") # type: ignore

View File

@@ -0,0 +1,109 @@
# coding=utf-8
#
# Copyright (C) 2018 - Martin Owens <doctormo@gmail.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-1301, USA.
#
"""
Deprecation functionality which does not require imports from Inkex.
"""
import os
import traceback
import warnings
from typing import Optional
try:
DEPRECATION_LEVEL = int(os.environ.get("INKEX_DEPRECATION_LEVEL", 1))
except ValueError:
DEPRECATION_LEVEL = 1
def _deprecated(msg, stack=2, level=DEPRECATION_LEVEL):
"""Internal method for raising a deprecation warning"""
if level > 1:
msg += " ; ".join(traceback.format_stack())
if level:
warnings.warn(msg, category=DeprecationWarning, stacklevel=stack + 1)
def deprecate(func, version: Optional[str] = None):
r"""Function decorator for deprecation functions which have a one-liner
equivalent in the new API. The one-liner has to passed as a string
to the decorator.
>>> @deprecate
>>> def someOldFunction(*args):
>>> '''Example replacement code someNewFunction('foo', ...)'''
>>> someNewFunction('foo', *args)
Or if the args API is the same:
>>> someOldFunction = deprecate(someNewFunction)
"""
def _inner(*args, **kwargs):
_deprecated(f"{func.__module__}.{func.__name__} -> {func.__doc__}", stack=2)
return func(*args, **kwargs)
_inner.__name__ = func.__name__
if func.__doc__:
if version is None:
_inner.__doc__ = "Deprecated -> " + func.__doc__
else:
_inner.__doc__ = f"""{func.__doc__}\n\n.. deprecated:: {version}\n"""
return _inner
class DeprecatedSvgMixin:
"""Mixin which adds deprecated API elements to the SvgDocumentElement"""
@property
def selected(self):
"""svg.selection"""
return self.selection
@deprecate
def set_selected(self, *ids):
r"""svg.selection.set(\*ids)"""
return self.selection.set(*ids)
@deprecate
def get_z_selected(self):
"""svg.selection.rendering_order()"""
return self.selection.rendering_order()
@deprecate
def get_selected(self, *types):
r"""svg.selection.filter(\*types).values()"""
return self.selection.filter(*types).values()
@deprecate
def get_selected_or_all(self, *types):
"""Set select_all = True in extension class"""
if not self.selection:
self.selection.set_all()
return self.selection.filter(*types)
@deprecate
def get_selected_bbox(self):
"""selection.bounding_box()"""
return self.selection.bounding_box()
@deprecate
def get_first_selected(self, *types):
r"""selection.filter(\*types).first() or [0] if you'd like an error"""
return self.selection.filter(*types).first()

View File

@@ -0,0 +1,56 @@
"""
Element based interface provides the bulk of features that allow you to
interact directly with the SVG xml interface.
See the documentation for each of the elements for details on how it works.
"""
from ._utils import addNS, NSS
from ._parser import SVG_PARSER, load_svg
from ._base import ShapeElement, BaseElement
from ._svg import SvgDocumentElement
from ._groups import Group, Layer, Anchor, Marker, ClipPath
from ._polygons import PathElement, Polyline, Polygon, Line, Rectangle, Circle, Ellipse
from ._text import (
FlowRegion,
FlowRoot,
FlowPara,
FlowDiv,
FlowSpan,
TextElement,
TextPath,
Tspan,
SVGfont,
FontFace,
Glyph,
MissingGlyph,
)
from ._use import Symbol, Use
from ._meta import (
Defs,
StyleElement,
Script,
Desc,
Title,
NamedView,
Guide,
Metadata,
ForeignObject,
Switch,
Grid,
Page,
)
from ._filters import (
Filter,
Pattern,
Mask,
Gradient,
LinearGradient,
RadialGradient,
PathEffect,
Stop,
MeshGradient,
MeshRow,
MeshPatch,
)
from ._image import Image

Some files were not shown because too many files have changed in this diff Show More