ajout fichiers manquant
This commit is contained in:
742
venv/Lib/site-packages/weasyprint/svg/__init__.py
Normal file
742
venv/Lib/site-packages/weasyprint/svg/__init__.py
Normal file
@@ -0,0 +1,742 @@
|
||||
"""
|
||||
weasyprint.svg
|
||||
--------------
|
||||
|
||||
Render SVG images.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from math import cos, hypot, pi, radians, sin, sqrt
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from cssselect2 import ElementWrapper
|
||||
|
||||
from ..urls import get_url_attribute
|
||||
from .bounding_box import bounding_box, is_valid_bounding_box
|
||||
from .css import parse_declarations, parse_stylesheets
|
||||
from .defs import (
|
||||
apply_filters, clip_path, draw_gradient_or_pattern, paint_mask, use)
|
||||
from .images import image, svg
|
||||
from .path import path
|
||||
from .shapes import circle, ellipse, line, polygon, polyline, rect
|
||||
from .text import text
|
||||
from .utils import (
|
||||
PointError, color, normalize, parse_url, preserve_ratio, size, transform)
|
||||
|
||||
TAGS = {
|
||||
'a': text,
|
||||
'circle': circle,
|
||||
'clipPath': clip_path,
|
||||
'ellipse': ellipse,
|
||||
'image': image,
|
||||
'line': line,
|
||||
'path': path,
|
||||
'polyline': polyline,
|
||||
'polygon': polygon,
|
||||
'rect': rect,
|
||||
'svg': svg,
|
||||
'text': text,
|
||||
'textPath': text,
|
||||
'tspan': text,
|
||||
'use': use,
|
||||
}
|
||||
|
||||
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',
|
||||
))
|
||||
|
||||
DEF_TYPES = frozenset((
|
||||
'clipPath',
|
||||
'filter',
|
||||
'gradient',
|
||||
'image',
|
||||
'marker',
|
||||
'mask',
|
||||
'path',
|
||||
'pattern',
|
||||
))
|
||||
|
||||
|
||||
class Node:
|
||||
"""An SVG document node."""
|
||||
|
||||
def __init__(self, wrapper, style):
|
||||
self._wrapper = wrapper
|
||||
self._etree_node = wrapper.etree_element
|
||||
self._style = style
|
||||
|
||||
self.attrib = wrapper.etree_element.attrib.copy()
|
||||
|
||||
self.vertices = []
|
||||
self.bounding_box = None
|
||||
|
||||
def copy(self):
|
||||
"""Create a deep copy of the node as it was when first created."""
|
||||
return Node(self._wrapper, self._style)
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Get attribute."""
|
||||
return self.attrib.get(key, default)
|
||||
|
||||
@property
|
||||
def tag(self):
|
||||
"""XML tag name with no namespace."""
|
||||
return self._etree_node.tag.split('}', 1)[-1]
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""XML node text."""
|
||||
return self._etree_node.text
|
||||
|
||||
@property
|
||||
def tail(self):
|
||||
"""Text after the XML node."""
|
||||
return self._etree_node.tail
|
||||
|
||||
def __iter__(self):
|
||||
"""Yield node children, handling cascade."""
|
||||
for wrapper in self._wrapper:
|
||||
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:
|
||||
child.attrib[name] = value.strip()
|
||||
|
||||
# Replace 'currentColor' value
|
||||
for key in COLOR_ATTRIBUTES:
|
||||
if child.get(key) == 'currentColor':
|
||||
child.attrib[key] = child.get('color', 'black')
|
||||
|
||||
# Handle 'inherit' values
|
||||
for key, value in child.attrib.items():
|
||||
if value == 'inherit':
|
||||
child.attrib[key] = self.get(key)
|
||||
|
||||
# Fix text in text tags
|
||||
if child.tag in ('text', 'textPath', 'a'):
|
||||
children, _ = child.text_children(
|
||||
wrapper, trailing_space=True, text_root=True)
|
||||
child._wrapper.etree_children = [
|
||||
child._etree_node for child in children]
|
||||
|
||||
yield child
|
||||
|
||||
def get_viewbox(self):
|
||||
"""Get node viewBox as a tuple of floats."""
|
||||
viewbox = self.get('viewBox')
|
||||
if viewbox:
|
||||
return tuple(
|
||||
float(number) for number in normalize(viewbox).split())
|
||||
|
||||
def get_href(self, base_url):
|
||||
"""Get the href attribute, with or without a namespace."""
|
||||
for attr_name in ('{http://www.w3.org/1999/xlink}href', 'href'):
|
||||
url = get_url_attribute(self, attr_name, base_url)
|
||||
if url:
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def process_whitespace(string, preserve):
|
||||
"""Replace newlines by spaces, and merge spaces if not preserved."""
|
||||
# TODO: should be merged with build.process_whitespace
|
||||
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)
|
||||
|
||||
def get_child(self, id_):
|
||||
"""Get a child with given id in the whole child tree."""
|
||||
for child in self:
|
||||
if child.get('id') == id_:
|
||||
return child
|
||||
grandchild = child.get_child(id_)
|
||||
if grandchild:
|
||||
return grandchild
|
||||
|
||||
def text_children(self, element, trailing_space, text_root=False):
|
||||
"""Handle text node by fixing whitespaces and flattening tails."""
|
||||
children = []
|
||||
space = '{http://www.w3.org/XML/1998/namespace}space'
|
||||
preserve = self.get(space) == 'preserve'
|
||||
self._etree_node.text = self.process_whitespace(
|
||||
element.etree_element.text, preserve)
|
||||
if trailing_space and not preserve:
|
||||
self._etree_node.text = self.text.lstrip(' ')
|
||||
|
||||
original_rotate = [
|
||||
float(i) for i in
|
||||
normalize(self.get('rotate')).strip().split(' ') if i]
|
||||
rotate = original_rotate.copy()
|
||||
if original_rotate:
|
||||
self.pop_rotation(original_rotate, rotate)
|
||||
if self.text:
|
||||
trailing_space = self.text.endswith(' ')
|
||||
for child_element in element.iter_children():
|
||||
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
|
||||
child._etree_node.text = child.flatten()
|
||||
child_element = ElementWrapper.from_xml_root(child)
|
||||
else:
|
||||
child_node = Node(child_element, self._style)
|
||||
child_preserve = child_node.get(space) == 'preserve'
|
||||
child_node._etree_node.text = self.process_whitespace(
|
||||
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:
|
||||
child_node.pop_rotation(original_rotate, rotate)
|
||||
children.append(child_node)
|
||||
if child.tail:
|
||||
anonymous_etree = ElementTree.Element(
|
||||
'{http://www.w3.org/2000/svg}tspan')
|
||||
anonymous = Node(
|
||||
ElementWrapper.from_xml_root(anonymous_etree), self._style)
|
||||
anonymous._etree_node.text = self.process_whitespace(
|
||||
child.tail, preserve)
|
||||
if original_rotate:
|
||||
anonymous.pop_rotation(original_rotate, rotate)
|
||||
if trailing_space and not preserve:
|
||||
anonymous._etree_node.text = anonymous.text.lstrip(' ')
|
||||
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
|
||||
|
||||
def flatten(self):
|
||||
"""Flatten text in node and in its children."""
|
||||
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):
|
||||
"""Merge nested letter rotations."""
|
||||
self.attrib['rotate'] = ' '.join(
|
||||
str(rotate.pop(0) if rotate else original_rotate[-1])
|
||||
for i in range(len(self.text)))
|
||||
|
||||
|
||||
class SVG:
|
||||
"""An SVG document."""
|
||||
|
||||
def __init__(self, tree, url):
|
||||
wrapper = ElementWrapper.from_xml_root(tree)
|
||||
style = parse_stylesheets(wrapper, url)
|
||||
self.tree = Node(wrapper, style)
|
||||
self.url = url
|
||||
|
||||
self.filters = {}
|
||||
self.gradients = {}
|
||||
self.images = {}
|
||||
self.markers = {}
|
||||
self.masks = {}
|
||||
self.patterns = {}
|
||||
self.paths = {}
|
||||
|
||||
self.use_cache = {}
|
||||
|
||||
self.cursor_position = [0, 0]
|
||||
self.cursor_d_position = [0, 0]
|
||||
self.text_path_width = 0
|
||||
|
||||
self.parse_defs(self.tree)
|
||||
|
||||
def get_intrinsic_size(self, font_size):
|
||||
"""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
|
||||
|
||||
def get_viewbox(self):
|
||||
"""Get document viewBox as a tuple of floats."""
|
||||
return self.tree.get_viewbox()
|
||||
|
||||
def point(self, x, y, font_size):
|
||||
"""Compute size of an x/y or width/height couple."""
|
||||
return (
|
||||
size(x, font_size, self.inner_width),
|
||||
size(y, font_size, self.inner_height))
|
||||
|
||||
def length(self, length, font_size):
|
||||
"""Compute size of an arbirtary attribute."""
|
||||
return size(length, font_size, self.inner_diagonal)
|
||||
|
||||
def draw(self, stream, concrete_width, concrete_height, base_url,
|
||||
url_fetcher, context):
|
||||
"""Draw image on a stream."""
|
||||
self.stream = stream
|
||||
|
||||
self.concrete_width = concrete_width
|
||||
self.concrete_height = concrete_height
|
||||
self.normalized_diagonal = (
|
||||
hypot(concrete_width, concrete_height) / sqrt(2))
|
||||
|
||||
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))
|
||||
|
||||
self.base_url = base_url
|
||||
self.url_fetcher = url_fetcher
|
||||
self.context = context
|
||||
|
||||
self.draw_node(self.tree, size('12pt'))
|
||||
|
||||
def draw_node(self, node, font_size, fill_stroke=True):
|
||||
"""Draw a node."""
|
||||
if node.tag == 'defs':
|
||||
return
|
||||
|
||||
# Update font size
|
||||
font_size = size(node.get('font-size', '1em'), font_size, font_size)
|
||||
|
||||
if fill_stroke:
|
||||
self.stream.push_state()
|
||||
|
||||
# Apply filters
|
||||
filter_ = self.filters.get(parse_url(node.get('filter')).fragment)
|
||||
if filter_:
|
||||
apply_filters(self, node, filter_, font_size)
|
||||
|
||||
# Create substream for opacity
|
||||
opacity = float(node.get('opacity', 1))
|
||||
if fill_stroke and 0 <= opacity < 1:
|
||||
original_stream = self.stream
|
||||
box = self.calculate_bounding_box(node, font_size)
|
||||
if is_valid_bounding_box(box):
|
||||
coords = (box[0], box[1], box[0] + box[2], box[1] + box[3])
|
||||
else:
|
||||
coords = (0, 0, self.concrete_width, self.concrete_height)
|
||||
self.stream = self.stream.add_group(coords)
|
||||
|
||||
# Apply transform attribute
|
||||
self.transform(node.get('transform'), font_size)
|
||||
|
||||
# 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)
|
||||
clip_path._etree_node.tag = 'g'
|
||||
self.draw_node(clip_path, font_size, fill_stroke=False)
|
||||
# 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)
|
||||
|
||||
# Manage display and visibility
|
||||
display = node.get('display') != 'none'
|
||||
visible = display and (node.get('visibility') != 'hidden')
|
||||
|
||||
# Draw node
|
||||
if visible and node.tag in TAGS:
|
||||
try:
|
||||
TAGS[node.tag](self, node, font_size)
|
||||
except PointError:
|
||||
pass
|
||||
|
||||
# Draw node children
|
||||
if display and node.tag not in DEF_TYPES:
|
||||
for child in node:
|
||||
self.draw_node(child, font_size, fill_stroke)
|
||||
|
||||
# Apply mask
|
||||
mask = self.masks.get(parse_url(node.get('mask')).fragment)
|
||||
if mask:
|
||||
paint_mask(self, node, mask, opacity)
|
||||
|
||||
# Fill and stroke
|
||||
if fill_stroke:
|
||||
self.fill_stroke(node, font_size)
|
||||
|
||||
# Draw markers
|
||||
self.draw_markers(node, font_size, fill_stroke)
|
||||
|
||||
# Apply opacity stream and restore original stream
|
||||
if fill_stroke and 0 <= opacity < 1:
|
||||
group_id = self.stream.id
|
||||
self.stream = original_stream
|
||||
self.stream.set_alpha(opacity, stroke=True, fill=True)
|
||||
self.stream.draw_x_object(group_id)
|
||||
|
||||
# Clean text tag
|
||||
if node.tag == 'text':
|
||||
self.cursor_position = [0, 0]
|
||||
self.cursor_d_position = [0, 0]
|
||||
self.text_path_width = 0
|
||||
|
||||
if fill_stroke:
|
||||
self.stream.pop_state()
|
||||
|
||||
def draw_markers(self, node, font_size, fill_stroke):
|
||||
"""Draw markers defined in a node."""
|
||||
if not node.vertices:
|
||||
return
|
||||
|
||||
markers = {}
|
||||
common_marker = parse_url(node.get('marker')).fragment
|
||||
for position in ('start', 'mid', 'end'):
|
||||
attribute = f'marker-{position}'
|
||||
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'
|
||||
|
||||
# Draw marker
|
||||
marker = markers[position]
|
||||
if not marker:
|
||||
position = 'mid' if angles else 'start'
|
||||
continue
|
||||
|
||||
marker_node = self.markers.get(marker)
|
||||
|
||||
# Calculate position, scale and clipping
|
||||
if 'viewBox' in node.attrib:
|
||||
marker_width, marker_height = svg.point(
|
||||
marker_node.get('markerWidth', 3),
|
||||
marker_node.get('markerHeight', 3),
|
||||
font_size)
|
||||
scale_x, scale_y, translate_x, translate_y = preserve_ratio(
|
||||
svg, marker_node, font_size, marker_width, marker_height)
|
||||
|
||||
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:
|
||||
marker_width, marker_height = self.point(
|
||||
marker_node.get('markerWidth', 3),
|
||||
marker_node.get('markerHeight', 3),
|
||||
font_size)
|
||||
box = self.calculate_bounding_box(marker_node, font_size)
|
||||
if is_valid_bounding_box(box):
|
||||
scale_x = scale_y = min(
|
||||
marker_width / box[2], marker_height / box[3])
|
||||
else:
|
||||
scale_x = scale_y = 1
|
||||
translate_x, translate_y = self.point(
|
||||
marker_node.get('refX'), marker_node.get('refY'),
|
||||
font_size)
|
||||
clip_box = None
|
||||
|
||||
# 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)
|
||||
self.stream.transform(e=-translate_x, f=-translate_y)
|
||||
|
||||
overflow = marker_node.get('overflow', 'hidden')
|
||||
if clip_box and overflow in ('hidden', 'scroll'):
|
||||
self.stream.push_state()
|
||||
self.stream.rectangle(*clip_box)
|
||||
self.stream.pop_state()
|
||||
self.stream.clip()
|
||||
|
||||
self.draw_node(child, font_size, fill_stroke)
|
||||
self.stream.pop_state()
|
||||
|
||||
position = 'mid' if angles else 'start'
|
||||
|
||||
@staticmethod
|
||||
def get_paint(value):
|
||||
"""Get paint fill or stroke attribute with a color or a URL."""
|
||||
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
|
||||
|
||||
def fill_stroke(self, node, font_size, text=False):
|
||||
"""Paint fill and stroke for a node."""
|
||||
if node.tag in ('text', 'textPath', 'a') and not text:
|
||||
return
|
||||
|
||||
# Get fill data
|
||||
fill_source, fill_color = self.get_paint(node.get('fill', 'black'))
|
||||
fill_opacity = float(node.get('fill-opacity', 1))
|
||||
fill_drawn = draw_gradient_or_pattern(
|
||||
self, node, fill_source, font_size, fill_opacity, stroke=False)
|
||||
if fill_color and not fill_drawn:
|
||||
red, green, blue, alpha = color(fill_color)
|
||||
self.stream.set_color_rgb(red, green, blue)
|
||||
self.stream.set_alpha(alpha * fill_opacity)
|
||||
fill = fill_color or fill_drawn
|
||||
|
||||
# Get stroke data
|
||||
stroke_source, stroke_color = self.get_paint(node.get('stroke'))
|
||||
stroke_opacity = float(node.get('stroke-opacity', 1))
|
||||
stroke_drawn = draw_gradient_or_pattern(
|
||||
self, node, stroke_source, font_size, stroke_opacity, stroke=True)
|
||||
if stroke_color and not stroke_drawn:
|
||||
red, green, blue, alpha = color(stroke_color)
|
||||
self.stream.set_color_rgb(red, green, blue, stroke=True)
|
||||
self.stream.set_alpha(alpha * stroke_opacity, stroke=True)
|
||||
stroke = stroke_color or stroke_drawn
|
||||
stroke_width = self.length(node.get('stroke-width', '1px'), font_size)
|
||||
if stroke_width:
|
||||
self.stream.set_line_width(stroke_width)
|
||||
else:
|
||||
stroke = None
|
||||
|
||||
# Apply dash array
|
||||
dash_array = tuple(
|
||||
self.length(value, font_size) for value in
|
||||
normalize(node.get('stroke-dasharray')).split() if value != 'none')
|
||||
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
|
||||
line_cap = node.get('stroke-linecap', 'butt')
|
||||
if line_cap == 'round':
|
||||
line_cap = 1
|
||||
elif line_cap == 'square':
|
||||
line_cap = 2
|
||||
else:
|
||||
line_cap = 0
|
||||
self.stream.set_line_cap(line_cap)
|
||||
|
||||
# Apply line join
|
||||
line_join = node.get('stroke-linejoin', 'miter')
|
||||
if line_join == 'round':
|
||||
line_join = 1
|
||||
elif line_join == 'bevel':
|
||||
line_join = 2
|
||||
else:
|
||||
line_join = 0
|
||||
self.stream.set_line_join(line_join)
|
||||
|
||||
# Apply miter limit
|
||||
miter_limit = float(node.get('stroke-miterlimit', 4))
|
||||
if miter_limit < 0:
|
||||
miter_limit = 4
|
||||
self.stream.set_miter_limit(miter_limit)
|
||||
|
||||
# Fill and stroke
|
||||
even_odd = node.get('fill-rule') == 'evenodd'
|
||||
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)
|
||||
else:
|
||||
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()
|
||||
|
||||
def transform(self, transform_string, font_size):
|
||||
"""Apply a transformation string to the node."""
|
||||
if not transform_string:
|
||||
return
|
||||
|
||||
matrix = transform(transform_string, font_size, self.inner_diagonal)
|
||||
if matrix.determinant:
|
||||
self.stream.transform(*matrix.values)
|
||||
|
||||
def parse_defs(self, node):
|
||||
"""Parse defs included in a tree."""
|
||||
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
|
||||
for child in node:
|
||||
self.parse_defs(child)
|
||||
|
||||
def calculate_bounding_box(self, node, font_size, stroke=True):
|
||||
"""Calculate the bounding box of a node."""
|
||||
if stroke or node.bounding_box is None:
|
||||
box = bounding_box(self, node, font_size, stroke)
|
||||
if is_valid_bounding_box(box) and 0 not in box[2:]:
|
||||
if stroke:
|
||||
return box
|
||||
node.bounding_box = box
|
||||
return node.bounding_box
|
||||
|
||||
|
||||
class Pattern(SVG):
|
||||
"""SVG node applied as a pattern."""
|
||||
def __init__(self, tree, svg):
|
||||
self.svg = svg
|
||||
self.tree = tree
|
||||
self.url = svg.url
|
||||
|
||||
self.filters = {}
|
||||
self.gradients = {}
|
||||
self.images = {}
|
||||
self.markers = {}
|
||||
self.masks = {}
|
||||
self.patterns = {}
|
||||
self.paths = {}
|
||||
|
||||
def draw_node(self, node, font_size, fill_stroke=True):
|
||||
# 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
|
||||
super().draw_node(node, font_size, fill_stroke=True)
|
||||
126
venv/Lib/site-packages/weasyprint/svg/shapes.py
Normal file
126
venv/Lib/site-packages/weasyprint/svg/shapes.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
weasyprint.svg.shapes
|
||||
---------------------
|
||||
|
||||
Draw simple shapes.
|
||||
|
||||
"""
|
||||
|
||||
from math import atan2, pi, sqrt
|
||||
|
||||
from .utils import normalize, point
|
||||
|
||||
|
||||
def circle(svg, node, font_size):
|
||||
"""Draw circle tag."""
|
||||
r = svg.length(node.get('r'), font_size)
|
||||
if not r:
|
||||
return
|
||||
ratio = r / sqrt(pi)
|
||||
cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)
|
||||
|
||||
svg.stream.move_to(cx + r, cy)
|
||||
svg.stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)
|
||||
svg.stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)
|
||||
svg.stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)
|
||||
svg.stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def ellipse(svg, node, font_size):
|
||||
"""Draw ellipse tag."""
|
||||
rx, ry = svg.point(node.get('rx'), node.get('ry'), font_size)
|
||||
if not rx or not ry:
|
||||
return
|
||||
ratio_x = rx / sqrt(pi)
|
||||
ratio_y = ry / sqrt(pi)
|
||||
cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)
|
||||
|
||||
svg.stream.move_to(cx + rx, cy)
|
||||
svg.stream.curve_to(
|
||||
cx + rx, cy + ratio_y, cx + ratio_x, cy + ry, cx, cy + ry)
|
||||
svg.stream.curve_to(
|
||||
cx - ratio_x, cy + ry, cx - rx, cy + ratio_y, cx - rx, cy)
|
||||
svg.stream.curve_to(
|
||||
cx - rx, cy - ratio_y, cx - ratio_x, cy - ry, cx, cy - ry)
|
||||
svg.stream.curve_to(
|
||||
cx + ratio_x, cy - ry, cx + rx, cy - ratio_y, cx + rx, cy)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def rect(svg, node, font_size):
|
||||
"""Draw rect tag."""
|
||||
width, height = svg.point(node.get('width'), node.get('height'), font_size)
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
|
||||
x, y = svg.point(node.get('x'), node.get('y'), font_size)
|
||||
|
||||
rx = node.get('rx')
|
||||
ry = node.get('ry')
|
||||
if rx and ry is None:
|
||||
ry = rx
|
||||
elif ry and rx is None:
|
||||
rx = ry
|
||||
rx, ry = svg.point(rx, ry, font_size)
|
||||
|
||||
if rx == 0 or ry == 0:
|
||||
svg.stream.rectangle(x, y, width, height)
|
||||
return
|
||||
|
||||
if rx > width / 2:
|
||||
rx = width / 2
|
||||
if ry > height / 2:
|
||||
ry = height / 2
|
||||
|
||||
# Inspired by Cairo Cookbook
|
||||
# http://cairographics.org/cookbook/roundedrectangles/
|
||||
ARC_TO_BEZIER = 4 * (2 ** .5 - 1) / 3
|
||||
c1, c2 = ARC_TO_BEZIER * rx, ARC_TO_BEZIER * ry
|
||||
|
||||
svg.stream.move_to(x + rx, y)
|
||||
svg.stream.line_to(x + width - rx, y)
|
||||
svg.stream.curve_to(
|
||||
x + width - rx + c1, y, x + width, y + c2, x + width, y + ry)
|
||||
svg.stream.line_to(x + width, y + height - ry)
|
||||
svg.stream.curve_to(
|
||||
x + width, y + height - ry + c2, x + width + c1 - rx, y + height,
|
||||
x + width - rx, y + height)
|
||||
svg.stream.line_to(x + rx, y + height)
|
||||
svg.stream.curve_to(
|
||||
x + rx - c1, y + height, x, y + height - c2, x, y + height - ry)
|
||||
svg.stream.line_to(x, y + ry)
|
||||
svg.stream.curve_to(x, y + ry - c2, x + rx - c1, y, x + rx, y)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def line(svg, node, font_size):
|
||||
"""Draw line tag."""
|
||||
x1, y1 = svg.point(node.get('x1'), node.get('y1'), font_size)
|
||||
x2, y2 = svg.point(node.get('x2'), node.get('y2'), font_size)
|
||||
svg.stream.move_to(x1, y1)
|
||||
svg.stream.line_to(x2, y2)
|
||||
angle = atan2(y2 - y1, x2 - x1)
|
||||
node.vertices = [(x1, y1), (pi - angle, angle), (x2, y2)]
|
||||
|
||||
|
||||
def polygon(svg, node, font_size):
|
||||
"""Draw polygon tag."""
|
||||
polyline(svg, node, font_size)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def polyline(svg, node, font_size):
|
||||
"""Draw polyline tag."""
|
||||
points = normalize(node.get('points'))
|
||||
if points:
|
||||
x, y, points = point(svg, points, font_size)
|
||||
svg.stream.move_to(x, y)
|
||||
node.vertices = [(x, y)]
|
||||
while points:
|
||||
x_old, y_old = x, y
|
||||
x, y, points = point(svg, points, font_size)
|
||||
angle = atan2(x - x_old, y - y_old)
|
||||
node.vertices.append((pi - angle, angle))
|
||||
svg.stream.line_to(x, y)
|
||||
node.vertices.append((x, y))
|
||||
Reference in New Issue
Block a user