initial commit

This commit is contained in:
2026-01-17 23:40:16 -05:00
commit 513afcb35a
220 changed files with 31164 additions and 0 deletions

49
deps/inkex/colors/__init__.py vendored Normal file
View File

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

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
deps/inkex/colors/color.py vendored Normal file
View File

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

122
deps/inkex/colors/converters.py vendored Normal file
View File

@@ -0,0 +1,122 @@
# coding=utf-8
#
# Copyright (C) 2018-2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
Basic color errors and common functions
"""
from collections import defaultdict
from typing import Dict, List, Callable
ConverterFunc = Callable[[float], List[float]]
class Converters:
"""
Record how colors can be converted between different spaces and provides
a way to path-find between multiple step conversions.
"""
links: Dict[str, Dict[str, ConverterFunc]] = defaultdict(dict)
chains: Dict[str, List[List[str]]] = {}
@classmethod
def add_space(cls, color_cls):
"""
Records the stated links between this class and other color spaces
"""
for name, func in color_cls.__dict__.items():
if not name.startswith("convert_"):
continue
_, direction, space = name.split("_", 2)
from_name = color_cls.name if direction == "to" else space
to_name = color_cls.name if direction == "from" else space
if from_name != to_name:
if not isinstance(func, staticmethod):
raise TypeError(f"Method '{name}' must be a static method.")
cls.links[from_name][to_name] = func.__func__
@classmethod
def get_chain(cls, source, target):
"""
Get a chain of conversions between two color spaces, if possible.
"""
def build_chains(chains, space):
new_chains = []
for chain in chains:
for hop in cls.links[space]:
if hop not in chain:
new_chains += build_chains([chain + [hop]], hop)
return chains + new_chains
if source not in cls.chains:
cls.chains[source] = build_chains([[source]], source)
chosen = None
for chain in cls.chains[source] or ():
if chain[-1] == target and (not chosen or len(chain) < len(chosen)):
chosen = chain
return chosen
@classmethod
def find_converter(cls, source, target):
"""
Find a way to convert from source to target using any conversion functions.
Will hop from one space to another if needed.
"""
func = None
# Passthough
if source == target:
return lambda self: self
if func is None:
chain = cls.get_chain(source.name, target.name)
if chain:
return cls.generate_converter(chain, source, target)
# Returning a function means we only run this function once, even when not found
def _error(self):
raise NotImplementedError(
f"Color space {source} can not be converted to {target}."
)
return _error
@classmethod
def generate_converter(cls, chain, source_cls, target_cls):
"""
Put together a function that can do every step of the chain of conversions
"""
# Build a list of functions to run
funcs = [cls.links[a][b] for a, b in zip(chain, chain[1:])]
funcs.insert(0, source_cls.to_units)
funcs.append(target_cls.from_units)
def _inner(values):
if hasattr(values, "alpha") and values.alpha is not None:
values = list(values) + [values.alpha]
for func in funcs:
values = func(*values)
return target_cls(values)
return _inner

11
deps/inkex/colors/spaces/__init__.py vendored Normal file
View File

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

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
deps/inkex/colors/spaces/cms.py vendored Normal file
View File

@@ -0,0 +1,95 @@
# coding=utf-8
#
# Copyright (C) 2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
SVG icc-color parser
"""
from ..color import Color, AlphaNotAllowed, ColorError, round_by_type
from .css import CssColor
from .rgb import ColorRGB
class ColorCMS(CssColor, AlphaNotAllowed):
"""
Parse and print SVG icc-color objects into their values and the fallback RGB
"""
name = "cms"
css_func = "icc-color"
channels = ()
scales = ()
def __init__(self, values, icc_profile=None, fallback=None):
if isinstance(values, str):
if values.strip().startswith("#") and " " in values:
fallback, values = values.split(" ", 1)
fallback = Color(fallback)
icc_profile, values = self.parse_css_color(values)
if icc_profile is None:
raise ColorError("CMS Color requires an icc color profile name.")
self.icc_profile = icc_profile
self.fallback_rgb = fallback
super().__init__(values)
def __str__(self) -> str:
values = self.css_join.join([f"{v:g}" for v in self.get_css_values()])
fallback = str(ColorRGB(self.fallback_rgb)) + " " if self.fallback_rgb else ""
return f"{fallback}{self.css_func}({self.icc_profile}, {values})"
@classmethod
def can_parse(cls, string: str) -> bool:
# Custom detection because of RGB fallback prefix
return "icc-color" in string.replace("(", " ").split()
@classmethod
def constrain(cls, index, value):
return min(max(round_by_type(float, value), 0.0), 1.0)
@classmethod
def scale_up(cls, index, value):
return value # All cms values are already 0.0 to 1.0
@classmethod
def scale_down(cls, index, value):
return value # All cms values are already 0.0 to 1.0
@staticmethod
def convert_to_rgb(*data):
"""Catch attempted conversions to rgb"""
raise NotImplementedError("Can not convert to RGB from icc color")
@staticmethod
def convert_from_rgb(*data):
"""Catch attempted conversions from rgb"""
raise NotImplementedError("Can not convert from RGB to icc color")
# This is research code for a future developer to use. We already use PIL and this will
# allow icc colors to be converted in python. This isn't needed right now, so this work
# will be left undone.
# @staticmethod
# def convert_to_rgb():
# from PIL import Image, ImageCms
# pixel = Image.fromarray([[int(r * 255), int(g * 255), int(b * 255)]], 'RGB')
# transform = ImageCms.buildTransform(sRGB_profile, self.this_profile, "RGB",
# self.this_profile_mode, self.this_rendering_intent, 0)
# transform.apply_in_place(pixel)
# return [p / 255 for p in pixel[0]]

81
deps/inkex/colors/spaces/cmyk.py vendored Normal file
View File

@@ -0,0 +1,81 @@
# coding=utf-8
#
# Copyright (C) 2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# pylint: disable=W0223
"""
DeviceCMYK Color Space
"""
from .css import CssColorModule4
class ColorDeviceCMYK(CssColorModule4):
"""
Parse the device-cmyk CSS Color Module 4 format.
Note that this format is NOT true CMYK as you might expect in a printer and
is instead is an aproximation of the intended ink levels if this was converted
into a real CMYK color profile using a color management system.
"""
name = "cmyk"
channels = ("cyan", "magenta", "yellow", "black")
scales = ((0, 100), (0, 100), (0, 100), (0, 100), (0.0, 1.0))
css_either_prefix = "device-cmyk"
cyan = property(
lambda self: self[0], lambda self, value: self.__setitem__(0, value)
)
magenta = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
yellow = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
black = property(
lambda self: self[3], lambda self, value: self.__setitem__(3, value)
)
@staticmethod
def convert_to_rgb(cyan, magenta, yellow, black, *alpha):
"""
Convert a set of Device-CMYK identities into RGB
"""
white = 1.0 - black
return [
1.0 - min((1.0, cyan * white + black)),
1.0 - min((1.0, magenta * white + black)),
1.0 - min((1.0, yellow * white + black)),
] + list(alpha)
@staticmethod
def convert_from_rgb(red, green, blue, *alpha):
"""
Convert RGB into Device-CMYK
"""
white = max((red, green, blue))
black = 1.0 - white
return [
# Each channel is it's color chart oposite (cyan->red)
# with a bit of white removed.
(white and (1.0 - red - black) / white or 0.0),
(white and (1.0 - green - black) / white or 0.0),
(white and (1.0 - blue - black) / white or 0.0),
black,
] + list(alpha)

139
deps/inkex/colors/spaces/css.py vendored Normal file
View File

@@ -0,0 +1,139 @@
# coding=utf-8
#
# Copyright (C) 2018-2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# pylint: disable=W0223
"""
Parsing CSS elements from colors
"""
from typing import Optional, Union
from ..color import Color, ColorError, ColorIdError
class CssColor(Color):
"""
A Color which is always parsed and printed from a css format.
"""
# A list of css prefixes which ar valid for this space
css_noalpha_prefix: Optional[str] = None
css_alpha_prefix: Optional[str] = None
css_either_prefix: Optional[str] = None
# Some CSS formats require commas, others do not
css_join: str = ", "
css_join_alpha: str = ", "
css_func = "color"
def __str__(self):
values = self.css_join.join([f"{v:g}" for v in self.get_css_values()])
prefix = self.css_noalpha_prefix or self.css_either_prefix
if self.alpha is not None:
# Alpha is stored as a percent for clarity
alpha = int(self.alpha * 100)
values += self.css_join_alpha + f"{alpha}%"
if not self.css_either_prefix:
prefix = self.css_alpha_prefix
if prefix is None:
raise ColorError(f"Can't encode color {self.name} into CSS color format.")
return f"{prefix}({values})"
@classmethod
def can_parse(cls, string: str):
string = string.replace(" ", "")
if "(" not in string or ")" not in string:
return False
for prefix in (
cls.css_noalpha_prefix,
cls.css_alpha_prefix,
cls.css_either_prefix,
):
if prefix and (prefix + "(" in string or "color(" + prefix in string):
return True
return False
def __init__(self, value, alpha=None):
if isinstance(value, str):
prefix, values = self.parse_css_color(value)
has_alpha = (
self.channels is not None and len(values) == len(self.channels) + 1
)
if prefix == self.css_noalpha_prefix or (
prefix == self.css_either_prefix and not has_alpha
):
super().__init__(values)
elif prefix == self.css_alpha_prefix or (
prefix == self.css_either_prefix and has_alpha
):
super().__init__(values, values.pop())
else:
raise ColorError(f"Could not parse {self.name} css color: '{value}'")
else:
super().__init__(value, alpha=alpha)
@classmethod
def parse_css_color(cls, value):
"""Parse a css string into a list of values and it's color space prefix"""
prefix, values = value.lower().strip().strip(")").split("(")
# Some css formats use commas, others do not
if "," in cls.css_join:
values = values.replace(",", " ")
if "/" in cls.css_join_alpha:
values = values.replace("/", " ")
# Split values by spaces
values = values.split()
prefix = prefix.strip()
if prefix == cls.css_func:
prefix = values.pop(0)
if prefix == "url":
raise ColorIdError("Can not parse url as if it was a color.")
return prefix, [cls.parse_css_value(i, v) for i, v in enumerate(values)]
def get_css_values(self):
"""Return a list of values used for css string output"""
return self
@classmethod
def parse_css_value(cls, index, value) -> Union[int, float]:
"""Parse a CSS value such as 100%, 360 or 0.4"""
if cls.scales and index >= len(cls.scales):
raise ValueError("Can't add any more values to color.")
if isinstance(value, str):
value = value.strip()
if value.endswith("%"):
value = float(value.strip("%")) / 100
elif "." in value:
value = float(value)
else:
value = int(value)
if isinstance(value, float) and value <= 1.0:
value = cls.scale_up(index, value)
return cls.constrain(index, value)
class CssColorModule4(CssColor):
"""Tweak the css parser for CSS Module Four formating"""
css_join = " "
css_join_alpha = " / "

107
deps/inkex/colors/spaces/hsl.py vendored Normal file
View File

@@ -0,0 +1,107 @@
# coding=utf-8
#
# Copyright (C) 2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# pylint: disable=W0223
"""
HSL Color Space
"""
from .css import CssColor
class ColorHSL(CssColor):
"""
Parse the HSL CSS Module Module 3 format.
"""
name = "hsl"
channels = ("hue", "saturation", "lightness")
scales = ((0, 360, True), (0, 100), (0, 100), (0.0, 1.0))
css_noalpha_prefix = "hsl"
css_alpha_prefix = "hsla"
hue = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
saturation = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
lightness = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
@staticmethod
def convert_from_rgb(red, green, blue, alpha=None):
"""RGB to HSL colour conversion"""
rgb_max = max(red, green, blue)
rgb_min = min(red, green, blue)
delta = rgb_max - rgb_min
hsl = [0.0, 0.0, (rgb_max + rgb_min) / 2.0]
if delta != 0:
if hsl[2] <= 0.5:
hsl[1] = delta / (rgb_max + rgb_min)
else:
hsl[1] = delta / (2 - rgb_max - rgb_min)
if red == rgb_max:
hsl[0] = (green - blue) / delta
elif green == rgb_max:
hsl[0] = 2.0 + (blue - red) / delta
elif blue == rgb_max:
hsl[0] = 4.0 + (red - green) / delta
hsl[0] /= 6.0
if hsl[0] < 0:
hsl[0] += 1
if hsl[0] > 1:
hsl[0] -= 1
if alpha is not None:
hsl.append(alpha)
return hsl
@staticmethod
def convert_to_rgb(hue, sat, light, *alpha):
"""HSL to RGB Color Conversion"""
if sat == 0:
return [light, light, light] # Gray
if light < 0.5:
val2 = light * (1 + sat)
else:
val2 = light + sat - light * sat
val1 = 2 * light - val2
ret = [
_hue_to_rgb(val1, val2, hue * 6 + 2.0),
_hue_to_rgb(val1, val2, hue * 6),
_hue_to_rgb(val1, val2, hue * 6 - 2.0),
]
return ret + list(alpha)
def _hue_to_rgb(val1, val2, hue):
if hue < 0:
hue += 6.0
if hue > 6:
hue -= 6.0
if hue < 1:
return val1 + (val2 - val1) * hue
if hue < 3:
return val2
if hue < 4:
return val1 + (val2 - val1) * (4 - hue)
return val1

88
deps/inkex/colors/spaces/hsv.py vendored Normal file
View File

@@ -0,0 +1,88 @@
# coding=utf-8
#
# Copyright (C) 2024 Jonathan Neuhauser
# 2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# pylint: disable=W0223
"""
HSV Color Space
"""
from .css import CssColorModule4
class ColorHSV(CssColorModule4):
"""
Parse the HWB CSS Color Module 4 format and retain as HSV values.
"""
name = "hsv"
channels = ("hue", "saturation", "value")
scales = ((0, 360, True), (0, 100), (0, 100), (0.0, 1.0))
# We use HWB to store HSV as this makes the most sense to Inkscape
css_either_prefix = "hwb"
hue = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
saturation = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
value = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
@classmethod
def parse_css_color(cls, value):
"""Parsing HWB as if it was HSV for css input"""
prefix, values = super().parse_css_color(value)
# See https://en.wikipedia.org/wiki/HWB_color_model#Converting_to_and_from_HSV
values[1] /= 100
values[2] /= 100
scale = values[1] + values[2]
if scale > 1.0:
values[1] /= scale
values[2] /= scale
values[1] = int(
(values[2] == 1.0 and 0.0 or (1.0 - (values[1] / (1.0 - values[2])))) * 100
)
values[2] = int((1.0 - values[2]) * 100)
return prefix, values
def get_css_values(self):
"""Convert our HSV values into HWB for css output"""
values = list(self)
values[1] = (100 - values[1]) * (values[2] / 100)
values[2] = 100 - values[2]
return values
@staticmethod
def convert_to_hsl(hue, saturation, value, *alpha):
"""Conversion according to
https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
.. versionadded:: 1.5"""
lum = value * (1 - saturation / 2)
sat = 0 if lum in (0, 1) else (value - lum) / min(lum, 1 - lum)
return [hue, sat, lum] + list(alpha)
@staticmethod
def convert_from_hsl(hue, saturation, lightness, *alpha):
"""Convertion according to Inkscape C++ codebase
.. versionadded:: 1.5"""
val = lightness + saturation * min(lightness, 1 - lightness)
sat = 0 if val == 0 else 2 * (1 - lightness / val)
return [hue, sat, val] + list(alpha)

236
deps/inkex/colors/spaces/named.py vendored Normal file
View File

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

55
deps/inkex/colors/spaces/none.py vendored Normal file
View File

@@ -0,0 +1,55 @@
# coding=utf-8
#
# Copyright (C) 2021 Jonathan Neuhauser
# 2020 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# pylint: disable=W0223
"""
An empty color for 'none'
"""
from ..color import Color, AlphaNotAllowed
class ColorNone(Color, AlphaNotAllowed):
"""A special color for 'none' colors"""
name = "none"
# Override opacity since none can not have opacity
default_alpha = 0.0
def __init__(self, value=None):
pass
def __str__(self) -> str:
return "none"
@classmethod
def can_parse(cls, string: str) -> bool:
"""Returns true if this is the word 'none'"""
return string == "none"
@staticmethod
def convert_to_rgb(*_):
"""Converting to RGB means transparent black"""
return [0, 0, 0, 0]
@staticmethod
def convert_from_rgb(*_):
"""Converting from RGB means throwing out all data"""
return []

105
deps/inkex/colors/spaces/rgb.py vendored Normal file
View File

@@ -0,0 +1,105 @@
# coding=utf-8
#
# Copyright (C) 2024, Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
RGB Colors
"""
from ..color import ColorError
from .css import CssColor
class ColorRGB(CssColor):
"""
Parse multiple versions of RGB from CSS module and standard hex formats.
"""
name = "rgb"
channels = ("red", "green", "blue")
scales = ((0, 255), (0, 255), (0, 255), (0.0, 1.0))
css_noalpha_prefix = "rgb"
css_alpha_prefix = "rgba"
red = property(lambda self: self[0], lambda self, value: self.__setitem__(0, value))
green = property(
lambda self: self[1], lambda self, value: self.__setitem__(1, value)
)
blue = property(
lambda self: self[2], lambda self, value: self.__setitem__(2, value)
)
@classmethod
def can_parse(cls, string: str) -> bool:
return "icc" not in string and (
string.startswith("#")
or string.lstrip("-").isdigit()
or super().can_parse(string)
)
def __init__(self, value, alpha=None):
# Not CSS, but inkscape, some old color values stores as 32bit int strings
if isinstance(value, str) and value.lstrip("-").isdigit():
value = int(value)
if isinstance(value, int):
super().__init__(
[
((value >> 24) & 255), # red
((value >> 16) & 255), # green
((value >> 8) & 255), # blue
((value & 255) / 255.0),
]
) # opacity
elif isinstance(value, str) and value.startswith("#") and " " not in value:
if len(value) == 4: # (css: #rgb -> #rrggbb)
# pylint: disable=consider-using-f-string
value = "#{1}{1}{2}{2}{3}{3}".format(*value)
elif len(value) == 5: # (css: #rgba -> #rrggbbaa)
# pylint: disable=consider-using-f-string
value = "#{1}{1}{2}{2}{3}{3}{4}{4}".format(*value)
# Convert hex to integers
try:
values = [int(value[i : i + 2], 16) for i in range(1, len(value), 2)]
if len(values) == 4:
values[3] /= 255
super().__init__(values)
except ValueError as error:
raise ColorError(f"Bad RGB hex color value '{value}'") from error
else:
super().__init__(value, alpha=alpha)
def __str__(self) -> str:
if self.alpha is not None:
return super().__str__()
if len(self) < len(self.channels):
raise ColorError(
f"Incorrect number of channels for Color Space {self.name}"
)
# Always hex values when outputting color
return "#{0:02x}{1:02x}{2:02x}".format(*(int(v) for v in self)) # pylint: disable=consider-using-f-string
def __int__(self) -> int:
return (
(self[0] << 24)
+ (self[1] << 16)
+ (self[2] << 8)
+ int((self.alpha or 1.0) * 255)
)

31
deps/inkex/colors/utils.py vendored Normal file
View File

@@ -0,0 +1,31 @@
# coding=utf-8
#
# Copyright (C) 2018-2024 Martin Owens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
Utilities for color support
"""
from .color import Color, ColorError
def is_color(color):
"""Determine if it is a color that we can use. If not, leave it unchanged."""
try:
return bool(Color(color))
except ColorError:
return False