1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-08-17 16:40:45 +03:00
WeasyPrint/weasyprint/svg/bounding_box.py
Guillaume Ayoub c9c9b3da9d Minor changes
Improvements provided by refurb.
2022-10-05 18:22:35 +02:00

357 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Calculate bounding boxes of SVG tags."""
from math import atan, atan2, cos, inf, isinf, pi, radians, sin, sqrt, tan
from .path import PATH_LETTERS
from .utils import normalize, point
EMPTY_BOUNDING_BOX = inf, inf, 0, 0
def bounding_box(svg, node, font_size, stroke):
"""Bounding box for any node."""
if node.tag not in BOUNDING_BOX_METHODS:
return EMPTY_BOUNDING_BOX
box = BOUNDING_BOX_METHODS[node.tag](svg, node, font_size)
if not is_valid_bounding_box(box):
return EMPTY_BOUNDING_BOX
if stroke and node.tag != 'g' and any(svg.get_paint(node.get('stroke'))):
stroke_width = svg.length(node.get('stroke-width', '1px'), font_size)
box = (
box[0] - stroke_width / 2, box[1] - stroke_width / 2,
box[2] + stroke_width, box[3] + stroke_width)
return box
def bounding_box_rect(svg, node, font_size):
"""Bounding box for rect node."""
x, y = svg.point(node.get('x'), node.get('y'), font_size)
width, height = svg.point(
node.get('width'), node.get('height'), font_size)
return x, y, width, height
def bounding_box_circle(svg, node, font_size):
"""Bounding box for circle node."""
cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)
r = svg.length(node.get('r'), font_size)
return cx - r, cy - r, 2 * r, 2 * r
def bounding_box_ellipse(svg, node, font_size):
"""Bounding box for ellipse node."""
rx, ry = svg.point(node.get('rx'), node.get('ry'), font_size)
cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)
return cx - rx, cy - ry, 2 * rx, 2 * ry
def bounding_box_line(svg, node, font_size):
"""Bounding box for line node."""
x1, y1 = svg.point(node.get('x1'), node.get('y1'), font_size)
x2, y2 = svg.point(node.get('x2'), node.get('y2'), font_size)
x, y = min(x1, x2), min(y1, y2)
width, height = max(x1, x2) - x, max(y1, y2) - y
return x, y, width, height
def bounding_box_polyline(svg, node, font_size):
"""Bounding box for polyline node."""
bounding_box = EMPTY_BOUNDING_BOX
points = []
normalized_points = normalize(node.get('points', ''))
while normalized_points:
x, y, normalized_points = point(svg, normalized_points, font_size)
points.append((x, y))
return extend_bounding_box(bounding_box, points)
def bounding_box_path(svg, node, font_size):
"""Bounding box for path node."""
path_data = node.get('d', '')
# Normalize path data for correct parsing
for letter in PATH_LETTERS:
path_data = path_data.replace(letter, f' {letter} ')
path_data = normalize(path_data)
bounding_box = EMPTY_BOUNDING_BOX
previous_x = 0
previous_y = 0
letter = 'M' # Move as default
while path_data:
path_data = path_data.strip()
if path_data.split(' ', 1)[0] in PATH_LETTERS:
letter, path_data = (f'{path_data} ').split(' ', 1)
if letter in 'aA':
# Elliptical arc curve
rx, ry, path_data = point(svg, path_data, font_size)
rotation, path_data = path_data.split(' ', 1)
rotation = radians(float(rotation))
# The large and sweep values are not always separated from the
# following values, here is the crazy parser
large, path_data = path_data[0], path_data[1:].strip()
while not large[-1].isdigit():
large, path_data = large + path_data[0], path_data[1:].strip()
sweep, path_data = path_data[0], path_data[1:].strip()
while not sweep[-1].isdigit():
sweep, path_data = sweep + path_data[0], path_data[1:].strip()
large, sweep = bool(int(large)), bool(int(sweep))
x, y, path_data = point(svg, path_data, font_size)
# Relative coordinate, convert to absolute
if letter == 'a':
x += previous_x
y += previous_y
# Extend bounding box with start and end coordinates
arc_bounding_box = _bounding_box_elliptical_arc(
previous_x, previous_y, rx, ry, rotation, large, sweep, x, y)
x1, y1, width, height = arc_bounding_box
x2 = x1 + width
y2 = y1 + height
points = (x1, y1), (x2, y2)
bounding_box = extend_bounding_box(bounding_box, points)
previous_x = x
previous_y = y
elif letter in 'cC':
# Curve
x1, y1, path_data = point(svg, path_data, font_size)
x2, y2, path_data = point(svg, path_data, font_size)
x, y, path_data = point(svg, path_data, font_size)
# Relative coordinates, convert to absolute
if letter == 'c':
x1 += previous_x
y1 += previous_y
x2 += previous_x
y2 += previous_y
x += previous_x
y += previous_y
# Extend bounding box with all coordinates
bounding_box = extend_bounding_box(
bounding_box, ((x1, y1), (x2, y2), (x, y)))
previous_x = x
previous_y = y
elif letter in 'hH':
# Horizontal line
x, path_data = (f'{path_data} ').split(' ', 1)
x, _ = svg.point(x, 0, font_size)
# Relative coordinate, convert to absolute
if letter == 'h':
x += previous_x
# Extend bounding box with coordinate
bounding_box = extend_bounding_box(
bounding_box, ((x, previous_y),))
previous_x = x
elif letter in 'lLmMtT':
# Line/Move/Smooth quadratic curve
x, y, path_data = point(svg, path_data, font_size)
# Relative coordinate, convert to absolute
if letter in 'lmt':
x += previous_x
y += previous_y
# Extend bounding box with coordinate
bounding_box = extend_bounding_box(bounding_box, ((x, y),))
previous_x = x
previous_y = y
elif letter in 'qQsS':
# Quadratic curve/Smooth curve
x1, y1, path_data = point(svg, path_data, font_size)
x, y, path_data = point(svg, path_data, font_size)
# Relative coordinates, convert to absolute
if letter in 'qs':
x1 += previous_x
y1 += previous_y
x += previous_x
y += previous_y
# Extend bounding box with coordinates
bounding_box = extend_bounding_box(
bounding_box, ((x1, y1), (x, y)))
previous_x = x
previous_y = y
elif letter in 'vV':
# Vertical line
y, path_data = (f'{path_data} ').split(' ', 1)
_, y = svg.point(0, y, font_size)
# Relative coordinate, convert to absolute
if letter == 'v':
y += previous_y
# Extend bounding box with coordinate
bounding_box = extend_bounding_box(
bounding_box, ((previous_x, y),))
previous_y = y
path_data = path_data.strip()
return bounding_box
def bounding_box_text(svg, node, font_size):
"""Bounding box for text node."""
return node.get('text_bounding_box')
def bounding_box_g(svg, node, font_size):
"""Bounding box for g node."""
bounding_box = EMPTY_BOUNDING_BOX
for child in node:
child_bounding_box = svg.calculate_bounding_box(child, font_size)
if is_valid_bounding_box(child_bounding_box):
minx, miny, width, height = child_bounding_box
maxx, maxy = minx + width, miny + height
bounding_box = extend_bounding_box(
bounding_box, ((minx, miny), (maxx, maxy)))
return bounding_box
def _bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
"""Bounding box of an elliptical arc in path node."""
rx, ry = abs(rx), abs(ry)
if 0 in (rx, ry):
return min(x, x1), min(y, y1), abs(x - x1), abs(y - y1)
x1prime = cos(phi) * (x1 - x) / 2 + sin(phi) * (y1 - y) / 2
y1prime = -sin(phi) * (x1 - x) / 2 + cos(phi) * (y1 - y) / 2
radicant = (
rx ** 2 * ry ** 2 - rx ** 2 * y1prime ** 2 - ry ** 2 * x1prime ** 2)
radicant /= rx ** 2 * y1prime ** 2 + ry ** 2 * x1prime ** 2
cxprime = cyprime = 0
if radicant < 0:
ratio = rx / ry
radicant = y1prime ** 2 + x1prime ** 2 / ratio ** 2
if radicant < 0:
return min(x, x1), min(y, y1), abs(x - x1), abs(y - y1)
ry = sqrt(radicant)
rx = ratio * ry
else:
factor = (-1 if large == sweep else 1) * sqrt(radicant)
cxprime = factor * rx * y1prime / ry
cyprime = -factor * ry * x1prime / rx
cx = cxprime * cos(phi) - cyprime * sin(phi) + (x1 + x) / 2
cy = cxprime * sin(phi) + cyprime * cos(phi) + (y1 + y) / 2
if phi in (0, pi):
minx = cx - rx
tminx = atan2(0, -rx)
maxx = cx + rx
tmaxx = atan2(0, rx)
miny = cy - ry
tminy = atan2(-ry, 0)
maxy = cy + ry
tmaxy = atan2(ry, 0)
elif phi in (pi / 2, 3 * pi / 2):
minx = cx - ry
tminx = atan2(0, -ry)
maxx = cx + ry
tmaxx = atan2(0, ry)
miny = cy - rx
tminy = atan2(-rx, 0)
maxy = cy + rx
tmaxy = atan2(rx, 0)
else:
tminx = -atan(ry * tan(phi) / rx)
tmaxx = pi - atan(ry * tan(phi) / rx)
minx = cx + rx * cos(tminx) * cos(phi) - ry * sin(tminx) * sin(phi)
maxx = cx + rx * cos(tmaxx) * cos(phi) - ry * sin(tmaxx) * sin(phi)
if minx > maxx:
minx, maxx = maxx, minx
tminx, tmaxx = tmaxx, tminx
tmp_y = cy + rx * cos(tminx) * sin(phi) + ry * sin(tminx) * cos(phi)
tminx = atan2(minx - cx, tmp_y - cy)
tmp_y = cy + rx * cos(tmaxx) * sin(phi) + ry * sin(tmaxx) * cos(phi)
tmaxx = atan2(maxx - cx, tmp_y - cy)
tminy = atan(ry / (tan(phi) * rx))
tmaxy = atan(ry / (tan(phi) * rx)) + pi
miny = cy + rx * cos(tminy) * sin(phi) + ry * sin(tminy) * cos(phi)
maxy = cy + rx * cos(tmaxy) * sin(phi) + ry * sin(tmaxy) * cos(phi)
if miny > maxy:
miny, maxy = maxy, miny
tminy, tmaxy = tmaxy, tminy
tmp_x = cx + rx * cos(tminy) * cos(phi) - ry * sin(tminy) * sin(phi)
tminy = atan2(tmp_x - cx, miny - cy)
tmp_x = cx + rx * cos(tmaxy) * cos(phi) - ry * sin(tmaxy) * sin(phi)
tmaxy = atan2(maxy - cy, tmp_x - cx)
angle1 = atan2(y1 - cy, x1 - cx)
angle2 = atan2(y - cy, x - cx)
if not sweep:
angle1, angle2 = angle2, angle1
other_arc = False
if angle1 > angle2:
angle1, angle2 = angle2, angle1
other_arc = True
if ((not other_arc and (angle1 > tminx or angle2 < tminx)) or
(other_arc and not (angle1 > tminx or angle2 < tminx))):
minx = min(x, x1)
if ((not other_arc and (angle1 > tmaxx or angle2 < tmaxx)) or
(other_arc and not (angle1 > tmaxx or angle2 < tmaxx))):
maxx = max(x, x1)
if ((not other_arc and (angle1 > tminy or angle2 < tminy)) or
(other_arc and not (angle1 > tminy or angle2 < tminy))):
miny = min(y, y1)
if ((not other_arc and (angle1 > tmaxy or angle2 < tmaxy)) or
(other_arc and not (angle1 > tmaxy or angle2 < tmaxy))):
maxy = max(y, y1)
return minx, miny, maxx - minx, maxy - miny
def extend_bounding_box(bounding_box, points):
"""Extend a bounding box to include given points."""
minx, miny, width, height = bounding_box
maxx, maxy = (
-inf if isinf(minx) else minx + width,
-inf if isinf(miny) else miny + height)
x_list, y_list = zip(*points)
minx, miny, maxx, maxy = (
min(minx, *x_list), min(miny, *y_list),
max(maxx, *x_list), max(maxy, *y_list))
return minx, miny, maxx - minx, maxy - miny
def is_valid_bounding_box(bounding_box):
"""Check that a bounding box doesnt have infinite boundaries."""
return bounding_box and not isinf(bounding_box[0] + bounding_box[1])
BOUNDING_BOX_METHODS = {
'rect': bounding_box_rect,
'circle': bounding_box_circle,
'ellipse': bounding_box_ellipse,
'line': bounding_box_line,
'polyline': bounding_box_polyline,
'polygon': bounding_box_polyline,
'path': bounding_box_path,
'g': bounding_box_g,
'marker': bounding_box_g,
'text': bounding_box_text,
'tspan': bounding_box_text,
'textPath': bounding_box_text,
}