2022-03-25 13:47:27 +03:00
|
|
|
|
"""Parse and draw definitions: gradients, patterns, masks, uses…"""
|
2021-04-12 17:27:55 +03:00
|
|
|
|
|
2021-03-19 17:58:22 +03:00
|
|
|
|
from itertools import cycle
|
2021-03-20 11:41:37 +03:00
|
|
|
|
from math import ceil, hypot
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
2022-03-19 19:22:31 +03:00
|
|
|
|
from ..matrix import Matrix
|
2021-04-12 16:11:13 +03:00
|
|
|
|
from .bounding_box import is_valid_bounding_box
|
2021-04-16 16:33:04 +03:00
|
|
|
|
from .utils import color, parse_url, size, transform
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
|
|
|
|
|
2021-03-15 16:21:36 +03:00
|
|
|
|
def use(svg, node, font_size):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Draw use tags."""
|
2021-05-27 13:04:41 +03:00
|
|
|
|
from . import NOT_INHERITED_ATTRIBUTES, SVG
|
2021-03-15 16:21:36 +03:00
|
|
|
|
|
2021-07-30 17:32:25 +03:00
|
|
|
|
x, y = svg.point(node.get('x'), node.get('y'), font_size)
|
|
|
|
|
|
2021-03-15 16:21:36 +03:00
|
|
|
|
for attribute in ('x', 'y', 'viewBox', 'mask'):
|
|
|
|
|
if attribute in node.attrib:
|
|
|
|
|
del node.attrib[attribute]
|
|
|
|
|
|
2022-01-03 16:14:36 +03:00
|
|
|
|
parsed_url = parse_url(node.get_href(svg.url))
|
2022-05-10 15:29:34 +03:00
|
|
|
|
svg_url = parse_url(svg.url)
|
|
|
|
|
if svg_url.scheme == 'data':
|
|
|
|
|
svg_url = parse_url('')
|
2022-05-27 18:32:16 +03:00
|
|
|
|
same_origin = (
|
|
|
|
|
parsed_url[:3] == ('', '', '') or
|
|
|
|
|
parsed_url[:3] == svg_url[:3])
|
|
|
|
|
if parsed_url.fragment and same_origin:
|
2021-09-11 15:57:47 +03:00
|
|
|
|
if parsed_url.fragment in svg.use_cache:
|
|
|
|
|
tree = svg.use_cache[parsed_url.fragment].copy()
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
tree = svg.tree.get_child(parsed_url.fragment).copy()
|
2022-01-03 16:14:36 +03:00
|
|
|
|
except Exception:
|
2021-09-11 15:57:47 +03:00
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
svg.use_cache[parsed_url.fragment] = tree
|
2021-03-15 16:21:36 +03:00
|
|
|
|
else:
|
|
|
|
|
url = parsed_url.geturl()
|
|
|
|
|
try:
|
|
|
|
|
bytestring_svg = svg.url_fetcher(url)
|
|
|
|
|
use_svg = SVG(bytestring_svg, url)
|
2022-01-03 16:14:36 +03:00
|
|
|
|
except Exception:
|
2021-03-15 16:21:36 +03:00
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
use_svg.get_intrinsic_size(font_size)
|
|
|
|
|
tree = use_svg.tree
|
|
|
|
|
|
|
|
|
|
if tree.tag in ('svg', 'symbol'):
|
|
|
|
|
# Explicitely specified
|
2022-06-28 16:57:49 +03:00
|
|
|
|
# https://www.w3.org/TR/SVG11/struct.html#UseElement
|
2021-07-30 16:42:51 +03:00
|
|
|
|
tree._etree_node.tag = 'svg'
|
2021-03-15 16:21:36 +03:00
|
|
|
|
if 'width' in node.attrib and 'height' in node.attrib:
|
2021-07-30 16:49:10 +03:00
|
|
|
|
tree.attrib['width'] = node.attrib['width']
|
|
|
|
|
tree.attrib['height'] = node.attrib['height']
|
2021-03-15 16:21:36 +03:00
|
|
|
|
|
2021-05-27 13:04:41 +03:00
|
|
|
|
# Cascade
|
|
|
|
|
for key, value in node.attrib.items():
|
|
|
|
|
if key not in NOT_INHERITED_ATTRIBUTES:
|
|
|
|
|
if key not in tree.attrib:
|
|
|
|
|
tree.attrib[key] = value
|
|
|
|
|
|
2023-04-29 17:30:41 +03:00
|
|
|
|
node.override_iter(iter((tree,)))
|
2021-07-30 17:32:25 +03:00
|
|
|
|
svg.stream.transform(e=x, f=y)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
def draw_gradient_or_pattern(svg, node, name, font_size, opacity, stroke):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Draw given gradient or pattern."""
|
2021-03-19 17:58:22 +03:00
|
|
|
|
if name in svg.gradients:
|
2021-06-02 21:35:06 +03:00
|
|
|
|
return draw_gradient(
|
|
|
|
|
svg, node, svg.gradients[name], font_size, opacity, stroke)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
elif name in svg.patterns:
|
2021-06-02 21:35:06 +03:00
|
|
|
|
return draw_pattern(
|
|
|
|
|
svg, node, svg.patterns[name], font_size, opacity, stroke)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
def draw_gradient(svg, node, gradient, font_size, opacity, stroke):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Draw given gradient node."""
|
|
|
|
|
# TODO: merge with Gradient.draw
|
2021-03-19 17:58:22 +03:00
|
|
|
|
positions = []
|
|
|
|
|
colors = []
|
|
|
|
|
for child in gradient:
|
|
|
|
|
positions.append(max(
|
|
|
|
|
positions[-1] if positions else 0,
|
|
|
|
|
size(child.get('offset'), font_size, 1)))
|
2021-06-02 21:35:06 +03:00
|
|
|
|
stop_opacity = float(child.get('stop-opacity', 1)) * opacity
|
2021-04-05 13:11:36 +03:00
|
|
|
|
stop_color = color(child.get('stop-color', 'black'))
|
2021-06-02 21:35:06 +03:00
|
|
|
|
if stop_opacity < 1:
|
|
|
|
|
stop_color = tuple(
|
|
|
|
|
stop_color[:3] + (stop_color[3] * stop_opacity,))
|
2021-04-05 13:11:36 +03:00
|
|
|
|
colors.append(stop_color)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
2021-04-05 13:11:36 +03:00
|
|
|
|
if not colors:
|
|
|
|
|
return False
|
|
|
|
|
elif len(colors) == 1:
|
2021-03-19 17:58:22 +03:00
|
|
|
|
red, green, blue, alpha = colors[0]
|
|
|
|
|
svg.stream.set_color_rgb(red, green, blue)
|
|
|
|
|
if alpha != 1:
|
|
|
|
|
svg.stream.set_alpha(alpha, stroke=stroke)
|
|
|
|
|
return True
|
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
bounding_box = svg.calculate_bounding_box(node, font_size, stroke)
|
2021-04-12 16:11:13 +03:00
|
|
|
|
if not is_valid_bounding_box(bounding_box):
|
2021-03-20 11:41:37 +03:00
|
|
|
|
return False
|
|
|
|
|
if gradient.get('gradientUnits') == 'userSpaceOnUse':
|
2021-07-31 09:29:23 +03:00
|
|
|
|
width, height = svg.inner_width, svg.inner_height
|
2022-11-19 00:59:56 +03:00
|
|
|
|
matrix = Matrix()
|
2021-03-20 11:41:37 +03:00
|
|
|
|
else:
|
2022-11-19 00:59:56 +03:00
|
|
|
|
width, height = 1, 1
|
|
|
|
|
e, f, a, d = bounding_box
|
|
|
|
|
matrix = Matrix(a=a, d=d, e=e, f=f)
|
2021-04-05 13:11:36 +03:00
|
|
|
|
|
2021-03-19 17:58:22 +03:00
|
|
|
|
spread = gradient.get('spreadMethod', 'pad')
|
2021-07-25 19:30:32 +03:00
|
|
|
|
if spread in ('repeat', 'reflect'):
|
|
|
|
|
if positions[0] > 0:
|
|
|
|
|
positions.insert(0, 0)
|
|
|
|
|
colors.insert(0, colors[0])
|
|
|
|
|
if positions[-1] < 1:
|
|
|
|
|
positions.append(1)
|
|
|
|
|
colors.append(colors[-1])
|
|
|
|
|
else:
|
2021-03-19 17:58:22 +03:00
|
|
|
|
# Add explicit colors at boundaries if needed, because PDF doesn’t
|
|
|
|
|
# extend color stops that are not displayed
|
|
|
|
|
if positions[0] == positions[1]:
|
2021-04-15 18:52:18 +03:00
|
|
|
|
if gradient.tag == 'radialGradient':
|
|
|
|
|
# Avoid negative radius for radial gradients
|
|
|
|
|
positions.insert(0, 0)
|
|
|
|
|
else:
|
|
|
|
|
positions.insert(0, positions[0] - 1)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
colors.insert(0, colors[0])
|
|
|
|
|
if positions[-2] == positions[-1]:
|
|
|
|
|
positions.append(positions[-1] + 1)
|
|
|
|
|
colors.append(colors[-1])
|
|
|
|
|
|
2022-12-02 17:31:06 +03:00
|
|
|
|
if 'gradientTransform' in gradient.attrib:
|
|
|
|
|
transform_matrix = transform(
|
|
|
|
|
gradient.get('gradientTransform'), font_size,
|
|
|
|
|
svg.normalized_diagonal)
|
|
|
|
|
matrix = transform_matrix @ matrix
|
|
|
|
|
|
2021-03-20 11:41:37 +03:00
|
|
|
|
if gradient.tag == 'linearGradient':
|
|
|
|
|
shading_type = 2
|
|
|
|
|
x1, y1 = (
|
2022-11-19 00:59:56 +03:00
|
|
|
|
size(gradient.get('x1', 0), font_size, width),
|
|
|
|
|
size(gradient.get('y1', 0), font_size, height))
|
2021-03-20 11:41:37 +03:00
|
|
|
|
x2, y2 = (
|
2022-11-19 00:59:56 +03:00
|
|
|
|
size(gradient.get('x2', '100%'), font_size, width),
|
|
|
|
|
size(gradient.get('y2', 0), font_size, height))
|
2021-03-20 11:41:37 +03:00
|
|
|
|
positions, colors, coords = spread_linear_gradient(
|
2022-12-02 17:31:06 +03:00
|
|
|
|
spread, positions, colors, x1, y1, x2, y2, bounding_box, matrix)
|
2021-03-20 11:41:37 +03:00
|
|
|
|
else:
|
|
|
|
|
assert gradient.tag == 'radialGradient'
|
|
|
|
|
shading_type = 3
|
|
|
|
|
cx, cy = (
|
2022-11-19 00:59:56 +03:00
|
|
|
|
size(gradient.get('cx', '50%'), font_size, width),
|
|
|
|
|
size(gradient.get('cy', '50%'), font_size, height))
|
|
|
|
|
r = size(gradient.get('r', '50%'), font_size, hypot(width, height))
|
2021-03-20 11:41:37 +03:00
|
|
|
|
fx, fy = (
|
|
|
|
|
size(gradient.get('fx', cx), font_size, width),
|
|
|
|
|
size(gradient.get('fy', cy), font_size, height))
|
2022-11-19 00:59:56 +03:00
|
|
|
|
fr = size(gradient.get('fr', 0), font_size, hypot(width, height))
|
2021-03-20 11:41:37 +03:00
|
|
|
|
positions, colors, coords = spread_radial_gradient(
|
2022-11-14 11:39:50 +03:00
|
|
|
|
spread, positions, colors, fx, fy, fr, cx, cy, r, width, height,
|
|
|
|
|
matrix)
|
|
|
|
|
|
2021-03-19 17:58:22 +03:00
|
|
|
|
alphas = [color[3] for color in colors]
|
|
|
|
|
alpha_couples = [
|
|
|
|
|
(alphas[i], alphas[i + 1])
|
|
|
|
|
for i in range(len(alphas) - 1)]
|
|
|
|
|
color_couples = [
|
|
|
|
|
[colors[i][:3], colors[i + 1][:3], 1]
|
|
|
|
|
for i in range(len(colors) - 1)]
|
|
|
|
|
|
|
|
|
|
# Premultiply colors
|
|
|
|
|
for i, alpha in enumerate(alphas):
|
|
|
|
|
if alpha == 0:
|
|
|
|
|
if i > 0:
|
|
|
|
|
color_couples[i - 1][1] = color_couples[i - 1][0]
|
|
|
|
|
if i < len(colors) - 1:
|
|
|
|
|
color_couples[i][0] = color_couples[i][1]
|
|
|
|
|
for i, (a0, a1) in enumerate(alpha_couples):
|
|
|
|
|
if 0 not in (a0, a1) and (a0, a1) != (1, 1):
|
|
|
|
|
color_couples[i][2] = a0 / a1
|
|
|
|
|
|
2022-12-02 17:31:06 +03:00
|
|
|
|
bx1, by1 = 0, 0
|
2021-04-26 18:59:20 +03:00
|
|
|
|
if 'gradientTransform' in gradient.attrib:
|
2022-12-02 17:31:06 +03:00
|
|
|
|
bx1, by1 = transform_matrix.invert.transform_point(bx1, by1)
|
|
|
|
|
bx2, by2 = transform_matrix.invert.transform_point(width, height)
|
|
|
|
|
width, height = bx2 - bx1, by2 - by1
|
2021-04-26 18:59:20 +03:00
|
|
|
|
|
2022-11-14 11:39:50 +03:00
|
|
|
|
pattern = svg.stream.add_pattern(
|
2022-12-02 17:31:06 +03:00
|
|
|
|
bx1, by1, width, height, width, height, matrix @ svg.stream.ctm)
|
|
|
|
|
group = pattern.add_group(bx1, by1, width, height)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
2022-05-27 12:26:15 +03:00
|
|
|
|
domain = (positions[0], positions[-1])
|
|
|
|
|
extend = spread not in ('repeat', 'reflect')
|
|
|
|
|
encode = (len(colors) - 1) * (0, 1)
|
|
|
|
|
bounds = positions[1:-1]
|
|
|
|
|
sub_functions = (
|
2022-05-23 16:57:56 +03:00
|
|
|
|
group.create_interpolation_function(domain, c0, c1, n)
|
2022-05-27 12:26:15 +03:00
|
|
|
|
for c0, c1, n in color_couples)
|
|
|
|
|
function = group.create_stitching_function(
|
|
|
|
|
domain, encode, bounds, sub_functions)
|
|
|
|
|
shading = group.add_shading(
|
|
|
|
|
shading_type, 'RGB', domain, coords, extend, function)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
|
|
|
|
|
if any(alpha != 1 for alpha in alphas):
|
2022-12-02 17:31:06 +03:00
|
|
|
|
alpha_stream = group.set_alpha_state(bx1, by1, width, height)
|
2022-05-27 12:26:15 +03:00
|
|
|
|
domain = (positions[0], positions[-1])
|
|
|
|
|
extend = spread not in ('repeat', 'reflect')
|
|
|
|
|
encode = (len(colors) - 1) * (0, 1)
|
|
|
|
|
bounds = positions[1:-1]
|
|
|
|
|
sub_functions = (
|
2022-07-10 21:37:33 +03:00
|
|
|
|
group.create_interpolation_function((0, 1), [c0], [c1], 1)
|
2022-05-27 12:26:15 +03:00
|
|
|
|
for c0, c1 in alpha_couples)
|
|
|
|
|
function = group.create_stitching_function(
|
|
|
|
|
domain, encode, bounds, sub_functions)
|
|
|
|
|
alpha_shading = alpha_stream.add_shading(
|
|
|
|
|
shading_type, 'Gray', domain, coords, extend, function)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
alpha_stream.stream = [f'/{alpha_shading.id} sh']
|
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
group.shading(shading.id)
|
2022-09-27 11:59:31 +03:00
|
|
|
|
pattern.set_alpha(1)
|
2021-06-02 21:35:06 +03:00
|
|
|
|
pattern.draw_x_object(group.id)
|
2021-03-19 17:58:22 +03:00
|
|
|
|
svg.stream.color_space('Pattern', stroke=stroke)
|
|
|
|
|
svg.stream.set_color_special(pattern.id, stroke=stroke)
|
|
|
|
|
return True
|
2021-03-20 11:41:37 +03:00
|
|
|
|
|
|
|
|
|
|
2022-12-02 17:31:06 +03:00
|
|
|
|
def spread_linear_gradient(spread, positions, colors, x1, y1, x2, y2,
|
|
|
|
|
bounding_box, matrix):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Repeat linear gradient."""
|
|
|
|
|
# TODO: merge with LinearGradient.layout
|
2021-03-20 11:41:37 +03:00
|
|
|
|
from ..images import gradient_average_color, normalize_stop_positions
|
|
|
|
|
|
|
|
|
|
first, last, positions = normalize_stop_positions(positions)
|
|
|
|
|
if spread in ('repeat', 'reflect'):
|
|
|
|
|
# Render as a solid color if the first and last positions are equal
|
|
|
|
|
# See https://drafts.csswg.org/css-images-3/#repeating-gradients
|
|
|
|
|
if first == last:
|
|
|
|
|
average_color = gradient_average_color(colors, positions)
|
|
|
|
|
return 1, 'solid', None, [], [average_color]
|
|
|
|
|
|
|
|
|
|
# Define defined gradient length and steps between positions
|
|
|
|
|
stop_length = last - first
|
|
|
|
|
position_steps = [
|
|
|
|
|
positions[i + 1] - positions[i]
|
|
|
|
|
for i in range(len(positions) - 1)]
|
|
|
|
|
|
|
|
|
|
# Create cycles used to add colors
|
|
|
|
|
if spread == 'repeat':
|
|
|
|
|
next_steps = cycle([0] + position_steps)
|
|
|
|
|
next_colors = cycle(colors)
|
|
|
|
|
previous_steps = cycle([0] + position_steps[::-1])
|
|
|
|
|
previous_colors = cycle(colors[::-1])
|
|
|
|
|
else:
|
|
|
|
|
assert spread == 'reflect'
|
|
|
|
|
next_steps = cycle(
|
|
|
|
|
[0] + position_steps[::-1] + [0] + position_steps)
|
|
|
|
|
next_colors = cycle(colors[::-1] + colors)
|
|
|
|
|
previous_steps = cycle(
|
|
|
|
|
[0] + position_steps + [0] + position_steps[::-1])
|
|
|
|
|
previous_colors = cycle(colors + colors[::-1])
|
|
|
|
|
|
2022-12-02 17:31:06 +03:00
|
|
|
|
# Normalize bounding box
|
|
|
|
|
bx1, by1, bw, bh = bounding_box
|
|
|
|
|
bx1, bx2 = (bx1, bx1 + bw) if bw > 0 else (bx1 + bw, bx1)
|
|
|
|
|
by1, by2 = (by1, by1 + bh) if bh > 0 else (by1 + bh, by1)
|
|
|
|
|
|
|
|
|
|
# Transform gradient vector coordinates
|
2022-11-14 11:39:50 +03:00
|
|
|
|
tx1, ty1 = matrix.transform_point(x1, y1)
|
|
|
|
|
tx2, ty2 = matrix.transform_point(x2, y2)
|
2022-12-02 17:31:06 +03:00
|
|
|
|
|
|
|
|
|
# Find the extremities of the repeating vector, by projecting the
|
|
|
|
|
# bounding box corners on the gradient vector
|
|
|
|
|
xb, yb = tx1, ty1
|
|
|
|
|
xv, yv = tx2 - tx1, ty2 - ty1
|
|
|
|
|
xa1, xa2 = (bx1, bx2) if tx1 < tx2 else (bx2, bx1)
|
|
|
|
|
ya1, ya2 = (by1, by2) if ty1 < ty2 else (by2, by1)
|
|
|
|
|
min_vector = ((xa1 - xb) * xv + (ya1 - yb) * yv) / hypot(xv, yv) ** 2
|
|
|
|
|
max_vector = ((xa2 - xb) * xv + (ya2 - yb) * yv) / hypot(xv, yv) ** 2
|
|
|
|
|
|
|
|
|
|
# Add colors after last step
|
|
|
|
|
while last < max_vector:
|
2021-03-20 11:41:37 +03:00
|
|
|
|
step = next(next_steps)
|
|
|
|
|
colors.append(next(next_colors))
|
|
|
|
|
positions.append(positions[-1] + step)
|
|
|
|
|
last += step * stop_length
|
|
|
|
|
|
2022-12-02 17:31:06 +03:00
|
|
|
|
# Add colors before first step
|
|
|
|
|
while first > min_vector:
|
2021-03-20 11:41:37 +03:00
|
|
|
|
step = next(previous_steps)
|
|
|
|
|
colors.insert(0, next(previous_colors))
|
|
|
|
|
positions.insert(0, positions[0] - step)
|
|
|
|
|
first -= step * stop_length
|
|
|
|
|
|
|
|
|
|
x1, x2 = x1 + (x2 - x1) * first, x1 + (x2 - x1) * last
|
|
|
|
|
y1, y2 = y1 + (y2 - y1) * first, y1 + (y2 - y1) * last
|
|
|
|
|
coords = (x1, y1, x2, y2)
|
|
|
|
|
return positions, colors, coords
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def spread_radial_gradient(spread, positions, colors, fx, fy, fr, cx, cy, r,
|
2022-11-14 11:39:50 +03:00
|
|
|
|
width, height, matrix):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Repeat radial gradient."""
|
|
|
|
|
# TODO: merge with RadialGradient._repeat
|
2021-03-20 11:41:37 +03:00
|
|
|
|
from ..images import gradient_average_color, normalize_stop_positions
|
|
|
|
|
|
|
|
|
|
first, last, positions = normalize_stop_positions(positions)
|
|
|
|
|
fr, r = fr + (r - fr) * first, fr + (r - fr) * last
|
|
|
|
|
|
|
|
|
|
if spread in ('repeat', 'reflect'):
|
|
|
|
|
# Keep original lists and values, they’re useful
|
|
|
|
|
original_colors = colors.copy()
|
|
|
|
|
original_positions = positions.copy()
|
|
|
|
|
|
|
|
|
|
# Get the maximum distance between the center and the corners, to find
|
|
|
|
|
# how many times we have to repeat the colors outside
|
2022-11-14 12:35:47 +03:00
|
|
|
|
tw, th = matrix.invert.transform_point(width, height)
|
|
|
|
|
max_distance = hypot(
|
|
|
|
|
max(abs(fx), abs(tw - fx)), max(abs(fy), abs(th - fy)))
|
|
|
|
|
gradient_length = r - fr
|
2021-03-20 11:41:37 +03:00
|
|
|
|
repeat_after = ceil((max_distance - r) / gradient_length)
|
|
|
|
|
if repeat_after > 0:
|
|
|
|
|
# Repeat colors and extrapolate positions
|
|
|
|
|
repeat = 1 + repeat_after
|
|
|
|
|
if spread == 'repeat':
|
|
|
|
|
colors *= repeat
|
|
|
|
|
else:
|
|
|
|
|
assert spread == 'reflect'
|
|
|
|
|
colors = []
|
|
|
|
|
for i in range(repeat):
|
2022-11-14 12:35:47 +03:00
|
|
|
|
colors += original_colors[::-1 if i % 2 else 1]
|
2021-03-20 11:41:37 +03:00
|
|
|
|
positions = [
|
|
|
|
|
i + position for i in range(repeat) for position in positions]
|
|
|
|
|
r += gradient_length * repeat_after
|
|
|
|
|
|
|
|
|
|
if fr == 0:
|
|
|
|
|
# Inner circle has 0 radius, no need to repeat inside, return
|
|
|
|
|
coords = (fx, fy, fr, cx, cy, r)
|
|
|
|
|
return positions, colors, coords
|
|
|
|
|
|
|
|
|
|
# Find how many times we have to repeat the colors inside
|
|
|
|
|
repeat_before = fr / gradient_length
|
|
|
|
|
|
|
|
|
|
# Set the inner circle size to 0
|
|
|
|
|
fr = 0
|
|
|
|
|
|
|
|
|
|
# Find how many times the whole gradient can be repeated
|
|
|
|
|
full_repeat = int(repeat_before)
|
|
|
|
|
if full_repeat:
|
|
|
|
|
# Repeat colors and extrapolate positions
|
|
|
|
|
if spread == 'repeat':
|
|
|
|
|
colors += original_colors * full_repeat
|
|
|
|
|
else:
|
|
|
|
|
assert spread == 'reflect'
|
|
|
|
|
for i in range(full_repeat):
|
2022-11-14 12:35:47 +03:00
|
|
|
|
colors += original_colors[
|
|
|
|
|
::-1 if (i + repeat_after) % 2 else 1]
|
2021-03-20 11:41:37 +03:00
|
|
|
|
positions = [
|
|
|
|
|
i - full_repeat + position for i in range(full_repeat)
|
|
|
|
|
for position in original_positions] + positions
|
|
|
|
|
|
|
|
|
|
# Find the ratio of gradient that must be added to reach the center
|
|
|
|
|
partial_repeat = repeat_before - full_repeat
|
|
|
|
|
if partial_repeat == 0:
|
|
|
|
|
# No partial repeat, return
|
|
|
|
|
coords = (fx, fy, fr, cx, cy, r)
|
|
|
|
|
return positions, colors, coords
|
|
|
|
|
|
|
|
|
|
# Iterate through positions in reverse order, from the outer
|
|
|
|
|
# circle to the original inner circle, to find positions from
|
|
|
|
|
# the inner circle (including full repeats) to the center
|
|
|
|
|
assert (original_positions[0], original_positions[-1]) == (0, 1)
|
|
|
|
|
assert 0 < partial_repeat < 1
|
|
|
|
|
reverse = original_positions[::-1]
|
|
|
|
|
ratio = 1 - partial_repeat
|
2022-11-14 12:35:47 +03:00
|
|
|
|
if spread == 'reflect':
|
|
|
|
|
original_colors = original_colors[::-1]
|
2021-03-20 11:41:37 +03:00
|
|
|
|
for i, position in enumerate(reverse, start=1):
|
|
|
|
|
if position == ratio:
|
|
|
|
|
# The center is a color of the gradient, truncate original
|
|
|
|
|
# colors and positions and prepend them
|
|
|
|
|
colors = original_colors[-i:] + colors
|
|
|
|
|
new_positions = [
|
|
|
|
|
position - full_repeat - 1
|
|
|
|
|
for position in original_positions[-i:]]
|
|
|
|
|
positions = new_positions + positions
|
2022-11-14 12:35:47 +03:00
|
|
|
|
break
|
2021-03-20 11:41:37 +03:00
|
|
|
|
if position < ratio:
|
|
|
|
|
# The center is between two colors of the gradient,
|
|
|
|
|
# define the center color as the average of these two
|
|
|
|
|
# gradient colors
|
|
|
|
|
color = original_colors[-i]
|
|
|
|
|
next_color = original_colors[-(i - 1)]
|
|
|
|
|
next_position = original_positions[-(i - 1)]
|
|
|
|
|
average_colors = [color, color, next_color, next_color]
|
|
|
|
|
average_positions = [position, ratio, ratio, next_position]
|
|
|
|
|
zero_color = gradient_average_color(
|
|
|
|
|
average_colors, average_positions)
|
|
|
|
|
colors = [zero_color] + original_colors[-(i - 1):] + colors
|
|
|
|
|
new_positions = [
|
|
|
|
|
position - 1 - full_repeat for position
|
|
|
|
|
in original_positions[-(i - 1):]]
|
|
|
|
|
positions = (
|
|
|
|
|
[ratio - 1 - full_repeat] + new_positions + positions)
|
2022-11-14 12:35:47 +03:00
|
|
|
|
break
|
2021-03-20 11:41:37 +03:00
|
|
|
|
|
|
|
|
|
coords = (fx, fy, fr, cx, cy, r)
|
|
|
|
|
return positions, colors, coords
|
2021-03-24 16:10:50 +03:00
|
|
|
|
|
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
def draw_pattern(svg, node, pattern, font_size, opacity, stroke):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Draw given gradient node."""
|
2021-03-24 16:10:50 +03:00
|
|
|
|
from . import Pattern
|
|
|
|
|
|
|
|
|
|
pattern._etree_node.tag = 'svg'
|
2021-04-26 18:36:18 +03:00
|
|
|
|
|
2021-06-02 21:35:06 +03:00
|
|
|
|
bounding_box = svg.calculate_bounding_box(node, font_size, stroke)
|
2021-04-26 18:36:18 +03:00
|
|
|
|
if not is_valid_bounding_box(bounding_box):
|
|
|
|
|
return False
|
|
|
|
|
x, y = bounding_box[0], bounding_box[1]
|
2021-07-31 02:06:50 +03:00
|
|
|
|
matrix = Matrix(e=x, f=y)
|
2021-04-26 18:59:20 +03:00
|
|
|
|
if pattern.get('patternUnits') == 'userSpaceOnUse':
|
2021-07-31 09:29:23 +03:00
|
|
|
|
width, height = svg.inner_width, svg.inner_height
|
2021-04-26 18:59:20 +03:00
|
|
|
|
else:
|
|
|
|
|
width, height = bounding_box[2], bounding_box[3]
|
2021-04-26 18:36:18 +03:00
|
|
|
|
|
2021-03-24 16:10:50 +03:00
|
|
|
|
if pattern.get('patternUnits') == 'userSpaceOnUse':
|
|
|
|
|
x = size(pattern.get('x'), font_size, 1)
|
|
|
|
|
y = size(pattern.get('y'), font_size, 1)
|
|
|
|
|
pattern_width = size(pattern.get('width', 0), font_size, 1)
|
|
|
|
|
pattern_height = size(pattern.get('height', 0), font_size, 1)
|
|
|
|
|
else:
|
|
|
|
|
x = size(pattern.get('x'), font_size, 1) * width
|
|
|
|
|
y = size(pattern.get('y'), font_size, 1) * height
|
|
|
|
|
pattern_width = (
|
2021-04-05 13:11:36 +03:00
|
|
|
|
size(pattern.attrib.pop('width', '1'), font_size, 1) * width)
|
2021-03-24 16:10:50 +03:00
|
|
|
|
pattern_height = (
|
2021-04-05 13:11:36 +03:00
|
|
|
|
size(pattern.attrib.pop('height', '1'), font_size, 1) * height)
|
2021-03-24 16:10:50 +03:00
|
|
|
|
if 'viewBox' not in pattern:
|
|
|
|
|
pattern.attrib['width'] = pattern_width
|
|
|
|
|
pattern.attrib['height'] = pattern_height
|
|
|
|
|
if pattern.get('patternContentUnits') == 'objectBoundingBox':
|
|
|
|
|
pattern.attrib['transform'] = f'scale({width}, {height})'
|
|
|
|
|
|
|
|
|
|
# Fail if pattern has an invalid size
|
|
|
|
|
if pattern_width == 0 or pattern_height == 0:
|
|
|
|
|
return False
|
|
|
|
|
|
2021-04-26 18:59:20 +03:00
|
|
|
|
if 'patternTransform' in pattern.attrib:
|
|
|
|
|
transform_matrix = transform(
|
2021-07-31 09:29:23 +03:00
|
|
|
|
pattern.get('patternTransform'), font_size, svg.inner_diagonal)
|
2021-04-26 18:59:20 +03:00
|
|
|
|
matrix = transform_matrix @ matrix
|
|
|
|
|
|
2021-07-25 19:30:32 +03:00
|
|
|
|
matrix = matrix @ svg.stream.ctm
|
2021-03-24 16:10:50 +03:00
|
|
|
|
stream_pattern = svg.stream.add_pattern(
|
2022-11-14 11:39:50 +03:00
|
|
|
|
0, 0, pattern_width, pattern_height, pattern_width, pattern_height,
|
|
|
|
|
matrix)
|
2021-06-02 21:35:06 +03:00
|
|
|
|
stream_pattern.set_alpha(opacity)
|
2021-04-26 18:36:18 +03:00
|
|
|
|
|
2022-11-19 00:59:56 +03:00
|
|
|
|
group = stream_pattern.add_group(0, 0, pattern_width, pattern_height)
|
2021-07-12 12:39:52 +03:00
|
|
|
|
Pattern(pattern, svg).draw(
|
2021-04-18 19:50:11 +03:00
|
|
|
|
group, pattern_width, pattern_height, svg.base_url,
|
2021-04-14 22:31:08 +03:00
|
|
|
|
svg.url_fetcher, svg.context)
|
2021-04-18 19:50:11 +03:00
|
|
|
|
stream_pattern.draw_x_object(group.id)
|
2021-03-24 16:10:50 +03:00
|
|
|
|
svg.stream.color_space('Pattern', stroke=stroke)
|
|
|
|
|
svg.stream.set_color_special(stream_pattern.id, stroke=stroke)
|
|
|
|
|
return True
|
2021-04-04 13:40:04 +03:00
|
|
|
|
|
|
|
|
|
|
2021-04-12 17:27:55 +03:00
|
|
|
|
def apply_filters(svg, node, filter_node, font_size):
|
|
|
|
|
"""Apply filters defined in given filter node."""
|
|
|
|
|
for child in filter_node:
|
2021-04-04 13:40:04 +03:00
|
|
|
|
if child.tag == 'feOffset':
|
2021-04-12 17:27:55 +03:00
|
|
|
|
if filter_node.get('primitiveUnits') == 'objectBoundingBox':
|
2021-04-12 16:11:13 +03:00
|
|
|
|
bounding_box = svg.calculate_bounding_box(node, font_size)
|
|
|
|
|
if is_valid_bounding_box(bounding_box):
|
|
|
|
|
_, _, width, height = bounding_box
|
|
|
|
|
dx = size(child.get('dx', 0), font_size, 1) * width
|
|
|
|
|
dy = size(child.get('dy', 0), font_size, 1) * height
|
|
|
|
|
else:
|
|
|
|
|
dx = dy = 0
|
2021-04-04 13:40:04 +03:00
|
|
|
|
else:
|
|
|
|
|
dx, dy = svg.point(
|
|
|
|
|
child.get('dx', 0), child.get('dy', 0), font_size)
|
2021-07-17 15:25:58 +03:00
|
|
|
|
svg.stream.transform(e=dx, f=dy)
|
2021-04-04 13:40:04 +03:00
|
|
|
|
elif child.tag == 'feBlend':
|
|
|
|
|
mode = child.get('mode', 'normal')
|
|
|
|
|
mode = mode.replace('-', ' ').title().replace(' ', '')
|
2022-05-27 12:26:15 +03:00
|
|
|
|
svg.stream.set_blend_mode(mode)
|
2021-04-05 13:11:36 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paint_mask(svg, node, mask, font_size):
|
2021-04-12 17:27:55 +03:00
|
|
|
|
"""Apply given mask node."""
|
2021-04-05 13:11:36 +03:00
|
|
|
|
mask._etree_node.tag = 'g'
|
|
|
|
|
|
|
|
|
|
if mask.get('maskUnits') == 'userSpaceOnUse':
|
2021-07-31 09:29:23 +03:00
|
|
|
|
width_ref, height_ref = svg.inner_width, svg.inner_height
|
2021-04-05 13:11:36 +03:00
|
|
|
|
else:
|
2021-07-25 19:30:32 +03:00
|
|
|
|
width_ref, height_ref = svg.point(
|
2021-04-11 17:13:59 +03:00
|
|
|
|
node.get('width'), node.get('height'), font_size)
|
2021-04-05 13:11:36 +03:00
|
|
|
|
|
|
|
|
|
mask.attrib['x'] = size(mask.get('x', '-10%'), font_size, width_ref)
|
|
|
|
|
mask.attrib['y'] = size(mask.get('y', '-10%'), font_size, height_ref)
|
|
|
|
|
mask.attrib['height'] = size(
|
|
|
|
|
mask.get('height', '120%'), font_size, height_ref)
|
|
|
|
|
mask.attrib['width'] = size(
|
|
|
|
|
mask.get('width', '120%'), font_size, width_ref)
|
|
|
|
|
|
|
|
|
|
if mask.get('maskUnits') == 'userSpaceOnUse':
|
|
|
|
|
x, y = mask.get('x'), mask.get('y')
|
|
|
|
|
width, height = mask.get('width'), mask.get('height')
|
|
|
|
|
mask.attrib['viewBox'] = f'{x} {y} {width} {height}'
|
2021-07-25 19:30:32 +03:00
|
|
|
|
else:
|
|
|
|
|
x, y = 0, 0
|
|
|
|
|
width, height = width_ref, height_ref
|
2021-04-05 13:11:36 +03:00
|
|
|
|
|
|
|
|
|
svg_stream = svg.stream
|
2022-05-27 12:26:15 +03:00
|
|
|
|
svg.stream = svg.stream.set_alpha_state(x, y, width, height)
|
2021-04-05 13:11:36 +03:00
|
|
|
|
svg.draw_node(mask, font_size)
|
|
|
|
|
svg.stream = svg_stream
|
2021-07-18 10:36:14 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clip_path(svg, node, font_size):
|
|
|
|
|
"""Store a clip path definition."""
|
|
|
|
|
if 'id' in node.attrib:
|
|
|
|
|
svg.paths[node.attrib['id']] = node
|