2022-03-25 13:47:27 +03:00
|
|
|
|
"""Render SVG images."""
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-03-07 22:34:24 +03:00
|
|
|
|
import re
|
2022-10-05 19:22:35 +03:00
|
|
|
|
from contextlib import suppress
|
2021-04-16 16:33:04 +03:00
|
|
|
|
from math import cos, hypot, pi, radians, sin, sqrt
|
2021-02-22 01:25:35 +03:00
|
|
|
|
from xml.etree import ElementTree
|
|
|
|
|
|
2021-03-23 17:06:33 +03:00
|
|
|
|
from cssselect2 import ElementWrapper
|
|
|
|
|
|
2022-01-03 16:14:36 +03:00
|
|
|
|
from ..urls import get_url_attribute
|
2021-03-23 17:06:33 +03:00
|
|
|
|
from .css import parse_declarations, parse_stylesheets
|
2024-03-17 13:34:03 +03:00
|
|
|
|
from .defs import apply_filters, clip_path, draw_gradient_or_pattern, paint_mask, use
|
2021-04-18 17:01:26 +03:00
|
|
|
|
from .images import image, svg
|
2021-03-12 17:20:43 +03:00
|
|
|
|
from .path import path
|
2021-03-12 18:08:39 +03:00
|
|
|
|
from .shapes import circle, ellipse, line, polygon, polyline, rect
|
2021-04-10 23:15:43 +03:00
|
|
|
|
from .text import text
|
2024-03-17 13:34:03 +03:00
|
|
|
|
|
|
|
|
|
from .bounding_box import ( # isort:skip
|
|
|
|
|
EMPTY_BOUNDING_BOX, bounding_box, extend_bounding_box, is_valid_bounding_box)
|
|
|
|
|
from .utils import ( # isort:skip
|
2024-02-03 12:57:04 +03:00
|
|
|
|
PointError, alpha_value, color, normalize, parse_url, preserve_ratio, size,
|
|
|
|
|
transform)
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
|
|
|
|
TAGS = {
|
2021-04-10 23:15:43 +03:00
|
|
|
|
'a': text,
|
2021-02-22 01:25:35 +03:00
|
|
|
|
'circle': circle,
|
2021-07-18 10:36:14 +03:00
|
|
|
|
'clipPath': clip_path,
|
2021-02-22 01:25:35 +03:00
|
|
|
|
'ellipse': ellipse,
|
2021-03-23 18:22:41 +03:00
|
|
|
|
'image': image,
|
2021-03-07 22:34:24 +03:00
|
|
|
|
'line': line,
|
2021-03-12 17:20:43 +03:00
|
|
|
|
'path': path,
|
2021-03-07 22:34:24 +03:00
|
|
|
|
'polyline': polyline,
|
|
|
|
|
'polygon': polygon,
|
2021-02-22 01:25:35 +03:00
|
|
|
|
'rect': rect,
|
|
|
|
|
'svg': svg,
|
2021-04-10 23:15:43 +03:00
|
|
|
|
'text': text,
|
2021-04-11 17:13:59 +03:00
|
|
|
|
'textPath': text,
|
2021-04-10 23:15:43 +03:00
|
|
|
|
'tspan': text,
|
2021-03-15 16:21:36 +03:00
|
|
|
|
'use': use,
|
2021-02-22 01:25:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-07 22:34:24 +03:00
|
|
|
|
NOT_INHERITED_ATTRIBUTES = frozenset((
|
|
|
|
|
'clip',
|
|
|
|
|
'clip-path',
|
|
|
|
|
'filter',
|
|
|
|
|
'height',
|
|
|
|
|
'id',
|
|
|
|
|
'mask',
|
|
|
|
|
'opacity',
|
|
|
|
|
'overflow',
|
|
|
|
|
'rotate',
|
|
|
|
|
'stop-color',
|
|
|
|
|
'stop-opacity',
|
|
|
|
|
'style',
|
|
|
|
|
'transform',
|
|
|
|
|
'transform-origin',
|
|
|
|
|
'viewBox',
|
|
|
|
|
'width',
|
|
|
|
|
'x',
|
|
|
|
|
'y',
|
|
|
|
|
'dx',
|
|
|
|
|
'dy',
|
|
|
|
|
'{http://www.w3.org/1999/xlink}href',
|
|
|
|
|
'href',
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
COLOR_ATTRIBUTES = frozenset((
|
|
|
|
|
'fill',
|
|
|
|
|
'flood-color',
|
|
|
|
|
'lighting-color',
|
|
|
|
|
'stop-color',
|
|
|
|
|
'stroke',
|
|
|
|
|
))
|
|
|
|
|
|
2021-03-12 17:47:49 +03:00
|
|
|
|
DEF_TYPES = frozenset((
|
2021-07-18 10:36:14 +03:00
|
|
|
|
'clipPath',
|
2021-03-12 17:47:49 +03:00
|
|
|
|
'filter',
|
2021-07-18 10:36:14 +03:00
|
|
|
|
'gradient',
|
2021-03-12 17:47:49 +03:00
|
|
|
|
'image',
|
2021-07-18 10:36:14 +03:00
|
|
|
|
'marker',
|
|
|
|
|
'mask',
|
|
|
|
|
'path',
|
|
|
|
|
'pattern',
|
2021-03-12 17:47:49 +03:00
|
|
|
|
))
|
|
|
|
|
|
2021-03-07 22:34:24 +03:00
|
|
|
|
|
|
|
|
|
class Node:
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""An SVG document node."""
|
|
|
|
|
|
2021-03-23 17:06:33 +03:00
|
|
|
|
def __init__(self, wrapper, style):
|
|
|
|
|
self._wrapper = wrapper
|
2021-04-12 15:25:55 +03:00
|
|
|
|
self._etree_node = wrapper.etree_element
|
2021-03-23 17:06:33 +03:00
|
|
|
|
self._style = style
|
2021-03-07 22:34:24 +03:00
|
|
|
|
|
2021-06-01 23:30:54 +03:00
|
|
|
|
self.attrib = wrapper.etree_element.attrib.copy()
|
2021-03-07 22:34:24 +03:00
|
|
|
|
|
|
|
|
|
self.vertices = []
|
2021-03-14 11:29:33 +03:00
|
|
|
|
self.bounding_box = None
|
2021-03-07 22:34:24 +03:00
|
|
|
|
|
2021-06-01 23:11:11 +03:00
|
|
|
|
def copy(self):
|
2021-06-01 23:30:54 +03:00
|
|
|
|
"""Create a deep copy of the node as it was when first created."""
|
|
|
|
|
return Node(self._wrapper, self._style)
|
2021-06-01 23:11:11 +03:00
|
|
|
|
|
|
|
|
|
def get(self, key, default=None):
|
|
|
|
|
"""Get attribute."""
|
|
|
|
|
return self.attrib.get(key, default)
|
|
|
|
|
|
2021-03-24 16:10:50 +03:00
|
|
|
|
@property
|
|
|
|
|
def tag(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""XML tag name with no namespace."""
|
2021-04-11 17:13:59 +03:00
|
|
|
|
return self._etree_node.tag.split('}', 1)[-1]
|
2021-03-24 16:10:50 +03:00
|
|
|
|
|
2021-04-10 23:15:43 +03:00
|
|
|
|
@property
|
|
|
|
|
def text(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""XML node text."""
|
2021-04-10 23:15:43 +03:00
|
|
|
|
return self._etree_node.text
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def tail(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Text after the XML node."""
|
2021-04-10 23:15:43 +03:00
|
|
|
|
return self._etree_node.tail
|
|
|
|
|
|
2024-01-23 00:47:39 +03:00
|
|
|
|
@property
|
|
|
|
|
def display(self):
|
|
|
|
|
"""Whether node should be displayed."""
|
|
|
|
|
return self.get('display') != 'none'
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def visible(self):
|
|
|
|
|
"""Whether node is visible."""
|
|
|
|
|
return self.display and self.get('visibility') != 'hidden'
|
|
|
|
|
|
2021-03-07 22:34:24 +03:00
|
|
|
|
def __iter__(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Yield node children, handling cascade."""
|
2021-03-23 17:06:33 +03:00
|
|
|
|
for wrapper in self._wrapper:
|
2021-04-12 15:25:55 +03:00
|
|
|
|
child = Node(wrapper, self._style)
|
|
|
|
|
|
|
|
|
|
# Cascade
|
|
|
|
|
for key, value in self.attrib.items():
|
|
|
|
|
if key not in NOT_INHERITED_ATTRIBUTES:
|
|
|
|
|
if key not in child.attrib:
|
|
|
|
|
child.attrib[key] = value
|
|
|
|
|
|
|
|
|
|
# Apply style attribute
|
|
|
|
|
style_attr = child.get('style')
|
|
|
|
|
if style_attr:
|
|
|
|
|
normal_attr, important_attr = parse_declarations(style_attr)
|
|
|
|
|
else:
|
|
|
|
|
normal_attr, important_attr = [], []
|
|
|
|
|
normal_matcher, important_matcher = self._style
|
|
|
|
|
normal = [rule[-1] for rule in normal_matcher.match(wrapper)]
|
|
|
|
|
important = [rule[-1] for rule in important_matcher.match(wrapper)]
|
|
|
|
|
declarations_lists = (
|
|
|
|
|
normal, [normal_attr], important, [important_attr])
|
|
|
|
|
for declarations_list in declarations_lists:
|
|
|
|
|
for declarations in declarations_list:
|
|
|
|
|
for name, value in declarations:
|
2021-05-27 17:36:34 +03:00
|
|
|
|
child.attrib[name] = value.strip()
|
2021-04-12 15:25:55 +03:00
|
|
|
|
|
|
|
|
|
# Replace 'currentColor' value
|
|
|
|
|
for key in COLOR_ATTRIBUTES:
|
|
|
|
|
if child.get(key) == 'currentColor':
|
|
|
|
|
child.attrib[key] = child.get('color', 'black')
|
|
|
|
|
|
|
|
|
|
# Handle 'inherit' values
|
2022-06-17 11:07:28 +03:00
|
|
|
|
for key, value in child.attrib.copy().items():
|
2021-04-12 15:25:55 +03:00
|
|
|
|
if value == 'inherit':
|
2022-06-17 11:07:28 +03:00
|
|
|
|
value = self.get(key)
|
|
|
|
|
if value is None:
|
|
|
|
|
del child.attrib[key]
|
|
|
|
|
else:
|
|
|
|
|
child.attrib[key] = value
|
2021-04-12 15:25:55 +03:00
|
|
|
|
|
|
|
|
|
# Fix text in text tags
|
|
|
|
|
if child.tag in ('text', 'textPath', 'a'):
|
2021-07-16 16:17:15 +03:00
|
|
|
|
children, _ = child.text_children(
|
2021-04-12 15:25:55 +03:00
|
|
|
|
wrapper, trailing_space=True, text_root=True)
|
2021-07-16 16:17:15 +03:00
|
|
|
|
child._wrapper.etree_children = [
|
|
|
|
|
child._etree_node for child in children]
|
2021-04-12 15:25:55 +03:00
|
|
|
|
|
|
|
|
|
yield child
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
|
|
|
|
def get_viewbox(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Get node viewBox as a tuple of floats."""
|
2021-03-14 11:29:33 +03:00
|
|
|
|
viewbox = self.get('viewBox')
|
2021-02-22 01:25:35 +03:00
|
|
|
|
if viewbox:
|
|
|
|
|
return tuple(
|
|
|
|
|
float(number) for number in normalize(viewbox).split())
|
|
|
|
|
|
2022-01-03 16:14:36 +03:00
|
|
|
|
def get_href(self, base_url):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Get the href attribute, with or without a namespace."""
|
2022-01-03 16:14:36 +03:00
|
|
|
|
for attr_name in ('{http://www.w3.org/1999/xlink}href', 'href'):
|
2022-12-02 23:30:45 +03:00
|
|
|
|
url = get_url_attribute(
|
|
|
|
|
self, attr_name, base_url, allow_relative=True)
|
2022-01-03 16:14:36 +03:00
|
|
|
|
if url:
|
|
|
|
|
return url
|
2021-03-15 16:21:36 +03:00
|
|
|
|
|
2022-11-13 14:25:00 +03:00
|
|
|
|
def del_href(self):
|
|
|
|
|
"""Remove the href attributes, with or without a namespace."""
|
|
|
|
|
for attr_name in ('{http://www.w3.org/1999/xlink}href', 'href'):
|
|
|
|
|
self.attrib.pop(attr_name, None)
|
|
|
|
|
|
2021-04-11 17:13:59 +03:00
|
|
|
|
@staticmethod
|
2021-04-12 15:25:55 +03:00
|
|
|
|
def process_whitespace(string, preserve):
|
|
|
|
|
"""Replace newlines by spaces, and merge spaces if not preserved."""
|
|
|
|
|
# TODO: should be merged with build.process_whitespace
|
2021-04-11 17:13:59 +03:00
|
|
|
|
if not string:
|
|
|
|
|
return ''
|
|
|
|
|
if preserve:
|
|
|
|
|
return re.sub('[\n\r\t]', ' ', string)
|
|
|
|
|
else:
|
|
|
|
|
string = re.sub('[\n\r]', '', string)
|
|
|
|
|
string = re.sub('\t', ' ', string)
|
|
|
|
|
return re.sub(' +', ' ', string)
|
|
|
|
|
|
2021-03-15 16:21:36 +03:00
|
|
|
|
def get_child(self, id_):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Get a child with given id in the whole child tree."""
|
2021-03-15 16:21:36 +03:00
|
|
|
|
for child in self:
|
|
|
|
|
if child.get('id') == id_:
|
|
|
|
|
return child
|
|
|
|
|
grandchild = child.get_child(id_)
|
|
|
|
|
if grandchild:
|
|
|
|
|
return grandchild
|
|
|
|
|
|
2021-04-10 23:15:43 +03:00
|
|
|
|
def text_children(self, element, trailing_space, text_root=False):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Handle text node by fixing whitespaces and flattening tails."""
|
2021-04-10 23:15:43 +03:00
|
|
|
|
children = []
|
|
|
|
|
space = '{http://www.w3.org/XML/1998/namespace}space'
|
|
|
|
|
preserve = self.get(space) == 'preserve'
|
2021-04-12 15:25:55 +03:00
|
|
|
|
self._etree_node.text = self.process_whitespace(
|
2021-04-10 23:15:43 +03:00
|
|
|
|
element.etree_element.text, preserve)
|
|
|
|
|
if trailing_space and not preserve:
|
|
|
|
|
self._etree_node.text = self.text.lstrip(' ')
|
2021-04-11 17:13:59 +03:00
|
|
|
|
|
|
|
|
|
original_rotate = [
|
|
|
|
|
float(i) for i in
|
|
|
|
|
normalize(self.get('rotate')).strip().split(' ') if i]
|
|
|
|
|
rotate = original_rotate.copy()
|
2021-04-10 23:15:43 +03:00
|
|
|
|
if original_rotate:
|
2021-04-11 17:13:59 +03:00
|
|
|
|
self.pop_rotation(original_rotate, rotate)
|
2021-04-10 23:15:43 +03:00
|
|
|
|
if self.text:
|
|
|
|
|
trailing_space = self.text.endswith(' ')
|
2024-02-16 23:59:21 +03:00
|
|
|
|
element_children = tuple(element.iter_children())
|
|
|
|
|
for child_element in element_children:
|
2021-04-10 23:15:43 +03:00
|
|
|
|
child = child_element.etree_element
|
|
|
|
|
if child.tag in ('{http://www.w3.org/2000/svg}tref', 'tref'):
|
|
|
|
|
child_node = Node(child_element, self._style)
|
|
|
|
|
child_node._etree_node.tag = 'tspan'
|
|
|
|
|
# Retrieve the referenced node and get its flattened text
|
|
|
|
|
# and remove the node children.
|
|
|
|
|
child = child_node._etree_node
|
2021-04-11 17:13:59 +03:00
|
|
|
|
child._etree_node.text = child.flatten()
|
2021-04-10 23:15:43 +03:00
|
|
|
|
child_element = ElementWrapper.from_xml_root(child)
|
|
|
|
|
else:
|
|
|
|
|
child_node = Node(child_element, self._style)
|
|
|
|
|
child_preserve = child_node.get(space) == 'preserve'
|
2021-04-12 15:25:55 +03:00
|
|
|
|
child_node._etree_node.text = self.process_whitespace(
|
2021-04-10 23:15:43 +03:00
|
|
|
|
child.text, child_preserve)
|
|
|
|
|
child_node.children, trailing_space = child_node.text_children(
|
|
|
|
|
child_element, trailing_space)
|
|
|
|
|
trailing_space = child_node.text.endswith(' ')
|
|
|
|
|
if original_rotate and 'rotate' not in child_node:
|
2021-04-11 17:13:59 +03:00
|
|
|
|
child_node.pop_rotation(original_rotate, rotate)
|
2021-04-10 23:15:43 +03:00
|
|
|
|
children.append(child_node)
|
2024-02-16 23:59:21 +03:00
|
|
|
|
tail = self.process_whitespace(child.tail, preserve)
|
|
|
|
|
if text_root and child_element is element_children[-1]:
|
|
|
|
|
if not preserve:
|
|
|
|
|
tail = tail.rstrip(' ')
|
|
|
|
|
if tail:
|
2021-04-10 23:15:43 +03:00
|
|
|
|
anonymous_etree = ElementTree.Element(
|
|
|
|
|
'{http://www.w3.org/2000/svg}tspan')
|
|
|
|
|
anonymous = Node(
|
|
|
|
|
ElementWrapper.from_xml_root(anonymous_etree), self._style)
|
2024-02-16 23:59:21 +03:00
|
|
|
|
anonymous._etree_node.text = tail
|
2021-04-10 23:15:43 +03:00
|
|
|
|
if original_rotate:
|
2021-04-11 17:13:59 +03:00
|
|
|
|
anonymous.pop_rotation(original_rotate, rotate)
|
2021-04-10 23:15:43 +03:00
|
|
|
|
if trailing_space and not preserve:
|
2021-04-20 18:05:05 +03:00
|
|
|
|
anonymous._etree_node.text = anonymous.text.lstrip(' ')
|
2021-04-10 23:15:43 +03:00
|
|
|
|
if anonymous.text:
|
|
|
|
|
trailing_space = anonymous.text.endswith(' ')
|
|
|
|
|
children.append(anonymous)
|
|
|
|
|
|
|
|
|
|
if text_root and not children and not preserve:
|
|
|
|
|
self._etree_node.text = self.text.rstrip(' ')
|
|
|
|
|
|
|
|
|
|
return children, trailing_space
|
|
|
|
|
|
2021-04-11 17:13:59 +03:00
|
|
|
|
def flatten(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Flatten text in node and in its children."""
|
2021-04-11 17:13:59 +03:00
|
|
|
|
flattened_text = [self.text or '']
|
|
|
|
|
for child in list(self):
|
|
|
|
|
flattened_text.append(child.flatten())
|
|
|
|
|
flattened_text.append(child.tail or '')
|
|
|
|
|
self.remove(child)
|
|
|
|
|
return ''.join(flattened_text)
|
|
|
|
|
|
|
|
|
|
def pop_rotation(self, original_rotate, rotate):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Merge nested letter rotations."""
|
2021-04-11 17:13:59 +03:00
|
|
|
|
self.attrib['rotate'] = ' '.join(
|
|
|
|
|
str(rotate.pop(0) if rotate else original_rotate[-1])
|
|
|
|
|
for i in range(len(self.text)))
|
|
|
|
|
|
2023-04-29 17:30:41 +03:00
|
|
|
|
def override_iter(self, iterator):
|
|
|
|
|
"""Override node’s children iterator."""
|
|
|
|
|
# As special methods are bound to classes and not instances, we have to
|
|
|
|
|
# create and assign a new type.
|
|
|
|
|
self.__class__ = type(
|
|
|
|
|
'Node', (Node,), {'__iter__': lambda _: iterator})
|
|
|
|
|
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
|
|
|
|
class SVG:
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""An SVG document."""
|
|
|
|
|
|
2021-09-12 18:40:19 +03:00
|
|
|
|
def __init__(self, tree, url):
|
2021-03-23 17:06:33 +03:00
|
|
|
|
wrapper = ElementWrapper.from_xml_root(tree)
|
|
|
|
|
style = parse_stylesheets(wrapper, url)
|
|
|
|
|
self.tree = Node(wrapper, style)
|
2021-03-14 11:29:33 +03:00
|
|
|
|
self.url = url
|
|
|
|
|
|
2022-10-15 17:19:37 +03:00
|
|
|
|
# Replace 'currentColor' value
|
|
|
|
|
for key in COLOR_ATTRIBUTES:
|
|
|
|
|
if self.tree.get(key) == 'currentColor':
|
|
|
|
|
self.tree.attrib[key] = self.tree.get('color', 'black')
|
|
|
|
|
|
2021-03-14 11:29:33 +03:00
|
|
|
|
self.filters = {}
|
|
|
|
|
self.gradients = {}
|
|
|
|
|
self.images = {}
|
|
|
|
|
self.markers = {}
|
|
|
|
|
self.masks = {}
|
|
|
|
|
self.patterns = {}
|
|
|
|
|
self.paths = {}
|
|
|
|
|
|
2021-09-11 15:57:47 +03:00
|
|
|
|
self.use_cache = {}
|
|
|
|
|
|
2021-04-10 23:15:43 +03:00
|
|
|
|
self.cursor_position = [0, 0]
|
|
|
|
|
self.cursor_d_position = [0, 0]
|
2021-04-12 15:30:30 +03:00
|
|
|
|
self.text_path_width = 0
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
self.parse_defs(self.tree)
|
2022-11-13 14:25:00 +03:00
|
|
|
|
self.inherit_defs()
|
2021-03-15 16:21:36 +03:00
|
|
|
|
|
2021-03-14 11:29:33 +03:00
|
|
|
|
def get_intrinsic_size(self, font_size):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Get intrinsic size of the image."""
|
|
|
|
|
intrinsic_width = self.tree.get('width', '100%')
|
|
|
|
|
if '%' in intrinsic_width:
|
|
|
|
|
intrinsic_width = None
|
|
|
|
|
else:
|
|
|
|
|
intrinsic_width = size(intrinsic_width, font_size)
|
|
|
|
|
|
|
|
|
|
intrinsic_height = self.tree.get('height', '100%')
|
|
|
|
|
if '%' in intrinsic_height:
|
|
|
|
|
intrinsic_height = None
|
|
|
|
|
else:
|
|
|
|
|
intrinsic_height = size(intrinsic_height, font_size)
|
|
|
|
|
|
|
|
|
|
return intrinsic_width, intrinsic_height
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
|
|
|
|
def get_viewbox(self):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Get document viewBox as a tuple of floats."""
|
2021-03-14 11:29:33 +03:00
|
|
|
|
return self.tree.get_viewbox()
|
|
|
|
|
|
2021-03-17 23:30:35 +03:00
|
|
|
|
def point(self, x, y, font_size):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Compute size of an x/y or width/height couple."""
|
2021-03-17 23:30:35 +03:00
|
|
|
|
return (
|
2021-07-31 09:29:23 +03:00
|
|
|
|
size(x, font_size, self.inner_width),
|
|
|
|
|
size(y, font_size, self.inner_height))
|
2021-03-17 23:30:35 +03:00
|
|
|
|
|
|
|
|
|
def length(self, length, font_size):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Compute size of an arbirtary attribute."""
|
2021-07-31 09:29:23 +03:00
|
|
|
|
return size(length, font_size, self.inner_diagonal)
|
2021-03-17 23:30:35 +03:00
|
|
|
|
|
2021-02-22 01:25:35 +03:00
|
|
|
|
def draw(self, stream, concrete_width, concrete_height, base_url,
|
2021-04-14 17:56:06 +03:00
|
|
|
|
url_fetcher, context):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Draw image on a stream."""
|
2021-02-22 01:25:35 +03:00
|
|
|
|
self.stream = stream
|
2021-07-31 09:29:23 +03:00
|
|
|
|
|
2021-02-22 01:25:35 +03:00
|
|
|
|
self.concrete_width = concrete_width
|
|
|
|
|
self.concrete_height = concrete_height
|
|
|
|
|
self.normalized_diagonal = (
|
2021-03-23 15:58:31 +03:00
|
|
|
|
hypot(concrete_width, concrete_height) / sqrt(2))
|
2021-07-31 09:29:23 +03:00
|
|
|
|
|
|
|
|
|
viewbox = self.get_viewbox()
|
|
|
|
|
if viewbox:
|
|
|
|
|
self.inner_width, self.inner_height = viewbox[2], viewbox[3]
|
|
|
|
|
else:
|
|
|
|
|
self.inner_width = self.concrete_width
|
|
|
|
|
self.inner_height = self.concrete_height
|
|
|
|
|
self.inner_diagonal = (
|
|
|
|
|
hypot(self.inner_width, self.inner_height) / sqrt(2))
|
|
|
|
|
|
2021-02-22 01:25:35 +03:00
|
|
|
|
self.base_url = base_url
|
|
|
|
|
self.url_fetcher = url_fetcher
|
2021-04-14 17:56:06 +03:00
|
|
|
|
self.context = context
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
|
|
|
|
self.draw_node(self.tree, size('12pt'))
|
|
|
|
|
|
2021-07-18 10:36:14 +03:00
|
|
|
|
def draw_node(self, node, font_size, fill_stroke=True):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Draw a node."""
|
2021-03-12 17:47:49 +03:00
|
|
|
|
if node.tag == 'defs':
|
|
|
|
|
return
|
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Update font size
|
2021-02-22 01:25:35 +03:00
|
|
|
|
font_size = size(node.get('font-size', '1em'), font_size, font_size)
|
|
|
|
|
|
2024-01-14 15:58:41 +03:00
|
|
|
|
original_streams = []
|
|
|
|
|
|
2021-07-18 10:36:14 +03:00
|
|
|
|
if fill_stroke:
|
|
|
|
|
self.stream.push_state()
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Apply filters
|
2021-04-04 13:40:04 +03:00
|
|
|
|
filter_ = self.filters.get(parse_url(node.get('filter')).fragment)
|
|
|
|
|
if filter_:
|
|
|
|
|
apply_filters(self, node, filter_, font_size)
|
|
|
|
|
|
2023-10-03 15:30:21 +03:00
|
|
|
|
# Apply transform attribute
|
|
|
|
|
self.transform(node.get('transform'), font_size)
|
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Create substream for opacity
|
2024-02-03 12:57:04 +03:00
|
|
|
|
opacity = alpha_value(node.get('opacity', 1))
|
2021-07-18 10:36:14 +03:00
|
|
|
|
if fill_stroke and 0 <= opacity < 1:
|
2024-01-14 15:58:41 +03:00
|
|
|
|
original_streams.append(self.stream)
|
2021-05-30 10:50:40 +03:00
|
|
|
|
box = self.calculate_bounding_box(node, font_size)
|
2023-10-03 15:30:21 +03:00
|
|
|
|
if not is_valid_bounding_box(box):
|
|
|
|
|
box = (0, 0, self.inner_width, self.inner_height)
|
|
|
|
|
self.stream = self.stream.add_group(*box)
|
2021-07-18 10:36:14 +03:00
|
|
|
|
|
|
|
|
|
# Clip
|
|
|
|
|
clip_path = parse_url(node.get('clip-path')).fragment
|
|
|
|
|
if clip_path and clip_path in self.paths:
|
|
|
|
|
old_ctm = self.stream.ctm
|
|
|
|
|
clip_path = self.paths[clip_path]
|
|
|
|
|
if clip_path.get('clipPathUnits') == 'objectBoundingBox':
|
|
|
|
|
x, y = self.point(node.get('x'), node.get('y'), font_size)
|
|
|
|
|
width, height = self.point(
|
|
|
|
|
node.get('width'), node.get('height'), font_size)
|
|
|
|
|
self.stream.transform(a=width, d=height, e=x, f=y)
|
2023-08-19 11:25:28 +03:00
|
|
|
|
original_tag = clip_path._etree_node.tag
|
2021-07-18 10:36:14 +03:00
|
|
|
|
clip_path._etree_node.tag = 'g'
|
|
|
|
|
self.draw_node(clip_path, font_size, fill_stroke=False)
|
2023-08-19 11:25:28 +03:00
|
|
|
|
clip_path._etree_node.tag = original_tag
|
2021-07-18 10:36:14 +03:00
|
|
|
|
# At least set the clipping area to an empty path, so that it’s
|
|
|
|
|
# totally clipped when the clipping path is empty.
|
|
|
|
|
self.stream.rectangle(0, 0, 0, 0)
|
|
|
|
|
self.stream.clip()
|
|
|
|
|
self.stream.end()
|
|
|
|
|
new_ctm = self.stream.ctm
|
|
|
|
|
if new_ctm.determinant:
|
|
|
|
|
self.stream.transform(*(old_ctm @ new_ctm.invert).values)
|
|
|
|
|
|
2023-10-16 19:59:42 +03:00
|
|
|
|
# Handle text anchor
|
2024-02-16 23:59:21 +03:00
|
|
|
|
if node.tag == 'text':
|
|
|
|
|
text_anchor = node.get('text-anchor')
|
|
|
|
|
children = tuple(node)
|
|
|
|
|
if children and not node.text:
|
|
|
|
|
text_anchor = children[0].get('text-anchor')
|
|
|
|
|
if node.tag == 'text' and text_anchor in ('middle', 'end'):
|
2023-10-16 19:59:42 +03:00
|
|
|
|
group = self.stream.add_group(0, 0, 0, 0) # BBox set after drawing
|
2024-01-14 15:58:41 +03:00
|
|
|
|
original_streams.append(self.stream)
|
|
|
|
|
self.stream = group
|
2023-10-16 19:59:42 +03:00
|
|
|
|
|
2024-01-23 01:10:10 +03:00
|
|
|
|
# Set text bounding box
|
|
|
|
|
if node.display and TAGS.get(node.tag) == text:
|
|
|
|
|
node.text_bounding_box = EMPTY_BOUNDING_BOX
|
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Draw node
|
2024-01-23 00:47:39 +03:00
|
|
|
|
if node.visible and node.tag in TAGS:
|
2022-10-05 19:22:35 +03:00
|
|
|
|
with suppress(PointError):
|
2021-07-30 17:08:19 +03:00
|
|
|
|
TAGS[node.tag](self, node, font_size)
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Draw node children
|
2024-01-23 00:47:39 +03:00
|
|
|
|
if node.display and node.tag not in DEF_TYPES:
|
2021-04-05 13:11:36 +03:00
|
|
|
|
for child in node:
|
2021-07-18 10:36:14 +03:00
|
|
|
|
self.draw_node(child, font_size, fill_stroke)
|
2024-02-14 00:36:45 +03:00
|
|
|
|
visible_text_child = (
|
|
|
|
|
TAGS.get(node.tag) == text and
|
|
|
|
|
TAGS.get(child.tag) == text and
|
|
|
|
|
child.visible)
|
|
|
|
|
if visible_text_child:
|
2023-10-16 19:59:42 +03:00
|
|
|
|
if not is_valid_bounding_box(child.text_bounding_box):
|
|
|
|
|
continue
|
|
|
|
|
x1, y1 = child.text_bounding_box[:2]
|
|
|
|
|
x2 = x1 + child.text_bounding_box[2]
|
|
|
|
|
y2 = y1 + child.text_bounding_box[3]
|
|
|
|
|
node.text_bounding_box = extend_bounding_box(
|
|
|
|
|
node.text_bounding_box, ((x1, y1), (x2, y2)))
|
|
|
|
|
|
|
|
|
|
# Handle text anchor
|
2024-02-16 23:59:21 +03:00
|
|
|
|
if node.tag == 'text' and text_anchor in ('middle', 'end'):
|
2023-10-16 19:59:42 +03:00
|
|
|
|
group_id = self.stream.id
|
2024-01-14 15:58:41 +03:00
|
|
|
|
self.stream = original_streams.pop()
|
2023-10-16 19:59:42 +03:00
|
|
|
|
self.stream.push_state()
|
|
|
|
|
if is_valid_bounding_box(node.text_bounding_box):
|
|
|
|
|
x, y, width, height = node.text_bounding_box
|
2024-04-26 19:39:52 +03:00
|
|
|
|
# Add extra space to include ink extents
|
|
|
|
|
group.extra['BBox'][:] = (
|
|
|
|
|
x - font_size, y - font_size,
|
|
|
|
|
x + width + font_size, y + height + font_size)
|
2023-10-16 19:59:42 +03:00
|
|
|
|
x_align = width / 2 if text_anchor == 'middle' else width
|
|
|
|
|
self.stream.transform(e=-x_align)
|
|
|
|
|
self.stream.draw_x_object(group_id)
|
|
|
|
|
self.stream.pop_state()
|
2021-04-05 13:11:36 +03:00
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Apply mask
|
2021-04-05 13:11:36 +03:00
|
|
|
|
mask = self.masks.get(parse_url(node.get('mask')).fragment)
|
|
|
|
|
if mask:
|
|
|
|
|
paint_mask(self, node, mask, opacity)
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Fill and stroke
|
2021-07-18 10:36:14 +03:00
|
|
|
|
if fill_stroke:
|
|
|
|
|
self.fill_stroke(node, font_size)
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Draw markers
|
2021-07-18 10:36:14 +03:00
|
|
|
|
self.draw_markers(node, font_size, fill_stroke)
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Apply opacity stream and restore original stream
|
2021-07-18 10:36:14 +03:00
|
|
|
|
if fill_stroke and 0 <= opacity < 1:
|
2021-03-31 16:28:17 +03:00
|
|
|
|
group_id = self.stream.id
|
2024-01-14 15:58:41 +03:00
|
|
|
|
self.stream = original_streams.pop()
|
2021-06-02 21:35:06 +03:00
|
|
|
|
self.stream.set_alpha(opacity, stroke=True, fill=True)
|
2021-03-31 16:28:17 +03:00
|
|
|
|
self.stream.draw_x_object(group_id)
|
|
|
|
|
|
2021-04-12 15:32:57 +03:00
|
|
|
|
# Clean text tag
|
2021-04-12 15:30:30 +03:00
|
|
|
|
if node.tag == 'text':
|
|
|
|
|
self.cursor_position = [0, 0]
|
|
|
|
|
self.cursor_d_position = [0, 0]
|
|
|
|
|
self.text_path_width = 0
|
|
|
|
|
|
2021-07-18 10:36:14 +03:00
|
|
|
|
if fill_stroke:
|
|
|
|
|
self.stream.pop_state()
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-07-18 10:36:14 +03:00
|
|
|
|
def draw_markers(self, node, font_size, fill_stroke):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Draw markers defined in a node."""
|
2021-03-14 11:29:33 +03:00
|
|
|
|
if not node.vertices:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
markers = {}
|
|
|
|
|
common_marker = parse_url(node.get('marker')).fragment
|
|
|
|
|
for position in ('start', 'mid', 'end'):
|
2021-04-11 17:13:59 +03:00
|
|
|
|
attribute = f'marker-{position}'
|
2021-03-14 11:29:33 +03:00
|
|
|
|
if attribute in node.attrib:
|
|
|
|
|
markers[position] = parse_url(node.attrib[attribute]).fragment
|
|
|
|
|
else:
|
|
|
|
|
markers[position] = common_marker
|
|
|
|
|
|
|
|
|
|
angle1, angle2 = None, None
|
|
|
|
|
position = 'start'
|
|
|
|
|
|
|
|
|
|
while node.vertices:
|
|
|
|
|
# Calculate position and angle
|
|
|
|
|
point = node.vertices.pop(0)
|
|
|
|
|
angles = node.vertices.pop(0) if node.vertices else None
|
|
|
|
|
if angles:
|
|
|
|
|
if position == 'start':
|
|
|
|
|
angle = pi - angles[0]
|
|
|
|
|
else:
|
|
|
|
|
angle = (angle2 + pi - angles[0]) / 2
|
|
|
|
|
angle1, angle2 = angles
|
|
|
|
|
else:
|
|
|
|
|
angle = angle2
|
|
|
|
|
position = 'end'
|
|
|
|
|
|
2021-04-11 17:13:59 +03:00
|
|
|
|
# Draw marker
|
2021-03-14 11:29:33 +03:00
|
|
|
|
marker = markers[position]
|
2021-04-12 16:11:13 +03:00
|
|
|
|
if not marker:
|
|
|
|
|
position = 'mid' if angles else 'start'
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
marker_node = self.markers.get(marker)
|
|
|
|
|
|
|
|
|
|
# Calculate position, scale and clipping
|
2023-10-11 22:50:02 +03:00
|
|
|
|
translate_x, translate_y = self.point(
|
|
|
|
|
marker_node.get('refX'), marker_node.get('refY'),
|
|
|
|
|
font_size)
|
2024-03-25 13:18:24 +03:00
|
|
|
|
marker_width, marker_height = self.point(
|
|
|
|
|
marker_node.get('markerWidth', 3),
|
|
|
|
|
marker_node.get('markerHeight', 3),
|
|
|
|
|
font_size)
|
2022-03-27 22:46:28 +03:00
|
|
|
|
if 'viewBox' in marker_node.attrib:
|
2023-10-11 22:50:02 +03:00
|
|
|
|
scale_x, scale_y, _, _ = preserve_ratio(
|
2022-03-27 22:46:28 +03:00
|
|
|
|
self, marker_node, font_size, marker_width, marker_height)
|
2021-04-12 16:11:13 +03:00
|
|
|
|
|
|
|
|
|
clip_x, clip_y, viewbox_width, viewbox_height = (
|
|
|
|
|
marker_node.get_viewbox())
|
|
|
|
|
|
|
|
|
|
align = marker_node.get(
|
|
|
|
|
'preserveAspectRatio', 'xMidYMid').split(' ')[0]
|
|
|
|
|
if align == 'none':
|
|
|
|
|
x_position = y_position = 'min'
|
|
|
|
|
else:
|
|
|
|
|
x_position = align[1:4].lower()
|
|
|
|
|
y_position = align[5:].lower()
|
|
|
|
|
|
|
|
|
|
if x_position == 'mid':
|
|
|
|
|
clip_x += (viewbox_width - marker_width / scale_x) / 2
|
|
|
|
|
elif x_position == 'max':
|
|
|
|
|
clip_x += viewbox_width - marker_width / scale_x
|
|
|
|
|
|
|
|
|
|
if y_position == 'mid':
|
|
|
|
|
clip_y += (
|
|
|
|
|
viewbox_height - marker_height / scale_y) / 2
|
|
|
|
|
elif y_position == 'max':
|
|
|
|
|
clip_y += viewbox_height - marker_height / scale_y
|
|
|
|
|
|
|
|
|
|
clip_box = (
|
|
|
|
|
clip_x, clip_y,
|
|
|
|
|
marker_width / scale_x, marker_height / scale_y)
|
|
|
|
|
else:
|
2024-03-25 13:18:24 +03:00
|
|
|
|
scale_x = scale_y = 1
|
|
|
|
|
clip_box = (0, 0, marker_width, marker_height)
|
2021-04-12 16:11:13 +03:00
|
|
|
|
|
|
|
|
|
# Scale
|
|
|
|
|
if marker_node.get('markerUnits') != 'userSpaceOnUse':
|
|
|
|
|
scale = self.length(node.get('stroke-width', 1), font_size)
|
|
|
|
|
scale_x *= scale
|
|
|
|
|
scale_y *= scale
|
|
|
|
|
|
|
|
|
|
# Override angle
|
|
|
|
|
node_angle = marker_node.get('orient', 0)
|
|
|
|
|
if node_angle not in ('auto', 'auto-start-reverse'):
|
|
|
|
|
angle = radians(float(node_angle))
|
|
|
|
|
elif node_angle == 'auto-start-reverse' and position == 'start':
|
|
|
|
|
angle += radians(180)
|
|
|
|
|
|
|
|
|
|
# Draw marker path
|
|
|
|
|
for child in marker_node:
|
|
|
|
|
self.stream.push_state()
|
|
|
|
|
|
|
|
|
|
self.stream.transform(
|
|
|
|
|
scale_x * cos(angle), scale_x * sin(angle),
|
|
|
|
|
-scale_y * sin(angle), scale_y * cos(angle),
|
|
|
|
|
*point)
|
2021-07-17 15:25:58 +03:00
|
|
|
|
self.stream.transform(e=-translate_x, f=-translate_y)
|
2021-04-12 16:11:13 +03:00
|
|
|
|
|
|
|
|
|
overflow = marker_node.get('overflow', 'hidden')
|
2024-03-25 13:18:24 +03:00
|
|
|
|
if overflow in ('hidden', 'scroll'):
|
2021-04-12 16:11:13 +03:00
|
|
|
|
self.stream.rectangle(*clip_box)
|
|
|
|
|
self.stream.clip()
|
2023-10-11 22:50:02 +03:00
|
|
|
|
self.stream.end()
|
2021-04-12 16:11:13 +03:00
|
|
|
|
|
2021-07-18 10:36:14 +03:00
|
|
|
|
self.draw_node(child, font_size, fill_stroke)
|
2021-04-12 16:11:13 +03:00
|
|
|
|
self.stream.pop_state()
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
2021-09-01 05:32:04 +03:00
|
|
|
|
position = 'mid' if angles else 'start'
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
@staticmethod
|
2021-05-30 10:50:30 +03:00
|
|
|
|
def get_paint(value):
|
|
|
|
|
"""Get paint fill or stroke attribute with a color or a URL."""
|
2021-04-12 15:25:55 +03:00
|
|
|
|
if not value or value == 'none':
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
value = value.strip()
|
|
|
|
|
match = re.compile(r'(url\(.+\)) *(.*)').search(value)
|
|
|
|
|
if match:
|
|
|
|
|
source = parse_url(match.group(1)).fragment
|
|
|
|
|
color = match.group(2) or None
|
|
|
|
|
else:
|
|
|
|
|
source = None
|
|
|
|
|
color = value or None
|
|
|
|
|
|
|
|
|
|
return source, color
|
|
|
|
|
|
2021-04-20 18:15:17 +03:00
|
|
|
|
def fill_stroke(self, node, font_size, text=False):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Paint fill and stroke for a node."""
|
2021-04-20 18:15:17 +03:00
|
|
|
|
if node.tag in ('text', 'textPath', 'a') and not text:
|
|
|
|
|
return
|
2021-04-12 15:25:55 +03:00
|
|
|
|
|
|
|
|
|
# Get fill data
|
2021-05-30 10:50:30 +03:00
|
|
|
|
fill_source, fill_color = self.get_paint(node.get('fill', 'black'))
|
2024-02-03 12:57:04 +03:00
|
|
|
|
fill_opacity = alpha_value(node.get('fill-opacity', 1))
|
2021-03-19 17:58:22 +03:00
|
|
|
|
fill_drawn = draw_gradient_or_pattern(
|
2021-06-02 21:35:06 +03:00
|
|
|
|
self, node, fill_source, font_size, fill_opacity, stroke=False)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
if fill_color and not fill_drawn:
|
2021-04-17 13:30:40 +03:00
|
|
|
|
red, green, blue, alpha = color(fill_color)
|
|
|
|
|
self.stream.set_color_rgb(red, green, blue)
|
2021-06-02 21:35:06 +03:00
|
|
|
|
self.stream.set_alpha(alpha * fill_opacity)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
fill = fill_color or fill_drawn
|
2021-02-22 01:25:35 +03:00
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
# Get stroke data
|
2021-05-30 10:50:30 +03:00
|
|
|
|
stroke_source, stroke_color = self.get_paint(node.get('stroke'))
|
2024-02-03 12:57:04 +03:00
|
|
|
|
stroke_opacity = alpha_value(node.get('stroke-opacity', 1))
|
2021-03-19 17:58:22 +03:00
|
|
|
|
stroke_drawn = draw_gradient_or_pattern(
|
2021-06-02 21:35:06 +03:00
|
|
|
|
self, node, stroke_source, font_size, stroke_opacity, stroke=True)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
if stroke_color and not stroke_drawn:
|
2021-04-17 13:30:40 +03:00
|
|
|
|
red, green, blue, alpha = color(stroke_color)
|
|
|
|
|
self.stream.set_color_rgb(red, green, blue, stroke=True)
|
2021-06-02 21:35:06 +03:00
|
|
|
|
self.stream.set_alpha(alpha * stroke_opacity, stroke=True)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
stroke = stroke_color or stroke_drawn
|
2021-04-11 17:13:59 +03:00
|
|
|
|
stroke_width = self.length(node.get('stroke-width', '1px'), font_size)
|
2021-02-22 01:25:35 +03:00
|
|
|
|
if stroke_width:
|
2021-04-05 13:11:36 +03:00
|
|
|
|
self.stream.set_line_width(stroke_width)
|
2021-07-23 23:36:05 +03:00
|
|
|
|
else:
|
|
|
|
|
stroke = None
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
# Apply dash array
|
2021-03-14 11:29:33 +03:00
|
|
|
|
dash_array = tuple(
|
2021-07-30 16:15:50 +03:00
|
|
|
|
self.length(value, font_size) for value in
|
2021-04-29 18:54:04 +03:00
|
|
|
|
normalize(node.get('stroke-dasharray')).split() if value != 'none')
|
2021-04-12 15:25:55 +03:00
|
|
|
|
dash_condition = (
|
|
|
|
|
dash_array and
|
|
|
|
|
not all(value == 0 for value in dash_array) and
|
|
|
|
|
not any(value < 0 for value in dash_array))
|
|
|
|
|
if dash_condition:
|
|
|
|
|
offset = self.length(node.get('stroke-dashoffset'), font_size)
|
|
|
|
|
if offset < 0:
|
|
|
|
|
sum_dashes = sum(float(value) for value in dash_array)
|
|
|
|
|
offset = sum_dashes - abs(offset) % sum_dashes
|
|
|
|
|
self.stream.set_dash(dash_array, offset)
|
|
|
|
|
|
|
|
|
|
# Apply line cap
|
2021-04-11 17:13:59 +03:00
|
|
|
|
line_cap = node.get('stroke-linecap', 'butt')
|
|
|
|
|
if line_cap == 'round':
|
|
|
|
|
line_cap = 1
|
|
|
|
|
elif line_cap == 'square':
|
|
|
|
|
line_cap = 2
|
2021-02-22 01:25:35 +03:00
|
|
|
|
else:
|
|
|
|
|
line_cap = 0
|
2021-04-11 17:13:59 +03:00
|
|
|
|
self.stream.set_line_cap(line_cap)
|
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
# Apply line join
|
2021-04-11 17:13:59 +03:00
|
|
|
|
line_join = node.get('stroke-linejoin', 'miter')
|
|
|
|
|
if line_join == 'round':
|
2021-02-22 01:25:35 +03:00
|
|
|
|
line_join = 1
|
|
|
|
|
elif line_join == 'bevel':
|
|
|
|
|
line_join = 2
|
2021-04-11 17:13:59 +03:00
|
|
|
|
else:
|
|
|
|
|
line_join = 0
|
|
|
|
|
self.stream.set_line_join(line_join)
|
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
# Apply miter limit
|
2021-04-11 17:13:59 +03:00
|
|
|
|
miter_limit = float(node.get('stroke-miterlimit', 4))
|
2021-02-22 01:25:35 +03:00
|
|
|
|
if miter_limit < 0:
|
|
|
|
|
miter_limit = 4
|
|
|
|
|
self.stream.set_miter_limit(miter_limit)
|
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
# Fill and stroke
|
2021-03-19 17:58:22 +03:00
|
|
|
|
even_odd = node.get('fill-rule') == 'evenodd'
|
2021-04-20 18:15:17 +03:00
|
|
|
|
if text:
|
|
|
|
|
if stroke and fill:
|
|
|
|
|
text_rendering = 2
|
|
|
|
|
elif stroke:
|
|
|
|
|
text_rendering = 1
|
|
|
|
|
elif fill:
|
|
|
|
|
text_rendering = 0
|
|
|
|
|
else:
|
|
|
|
|
text_rendering = 3
|
|
|
|
|
self.stream.set_text_rendering(text_rendering)
|
2021-04-20 17:53:21 +03:00
|
|
|
|
else:
|
2021-04-20 18:15:17 +03:00
|
|
|
|
if fill and stroke:
|
|
|
|
|
self.stream.fill_and_stroke(even_odd)
|
|
|
|
|
elif stroke:
|
|
|
|
|
self.stream.stroke()
|
|
|
|
|
elif fill:
|
|
|
|
|
self.stream.fill(even_odd)
|
|
|
|
|
else:
|
|
|
|
|
self.stream.end()
|
2021-03-07 22:34:24 +03:00
|
|
|
|
|
|
|
|
|
def transform(self, transform_string, font_size):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Apply a transformation string to the node."""
|
2021-03-07 22:34:24 +03:00
|
|
|
|
if not transform_string:
|
|
|
|
|
return
|
|
|
|
|
|
2021-07-31 09:29:23 +03:00
|
|
|
|
matrix = transform(transform_string, font_size, self.inner_diagonal)
|
2021-03-07 22:34:24 +03:00
|
|
|
|
if matrix.determinant:
|
2021-07-17 14:46:46 +03:00
|
|
|
|
self.stream.transform(*matrix.values)
|
2021-03-12 17:47:49 +03:00
|
|
|
|
|
2021-04-12 15:25:55 +03:00
|
|
|
|
def parse_defs(self, node):
|
|
|
|
|
"""Parse defs included in a tree."""
|
2021-03-12 17:47:49 +03:00
|
|
|
|
for def_type in DEF_TYPES:
|
|
|
|
|
if def_type in node.tag.lower() and 'id' in node.attrib:
|
|
|
|
|
getattr(self, f'{def_type}s')[node.attrib['id']] = node
|
2021-04-12 15:25:55 +03:00
|
|
|
|
for child in node:
|
|
|
|
|
self.parse_defs(child)
|
2021-03-14 11:29:33 +03:00
|
|
|
|
|
2022-11-13 14:25:00 +03:00
|
|
|
|
def inherit_defs(self):
|
|
|
|
|
"""Handle inheritance of different defined elements lists."""
|
|
|
|
|
for defs in (self.gradients, self.patterns):
|
|
|
|
|
for element in defs.values():
|
|
|
|
|
self.inherit_element(element, defs)
|
|
|
|
|
|
|
|
|
|
def inherit_element(self, element, defs):
|
|
|
|
|
"""Recursively handle inheritance of defined element."""
|
|
|
|
|
href = element.get_href(self.url)
|
|
|
|
|
if not href:
|
|
|
|
|
return
|
|
|
|
|
element.del_href()
|
|
|
|
|
parent = defs.get(parse_url(href).fragment)
|
|
|
|
|
if not parent:
|
|
|
|
|
return
|
|
|
|
|
self.inherit_element(parent, defs)
|
|
|
|
|
for key, value in parent.attrib.items():
|
|
|
|
|
if key not in element.attrib:
|
|
|
|
|
element.attrib[key] = value
|
|
|
|
|
if next(iter(element), None) is None:
|
2023-04-29 17:30:41 +03:00
|
|
|
|
element.override_iter(parent.__iter__())
|
2022-11-13 14:25:00 +03:00
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
def calculate_bounding_box(self, node, font_size, stroke=True):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""Calculate the bounding box of a node."""
|
2021-06-02 21:35:06 +03:00
|
|
|
|
if stroke or node.bounding_box is None:
|
|
|
|
|
box = bounding_box(self, node, font_size, stroke)
|
2021-04-12 16:11:13 +03:00
|
|
|
|
if is_valid_bounding_box(box) and 0 not in box[2:]:
|
2021-06-02 21:35:06 +03:00
|
|
|
|
if stroke:
|
|
|
|
|
return box
|
2021-04-12 16:11:13 +03:00
|
|
|
|
node.bounding_box = box
|
2021-03-14 11:29:33 +03:00
|
|
|
|
return node.bounding_box
|
2021-03-24 16:10:50 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Pattern(SVG):
|
2021-04-12 15:25:55 +03:00
|
|
|
|
"""SVG node applied as a pattern."""
|
2021-07-12 12:39:52 +03:00
|
|
|
|
def __init__(self, tree, svg):
|
2022-02-06 22:35:39 +03:00
|
|
|
|
super().__init__(tree._etree_node, svg.url)
|
2021-07-12 12:39:52 +03:00
|
|
|
|
self.svg = svg
|
2021-03-24 16:10:50 +03:00
|
|
|
|
self.tree = tree
|
2021-07-12 12:39:52 +03:00
|
|
|
|
|
2021-07-18 10:36:14 +03:00
|
|
|
|
def draw_node(self, node, font_size, fill_stroke=True):
|
2021-07-12 12:39:52 +03:00
|
|
|
|
# Store the original tree in self.tree when calling draw(), so that we
|
|
|
|
|
# can reach defs outside the pattern
|
|
|
|
|
if node == self.tree:
|
|
|
|
|
self.tree = self.svg.tree
|
2021-07-18 10:36:14 +03:00
|
|
|
|
super().draw_node(node, font_size, fill_stroke=True)
|