bundle: update (2026-01-18)
This commit is contained in:
33
extensions/km-hershey/deps/inkex/__init__.py
Normal file
33
extensions/km-hershey/deps/inkex/__init__.py
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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
567
extensions/km-hershey/deps/inkex/base.py
Normal file
567
extensions/km-hershey/deps/inkex/base.py
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
extensions/km-hershey/deps/inkex/bezier.py
Normal file
582
extensions/km-hershey/deps/inkex/bezier.py
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
extensions/km-hershey/deps/inkex/colors/__init__.py
Normal file
49
extensions/km-hershey/deps/inkex/colors/__init__.py
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 *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
295
extensions/km-hershey/deps/inkex/colors/color.py
Normal file
295
extensions/km-hershey/deps/inkex/colors/color.py
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
extensions/km-hershey/deps/inkex/colors/converters.py
Normal file
122
extensions/km-hershey/deps/inkex/colors/converters.py
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
extensions/km-hershey/deps/inkex/colors/spaces/__init__.py
Normal file
11
extensions/km-hershey/deps/inkex/colors/spaces/__init__.py
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
95
extensions/km-hershey/deps/inkex/colors/spaces/cms.py
Normal file
95
extensions/km-hershey/deps/inkex/colors/spaces/cms.py
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
extensions/km-hershey/deps/inkex/colors/spaces/cmyk.py
Normal file
81
extensions/km-hershey/deps/inkex/colors/spaces/cmyk.py
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
extensions/km-hershey/deps/inkex/colors/spaces/css.py
Normal file
139
extensions/km-hershey/deps/inkex/colors/spaces/css.py
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
extensions/km-hershey/deps/inkex/colors/spaces/hsl.py
Normal file
107
extensions/km-hershey/deps/inkex/colors/spaces/hsl.py
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
extensions/km-hershey/deps/inkex/colors/spaces/hsv.py
Normal file
88
extensions/km-hershey/deps/inkex/colors/spaces/hsv.py
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
extensions/km-hershey/deps/inkex/colors/spaces/named.py
Normal file
236
extensions/km-hershey/deps/inkex/colors/spaces/named.py
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
extensions/km-hershey/deps/inkex/colors/spaces/none.py
Normal file
55
extensions/km-hershey/deps/inkex/colors/spaces/none.py
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
extensions/km-hershey/deps/inkex/colors/spaces/rgb.py
Normal file
105
extensions/km-hershey/deps/inkex/colors/spaces/rgb.py
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
extensions/km-hershey/deps/inkex/colors/utils.py
Normal file
31
extensions/km-hershey/deps/inkex/colors/utils.py
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
extensions/km-hershey/deps/inkex/command.py
Normal file
347
extensions/km-hershey/deps/inkex/command.py
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
extensions/km-hershey/deps/inkex/css/__init__.py
Normal file
3
extensions/km-hershey/deps/inkex/css/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""CSS Processing module"""
|
||||
|
||||
from .compiler import CSSCompiler
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
483
extensions/km-hershey/deps/inkex/css/compiler.py
Normal file
483
extensions/km-hershey/deps/inkex/css/compiler.py
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
extensions/km-hershey/deps/inkex/css/parser.py
Normal file
548
extensions/km-hershey/deps/inkex/css/parser.py
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)})"
|
||||
@@ -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'
|
||||
|
||||
@@ -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."""
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
extensions/km-hershey/deps/inkex/deprecated-simple/ffgeom.py
Normal file
92
extensions/km-hershey/deps/inkex/deprecated-simple/ffgeom.py
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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)))
|
||||
@@ -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
extensions/km-hershey/deps/inkex/deprecated/__init__.py
Normal file
3
extensions/km-hershey/deps/inkex/deprecated/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import *
|
||||
from .meta import deprecate, _deprecated
|
||||
from .deprecatedeffect import DeprecatedEffect, Effect
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
304
extensions/km-hershey/deps/inkex/deprecated/deprecatedeffect.py
Normal file
304
extensions/km-hershey/deps/inkex/deprecated/deprecatedeffect.py
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
extensions/km-hershey/deps/inkex/deprecated/main.py
Normal file
237
extensions/km-hershey/deps/inkex/deprecated/main.py
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
extensions/km-hershey/deps/inkex/deprecated/meta.py
Normal file
109
extensions/km-hershey/deps/inkex/deprecated/meta.py
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
extensions/km-hershey/deps/inkex/elements/__init__.py
Normal file
56
extensions/km-hershey/deps/inkex/elements/__init__.py
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
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