initial commit
This commit is contained in:
33
deps/inkex/__init__.py
vendored
Normal file
33
deps/inkex/__init__.py
vendored
Normal 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.
|
||||
BIN
deps/inkex/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/base.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/base.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/base.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/base.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/bezier.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/bezier.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/bezier.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/bezier.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/command.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/command.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/command.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/command.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/extensions.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/extensions.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/extensions.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/extensions.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/localization.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/localization.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/localization.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/localization.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/properties.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/properties.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/properties.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/properties.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/styles.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/styles.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/styles.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/styles.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/transforms.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/transforms.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/transforms.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/transforms.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/units.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/units.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/units.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/units.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/utils.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/utils.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/__pycache__/utils.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/__pycache__/utils.cpython-313.pyc
vendored
Normal file
Binary file not shown.
567
deps/inkex/base.py
vendored
Normal file
567
deps/inkex/base.py
vendored
Normal 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
|
||||
582
deps/inkex/bezier.py
vendored
Normal file
582
deps/inkex/bezier.py
vendored
Normal 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
|
||||
49
deps/inkex/colors/__init__.py
vendored
Normal file
49
deps/inkex/colors/__init__.py
vendored
Normal 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 *
|
||||
BIN
deps/inkex/colors/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/color.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/color.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/color.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/color.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/converters.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/converters.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/converters.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/converters.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/utils.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/utils.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/__pycache__/utils.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/__pycache__/utils.cpython-313.pyc
vendored
Normal file
Binary file not shown.
295
deps/inkex/colors/color.py
vendored
Normal file
295
deps/inkex/colors/color.py
vendored
Normal 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)
|
||||
122
deps/inkex/colors/converters.py
vendored
Normal file
122
deps/inkex/colors/converters.py
vendored
Normal 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
|
||||
11
deps/inkex/colors/spaces/__init__.py
vendored
Normal file
11
deps/inkex/colors/spaces/__init__.py
vendored
Normal 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
|
||||
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cms.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/cmyk.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/css.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsl.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/hsv.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/named.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/none.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/colors/spaces/__pycache__/rgb.cpython-313.pyc
vendored
Normal file
Binary file not shown.
95
deps/inkex/colors/spaces/cms.py
vendored
Normal file
95
deps/inkex/colors/spaces/cms.py
vendored
Normal 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]]
|
||||
81
deps/inkex/colors/spaces/cmyk.py
vendored
Normal file
81
deps/inkex/colors/spaces/cmyk.py
vendored
Normal 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)
|
||||
139
deps/inkex/colors/spaces/css.py
vendored
Normal file
139
deps/inkex/colors/spaces/css.py
vendored
Normal 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 = " / "
|
||||
107
deps/inkex/colors/spaces/hsl.py
vendored
Normal file
107
deps/inkex/colors/spaces/hsl.py
vendored
Normal 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
|
||||
88
deps/inkex/colors/spaces/hsv.py
vendored
Normal file
88
deps/inkex/colors/spaces/hsv.py
vendored
Normal 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)
|
||||
236
deps/inkex/colors/spaces/named.py
vendored
Normal file
236
deps/inkex/colors/spaces/named.py
vendored
Normal 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)
|
||||
55
deps/inkex/colors/spaces/none.py
vendored
Normal file
55
deps/inkex/colors/spaces/none.py
vendored
Normal 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 []
|
||||
105
deps/inkex/colors/spaces/rgb.py
vendored
Normal file
105
deps/inkex/colors/spaces/rgb.py
vendored
Normal 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)
|
||||
)
|
||||
31
deps/inkex/colors/utils.py
vendored
Normal file
31
deps/inkex/colors/utils.py
vendored
Normal 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
|
||||
347
deps/inkex/command.py
vendored
Normal file
347
deps/inkex/command.py
vendored
Normal 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
|
||||
3
deps/inkex/css/__init__.py
vendored
Normal file
3
deps/inkex/css/__init__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"""CSS Processing module"""
|
||||
|
||||
from .compiler import CSSCompiler
|
||||
BIN
deps/inkex/css/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/compiler.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/compiler.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/compiler.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/compiler.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/parser.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/parser.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/css/__pycache__/parser.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/css/__pycache__/parser.cpython-313.pyc
vendored
Normal file
Binary file not shown.
483
deps/inkex/css/compiler.py
vendored
Normal file
483
deps/inkex/css/compiler.py
vendored
Normal 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()
|
||||
548
deps/inkex/css/parser.py
vendored
Normal file
548
deps/inkex/css/parser.py
vendored
Normal 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)})"
|
||||
4
deps/inkex/deprecated-simple/README.rst
vendored
Normal file
4
deps/inkex/deprecated-simple/README.rst
vendored
Normal 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'
|
||||
|
||||
46
deps/inkex/deprecated-simple/bezmisc.py
vendored
Normal file
46
deps/inkex/deprecated-simple/bezmisc.py
vendored
Normal 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."""
|
||||
)
|
||||
25
deps/inkex/deprecated-simple/cspsubdiv.py
vendored
Normal file
25
deps/inkex/deprecated-simple/cspsubdiv.py
vendored
Normal 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)
|
||||
52
deps/inkex/deprecated-simple/cubicsuperpath.py
vendored
Normal file
52
deps/inkex/deprecated-simple/cubicsuperpath.py
vendored
Normal 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)
|
||||
92
deps/inkex/deprecated-simple/ffgeom.py
vendored
Normal file
92
deps/inkex/deprecated-simple/ffgeom.py
vendored
Normal 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)
|
||||
80
deps/inkex/deprecated-simple/run_command.py
vendored
Normal file
80
deps/inkex/deprecated-simple/run_command.py
vendored
Normal 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
|
||||
68
deps/inkex/deprecated-simple/simplepath.py
vendored
Normal file
68
deps/inkex/deprecated-simple/simplepath.py
vendored
Normal 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()
|
||||
55
deps/inkex/deprecated-simple/simplestyle.py
vendored
Normal file
55
deps/inkex/deprecated-simple/simplestyle.py
vendored
Normal 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)))
|
||||
122
deps/inkex/deprecated-simple/simpletransform.py
vendored
Normal file
122
deps/inkex/deprecated-simple/simpletransform.py
vendored
Normal 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)
|
||||
3
deps/inkex/deprecated/__init__.py
vendored
Normal file
3
deps/inkex/deprecated/__init__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import *
|
||||
from .meta import deprecate, _deprecated
|
||||
from .deprecatedeffect import DeprecatedEffect, Effect
|
||||
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/deprecatedeffect.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/main.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/main.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/main.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/main.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/deprecated/__pycache__/meta.cpython-313.pyc
vendored
Normal file
Binary file not shown.
304
deps/inkex/deprecated/deprecatedeffect.py
vendored
Normal file
304
deps/inkex/deprecated/deprecatedeffect.py
vendored
Normal 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"""
|
||||
237
deps/inkex/deprecated/main.py
vendored
Normal file
237
deps/inkex/deprecated/main.py
vendored
Normal 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
|
||||
109
deps/inkex/deprecated/meta.py
vendored
Normal file
109
deps/inkex/deprecated/meta.py
vendored
Normal 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()
|
||||
56
deps/inkex/elements/__init__.py
vendored
Normal file
56
deps/inkex/elements/__init__.py
vendored
Normal 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
|
||||
BIN
deps/inkex/elements/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/elements/__pycache__/__init__.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/elements/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/elements/__pycache__/__init__.cpython-313.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/elements/__pycache__/_base.cpython-312.pyc
vendored
Normal file
BIN
deps/inkex/elements/__pycache__/_base.cpython-312.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/inkex/elements/__pycache__/_base.cpython-313.pyc
vendored
Normal file
BIN
deps/inkex/elements/__pycache__/_base.cpython-313.pyc
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user