Loading inkex/colors.py +9 −1 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ Basic color controls """ from .utils import PY3 from .tween import interpcoord # All the names that get added to the inkex API itself. __all__ = ('Color', 'ColorError', 'ColorIdError') Loading Loading @@ -297,7 +298,7 @@ class Color(list): return 'rgb', None if color.startswith('url('): raise ColorIdError("Gradient other referenced element id.") raise ColorIdError("Color references other element id, e.g. a gradient") # Next handle short colors (css: #abc -> #aabbcc) if color.startswith('#'): Loading Loading @@ -409,6 +410,13 @@ class Color(list): return Color() return Color(COLOR_SVG.get(str(self), str(self))) def interpolate(self, other, fraction): """Iterpolate two colours by the given fraction""" return Color( [interpcoord(c1, c2, fraction) for (c1, c2) in zip(self.to_floats(), other.to_floats())] ) def rgb_to_hsl(red, green, blue): """RGB to HSL colour conversion""" Loading inkex/elements/__init__.py +3 −3 Original line number Diff line number Diff line Loading @@ -14,5 +14,5 @@ from ._text import FlowRegion, FlowRoot, FlowPara, FlowDiv, FlowSpan, TextElemen from ._use import Symbol, Use from ._meta import Defs, StyleElement, Script, Desc, Title, NamedView, Guide, \ Metadata, ForeignObject, Switch, Grid from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, PathEffect from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, PathEffect, Stop from ._image import Image inkex/elements/_base.py +6 −0 Original line number Diff line number Diff line Loading @@ -333,6 +333,12 @@ class BaseElement(etree.ElementBase): if self.getparent() is not None: self.getparent().remove(self) def remove_all(self, *types): """Remove all children or child types""" for child in self: if not types or isinstance(child, types): self.remove(child) def replace_with(self, elem): """Replace this element with the given element""" self.addnext(elem) Loading inkex/elements/_filters.py +101 −1 Original line number Diff line number Diff line Loading @@ -24,10 +24,13 @@ Element interface for patterns, filters, gradients and path effects. """ from lxml import etree from copy import deepcopy from ..utils import addNS from ..transforms import Transform from ..tween import interpcoord, interp from ..styles import Style from ._base import BaseElement class Filter(BaseElement): Loading Loading @@ -92,6 +95,24 @@ class Filter(BaseElement): tag_name = 'feTurbulence' class Stop(BaseElement): tag_name = 'stop' @property def offset(self): return self.get('offset') @offset.setter def offset(self, number): self.set('offset', number) def interpolate(self, other, fraction): newstop = Stop() newstop.style = self.style.interpolate(other.style, fraction) newstop.offset = interpcoord(float(self.offset), float(other.offset), fraction) return newstop class Pattern(BaseElement): """Pattern element which is used in the def to control repeating fills""" tag_name = 'pattern' Loading @@ -102,14 +123,93 @@ class Gradient(BaseElement): """A gradient instruction usually in the defs""" WRAPPED_ATTRS = BaseElement.WRAPPED_ATTRS + (('gradientTransform', Transform),) orientation_attributes = () @property def stops(self): # type: () -> List[Stop] """Return an ordered list of own or linked stop nodes""" gradcolor = self.href if isinstance(self.href, LinearGradient) else self return sorted(gradcolor, key=lambda x: float(x.offset)) @property def stop_offsets(self): # type: () -> List[float] """Return a list of own or linked stop offsets""" return [child.offset for child in self.stops] @property def stop_styles(self): # type: () -> List[Style] """Return a list of own or linked offset styles""" return [child.style for child in self.stops] def remove_orientation(self): """Remove all orientation attributes from this element""" for attr in self.orientation_attributes: self.pop(attr) def interpolate(self, other, fraction): # type: (LinearGradient, float) -> LinearGradient """Interpolate with another gradient.""" if self.tag_name != other.tag_name: return self newgrad = self.copy() # interpolate transforms newtransform = self.gradientTransform.interpolate(other.gradientTransform, fraction) newgrad.gradientTransform = newtransform # interpolate orientation for attr in self.orientation_attributes: newattr = interpcoord(float(self.get(attr)), float(other.get(attr)), fraction) newgrad.set(attr, newattr) # interpolate stops if self.href is not None and self.href is other.href: # both gradients link to the same stops pass else: # gradients might have different stops newoffsets = sorted(self.stop_offsets + other.stop_offsets[1:-1]) sstops = interp(self.stop_offsets, self.stops, newoffsets) ostops = interp(other.stop_offsets, other.stops, newoffsets) newstops = [s1.interpolate(s2, fraction) for s1, s2 in zip(sstops, ostops)] newgrad.remove_all(Stop) newgrad.add(*newstops) return newgrad def stops_and_orientation(self): """Return a copy of all the stops in this gradient""" stops = self.copy() stops.remove_orientation() orientation = self.copy() orientation.remove_all(Stop) return stops, orientation class LinearGradient(Gradient): tag_name = 'linearGradient' orientation_attributes = ('x1', 'y1', 'x2', 'y2') def apply_transform(self): # type: () -> None """Apply transform to orientation points and set it to identity.""" trans = self.pop('gradientTransform') p1 = (float(self.get('x1')), float(self.get('y1'))) p2 = (float(self.get('x2')), float(self.get('y2'))) p1t = trans.apply_to_point(p1) p2t = trans.apply_to_point(p2) self.update(x1=p1t[0], y1=p1t[1], x2=p2t[0], y2=p2t[1]) class RadialGradient(Gradient): tag_name = 'radialGradient' orientation_attributes = ('cx', 'cy', 'fx', 'fy', 'r') def apply_transform(self): # type: () -> None """Apply transform to orientation points and set it to identity.""" trans = self.pop('gradientTransform') p1 = (float(self.get('cx')), float(self.get('cy'))) p2 = (float(self.get('fx')), float(self.get('fy'))) p1t = trans.apply_to_point(p1) p2t = trans.apply_to_point(p2) self.update(cx=p1t[0], cy=p1t[1], fx=p2t[0], fy=p2t[1]) class PathEffect(BaseElement): """Inkscape LPE element""" Loading inkex/styles.py +36 −2 Original line number Diff line number Diff line Loading @@ -25,7 +25,8 @@ import re from collections import OrderedDict from .utils import PY3 from .colors import Color from .colors import Color, ColorIdError from .tween import interpcoord, interpunit if PY3: unicode = str # pylint: disable=redefined-builtin,invalid-name Loading Loading @@ -72,7 +73,8 @@ class Classes(list): class Style(OrderedDict): """A list of style directives""" color_props = ('stroke', 'fill', 'stop-color', 'flood-color', 'lighting-color') opacity_props = ('stroke-opacity', 'fill-opacity', 'opacity') opacity_props = ('stroke-opacity', 'fill-opacity', 'opacity', 'stop-opacity') unit_props = ('stroke-width') def __init__(self, style=None, callback=None, **kw): # This callback is set twice because this is 'pre-initial' data (no callback) Loading Loading @@ -170,6 +172,38 @@ class Style(OrderedDict): if value == 'url(#{})'.format(old_id): self[name] = 'url(#{})'.format(new_id) def interpolate_prop(self, other, fraction, prop, svg=None): """Interpolate specific property.""" a1 = self[prop] a2 = other.get(prop, None) if a2 is None: val = a1 else: if prop in self.color_props: if isinstance(a1, Color): val = a1.interpolate(Color(a2), fraction) elif a1.startswith('url(') or a2.startswith('url('): # gradient requires changes to the whole svg # and needs to be handled externally val = a1 else: val = Color(a1).interpolate(Color(a2), fraction) elif prop in self.opacity_props: val = interpcoord(float(a1), float(a2), fraction) elif prop in self.unit_props: val = interpunit(a1, a2, fraction) else: val = a1 return val def interpolate(self, other, fraction): # type: (Style, float, Optional[str], Optional[str]) -> Style """Interpolate all properties.""" style = Style() for prop, value in self.items(): style[prop] = self.interpolate_prop(other, fraction, prop) return style class AttrFallbackStyle(object): """ A container for a style and an element that may have competing styles Loading Loading
inkex/colors.py +9 −1 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ Basic color controls """ from .utils import PY3 from .tween import interpcoord # All the names that get added to the inkex API itself. __all__ = ('Color', 'ColorError', 'ColorIdError') Loading Loading @@ -297,7 +298,7 @@ class Color(list): return 'rgb', None if color.startswith('url('): raise ColorIdError("Gradient other referenced element id.") raise ColorIdError("Color references other element id, e.g. a gradient") # Next handle short colors (css: #abc -> #aabbcc) if color.startswith('#'): Loading Loading @@ -409,6 +410,13 @@ class Color(list): return Color() return Color(COLOR_SVG.get(str(self), str(self))) def interpolate(self, other, fraction): """Iterpolate two colours by the given fraction""" return Color( [interpcoord(c1, c2, fraction) for (c1, c2) in zip(self.to_floats(), other.to_floats())] ) def rgb_to_hsl(red, green, blue): """RGB to HSL colour conversion""" Loading
inkex/elements/__init__.py +3 −3 Original line number Diff line number Diff line Loading @@ -14,5 +14,5 @@ from ._text import FlowRegion, FlowRoot, FlowPara, FlowDiv, FlowSpan, TextElemen from ._use import Symbol, Use from ._meta import Defs, StyleElement, Script, Desc, Title, NamedView, Guide, \ Metadata, ForeignObject, Switch, Grid from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, PathEffect from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, PathEffect, Stop from ._image import Image
inkex/elements/_base.py +6 −0 Original line number Diff line number Diff line Loading @@ -333,6 +333,12 @@ class BaseElement(etree.ElementBase): if self.getparent() is not None: self.getparent().remove(self) def remove_all(self, *types): """Remove all children or child types""" for child in self: if not types or isinstance(child, types): self.remove(child) def replace_with(self, elem): """Replace this element with the given element""" self.addnext(elem) Loading
inkex/elements/_filters.py +101 −1 Original line number Diff line number Diff line Loading @@ -24,10 +24,13 @@ Element interface for patterns, filters, gradients and path effects. """ from lxml import etree from copy import deepcopy from ..utils import addNS from ..transforms import Transform from ..tween import interpcoord, interp from ..styles import Style from ._base import BaseElement class Filter(BaseElement): Loading Loading @@ -92,6 +95,24 @@ class Filter(BaseElement): tag_name = 'feTurbulence' class Stop(BaseElement): tag_name = 'stop' @property def offset(self): return self.get('offset') @offset.setter def offset(self, number): self.set('offset', number) def interpolate(self, other, fraction): newstop = Stop() newstop.style = self.style.interpolate(other.style, fraction) newstop.offset = interpcoord(float(self.offset), float(other.offset), fraction) return newstop class Pattern(BaseElement): """Pattern element which is used in the def to control repeating fills""" tag_name = 'pattern' Loading @@ -102,14 +123,93 @@ class Gradient(BaseElement): """A gradient instruction usually in the defs""" WRAPPED_ATTRS = BaseElement.WRAPPED_ATTRS + (('gradientTransform', Transform),) orientation_attributes = () @property def stops(self): # type: () -> List[Stop] """Return an ordered list of own or linked stop nodes""" gradcolor = self.href if isinstance(self.href, LinearGradient) else self return sorted(gradcolor, key=lambda x: float(x.offset)) @property def stop_offsets(self): # type: () -> List[float] """Return a list of own or linked stop offsets""" return [child.offset for child in self.stops] @property def stop_styles(self): # type: () -> List[Style] """Return a list of own or linked offset styles""" return [child.style for child in self.stops] def remove_orientation(self): """Remove all orientation attributes from this element""" for attr in self.orientation_attributes: self.pop(attr) def interpolate(self, other, fraction): # type: (LinearGradient, float) -> LinearGradient """Interpolate with another gradient.""" if self.tag_name != other.tag_name: return self newgrad = self.copy() # interpolate transforms newtransform = self.gradientTransform.interpolate(other.gradientTransform, fraction) newgrad.gradientTransform = newtransform # interpolate orientation for attr in self.orientation_attributes: newattr = interpcoord(float(self.get(attr)), float(other.get(attr)), fraction) newgrad.set(attr, newattr) # interpolate stops if self.href is not None and self.href is other.href: # both gradients link to the same stops pass else: # gradients might have different stops newoffsets = sorted(self.stop_offsets + other.stop_offsets[1:-1]) sstops = interp(self.stop_offsets, self.stops, newoffsets) ostops = interp(other.stop_offsets, other.stops, newoffsets) newstops = [s1.interpolate(s2, fraction) for s1, s2 in zip(sstops, ostops)] newgrad.remove_all(Stop) newgrad.add(*newstops) return newgrad def stops_and_orientation(self): """Return a copy of all the stops in this gradient""" stops = self.copy() stops.remove_orientation() orientation = self.copy() orientation.remove_all(Stop) return stops, orientation class LinearGradient(Gradient): tag_name = 'linearGradient' orientation_attributes = ('x1', 'y1', 'x2', 'y2') def apply_transform(self): # type: () -> None """Apply transform to orientation points and set it to identity.""" trans = self.pop('gradientTransform') p1 = (float(self.get('x1')), float(self.get('y1'))) p2 = (float(self.get('x2')), float(self.get('y2'))) p1t = trans.apply_to_point(p1) p2t = trans.apply_to_point(p2) self.update(x1=p1t[0], y1=p1t[1], x2=p2t[0], y2=p2t[1]) class RadialGradient(Gradient): tag_name = 'radialGradient' orientation_attributes = ('cx', 'cy', 'fx', 'fy', 'r') def apply_transform(self): # type: () -> None """Apply transform to orientation points and set it to identity.""" trans = self.pop('gradientTransform') p1 = (float(self.get('cx')), float(self.get('cy'))) p2 = (float(self.get('fx')), float(self.get('fy'))) p1t = trans.apply_to_point(p1) p2t = trans.apply_to_point(p2) self.update(cx=p1t[0], cy=p1t[1], fx=p2t[0], fy=p2t[1]) class PathEffect(BaseElement): """Inkscape LPE element""" Loading
inkex/styles.py +36 −2 Original line number Diff line number Diff line Loading @@ -25,7 +25,8 @@ import re from collections import OrderedDict from .utils import PY3 from .colors import Color from .colors import Color, ColorIdError from .tween import interpcoord, interpunit if PY3: unicode = str # pylint: disable=redefined-builtin,invalid-name Loading Loading @@ -72,7 +73,8 @@ class Classes(list): class Style(OrderedDict): """A list of style directives""" color_props = ('stroke', 'fill', 'stop-color', 'flood-color', 'lighting-color') opacity_props = ('stroke-opacity', 'fill-opacity', 'opacity') opacity_props = ('stroke-opacity', 'fill-opacity', 'opacity', 'stop-opacity') unit_props = ('stroke-width') def __init__(self, style=None, callback=None, **kw): # This callback is set twice because this is 'pre-initial' data (no callback) Loading Loading @@ -170,6 +172,38 @@ class Style(OrderedDict): if value == 'url(#{})'.format(old_id): self[name] = 'url(#{})'.format(new_id) def interpolate_prop(self, other, fraction, prop, svg=None): """Interpolate specific property.""" a1 = self[prop] a2 = other.get(prop, None) if a2 is None: val = a1 else: if prop in self.color_props: if isinstance(a1, Color): val = a1.interpolate(Color(a2), fraction) elif a1.startswith('url(') or a2.startswith('url('): # gradient requires changes to the whole svg # and needs to be handled externally val = a1 else: val = Color(a1).interpolate(Color(a2), fraction) elif prop in self.opacity_props: val = interpcoord(float(a1), float(a2), fraction) elif prop in self.unit_props: val = interpunit(a1, a2, fraction) else: val = a1 return val def interpolate(self, other, fraction): # type: (Style, float, Optional[str], Optional[str]) -> Style """Interpolate all properties.""" style = Style() for prop, value in self.items(): style[prop] = self.interpolate_prop(other, fraction, prop) return style class AttrFallbackStyle(object): """ A container for a style and an element that may have competing styles Loading