feat: add comprehensive GitHub workflow and development tools
This commit is contained in:
356
app/.venv/Lib/site-packages/weasyprint/svg/bounding_box.py
Normal file
356
app/.venv/Lib/site-packages/weasyprint/svg/bounding_box.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""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 doesn’t 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,
|
||||
}
|
||||
Reference in New Issue
Block a user