1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-09-11 20:47:56 +03:00
WeasyPrint/weasyprint/svg/bounding_box.py

357 lines
12 KiB
Python
Raw Permalink Normal View History

2022-02-14 09:11:30 +03:00
"""Calculate bounding boxes of SVG tags."""
2021-04-12 16:11:13 +03:00
2022-01-24 13:34:10 +03:00
from math import atan, atan2, cos, inf, isinf, pi, radians, sin, sqrt, tan
2021-03-14 11:29:33 +03:00
from .path import PATH_LETTERS
from .utils import normalize, point
2021-03-14 11:29:33 +03:00
2022-01-24 13:34:10 +03:00
EMPTY_BOUNDING_BOX = inf, inf, 0, 0
2021-03-14 11:29:33 +03:00
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
2021-03-14 11:29:33 +03:00
def bounding_box_rect(svg, node, font_size):
2021-04-12 16:11:13 +03:00
"""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)
2021-03-14 11:29:33 +03:00
return x, y, width, height
def bounding_box_circle(svg, node, font_size):
2021-04-12 16:11:13 +03:00
"""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)
2021-03-14 11:29:33 +03:00
return cx - r, cy - r, 2 * r, 2 * r
def bounding_box_ellipse(svg, node, font_size):
2021-04-12 16:11:13 +03:00
"""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)
2021-03-14 11:29:33 +03:00
return cx - rx, cy - ry, 2 * rx, 2 * ry
def bounding_box_line(svg, node, font_size):
2021-04-12 16:11:13 +03:00
"""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)
2021-03-14 11:29:33 +03:00
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):
2021-04-12 16:11:13 +03:00
"""Bounding box for polyline node."""
2021-03-14 11:29:33 +03:00
bounding_box = EMPTY_BOUNDING_BOX
points = []
normalized_points = normalize(node.get('points', ''))
while normalized_points:
2021-04-18 18:19:52 +03:00
x, y, normalized_points = point(svg, normalized_points, font_size)
2021-03-14 11:29:33 +03:00
points.append((x, y))
return extend_bounding_box(bounding_box, points)
def bounding_box_path(svg, node, font_size):
2021-04-12 16:11:13 +03:00
"""Bounding box for path node."""
2021-03-14 11:29:33 +03:00
path_data = node.get('d', '')
# Normalize path data for correct parsing
for letter in PATH_LETTERS:
2021-04-11 17:13:59 +03:00
path_data = path_data.replace(letter, f' {letter} ')
2021-03-14 11:29:33 +03:00
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:
2022-05-17 17:40:30 +03:00
letter, path_data = (f'{path_data} ').split(' ', 1)
2021-03-14 11:29:33 +03:00
if letter in 'aA':
# Elliptical arc curve
2021-04-18 18:19:52 +03:00
rx, ry, path_data = point(svg, path_data, font_size)
2021-03-14 11:29:33 +03:00
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))
2021-04-18 18:19:52 +03:00
x, y, path_data = point(svg, path_data, font_size)
2021-03-14 11:29:33 +03:00
# Relative coordinate, convert to absolute
if letter == 'a':
x += previous_x
y += previous_y
# Extend bounding box with start and end coordinates
2021-04-12 16:11:13 +03:00
arc_bounding_box = _bounding_box_elliptical_arc(
2021-03-14 11:29:33 +03:00
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
2021-04-18 18:19:52 +03:00
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)
2021-03-14 11:29:33 +03:00
# 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
2022-05-17 17:40:30 +03:00
x, path_data = (f'{path_data} ').split(' ', 1)
x, _ = svg.point(x, 0, font_size)
2021-03-14 11:29:33 +03:00
# 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
2022-05-17 17:40:30 +03:00
y, path_data = (f'{path_data} ').split(' ', 1)
_, y = svg.point(0, y, font_size)
2021-03-14 11:29:33 +03:00
# 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
2021-04-12 14:32:40 +03:00
def bounding_box_text(svg, node, font_size):
2021-04-12 16:11:13 +03:00
"""Bounding box for text node."""
return node.get('text_bounding_box')
2021-04-12 14:32:40 +03:00
2021-04-12 16:11:13 +03:00
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
2021-03-14 11:29:33 +03:00
2021-04-12 16:11:13 +03:00
def _bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
"""Bounding box of an elliptical arc in path node."""
2021-03-14 11:29:33 +03:00
rx, ry = abs(rx), abs(ry)
2021-04-12 16:11:13 +03:00
if 0 in (rx, ry):
2021-03-14 11:29:33 +03:00
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):
2021-03-14 11:29:33 +03:00
minx = cx - rx
2021-04-12 16:11:13 +03:00
tminx = atan2(0, -rx)
2021-03-14 11:29:33 +03:00
maxx = cx + rx
2021-04-12 16:11:13 +03:00
tmaxx = atan2(0, rx)
2021-03-14 11:29:33 +03:00
miny = cy - ry
2021-04-12 16:11:13 +03:00
tminy = atan2(-ry, 0)
2021-03-14 11:29:33 +03:00
maxy = cy + ry
2021-04-12 16:11:13 +03:00
tmaxy = atan2(ry, 0)
elif phi in (pi / 2, 3 * pi / 2):
2021-03-14 11:29:33 +03:00
minx = cx - ry
2021-04-12 16:11:13 +03:00
tminx = atan2(0, -ry)
2021-03-14 11:29:33 +03:00
maxx = cx + ry
2021-04-12 16:11:13 +03:00
tmaxx = atan2(0, ry)
2021-03-14 11:29:33 +03:00
miny = cy - rx
2021-04-12 16:11:13 +03:00
tminy = atan2(-rx, 0)
2021-03-14 11:29:33 +03:00
maxy = cy + rx
2021-04-12 16:11:13 +03:00
tmaxy = atan2(rx, 0)
2021-03-14 11:29:33 +03:00
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)
2021-04-12 16:11:13 +03:00
tminx = atan2(minx - cx, tmp_y - cy)
2021-03-14 11:29:33 +03:00
tmp_y = cy + rx * cos(tmaxx) * sin(phi) + ry * sin(tmaxx) * cos(phi)
2021-04-12 16:11:13 +03:00
tmaxx = atan2(maxx - cx, tmp_y - cy)
2021-03-14 11:29:33 +03:00
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)
2021-04-12 16:11:13 +03:00
tminy = atan2(tmp_x - cx, miny - cy)
2021-03-14 11:29:33 +03:00
tmp_x = cx + rx * cos(tmaxy) * cos(phi) - ry * sin(tmaxy) * sin(phi)
2021-04-12 16:11:13 +03:00
tmaxy = atan2(maxy - cy, tmp_x - cx)
2021-03-14 11:29:33 +03:00
2021-04-12 16:11:13 +03:00
angle1 = atan2(y1 - cy, x1 - cx)
angle2 = atan2(y - cy, x - cx)
2021-03-14 11:29:33 +03:00
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):
2021-04-12 16:11:13 +03:00
"""Extend a bounding box to include given points."""
2021-03-14 11:29:33 +03:00
minx, miny, width, height = bounding_box
maxx, maxy = (
2022-01-24 13:34:10 +03:00
-inf if isinf(minx) else minx + width,
-inf if isinf(miny) else miny + height)
2021-03-14 11:29:33 +03:00
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):
2021-04-12 16:11:13 +03:00
"""Check that a bounding box doesnt have infinite boundaries."""
2021-03-14 11:29:33 +03:00
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,
2021-04-12 16:11:13 +03:00
'g': bounding_box_g,
'marker': bounding_box_g,
2021-04-12 14:32:40 +03:00
'text': bounding_box_text,
'tspan': bounding_box_text,
'textPath': bounding_box_text,
2021-03-14 11:29:33 +03:00
}