tractatus/pptx-env/lib/python3.12/site-packages/weasyprint/draw/border.py
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

725 lines
30 KiB
Python

"""Draw borders."""
from math import ceil, cos, floor, pi, sin, sqrt, tan
from ..formatting_structure import boxes
from ..layout import replaced
from ..layout.percent import percentage
from ..matrix import Matrix
from .color import get_color, styled_color
SIDES = ('top', 'right', 'bottom', 'left')
def set_mask_border(stream, box):
"""Set ``box`` mask border as alpha state on ``stream``."""
if box.style['mask_border_source'][0] == 'none' or box.mask_border_image is None:
return
x, y, w, h, tl, tr, br, bl = box.rounded_border_box()
matrix = Matrix(e=x, f=y)
matrix @= stream.ctm
mask_stream = stream.set_alpha_state(x, y, w, h, box.style['mask_border_mode'])
draw_border_image(
box, mask_stream, box.mask_border_image, box.style['mask_border_slice'],
box.style['mask_border_repeat'], box.style['mask_border_outset'],
box.style['mask_border_width'])
def draw_column_rules(stream, box):
"""Draw the column rules to a ``pdf.stream.Stream``."""
border_widths = (0, 0, 0, box.style['column_rule_width'])
skip_next = True
for child in box.children:
if child.style['column_span'] == 'all':
skip_next = True
continue
elif skip_next:
skip_next = False
continue
with stream.stacked():
rule_width = box.style['column_rule_width']
rule_style = box.style['column_rule_style']
if box.style['column_gap'] == 'normal':
gap = box.style['font_size'] # normal equals 1em
else:
gap = percentage(box.style['column_gap'], box.width)
position_x = (
child.position_x - (box.style['column_rule_width'] + gap) / 2)
border_box = position_x, child.position_y, rule_width, child.height
clip_border_segment(
stream, rule_style, rule_width, 'left', border_box, border_widths)
color = styled_color(
rule_style, get_color(box.style, 'column_rule_color'), 'left')
draw_rect_border(stream, border_box, border_widths, rule_style, color)
def draw_border(stream, box):
"""Draw the box borders and column rules to a ``pdf.stream.Stream``."""
# The box is hidden, easy.
if box.style['visibility'] != 'visible':
return
# Draw column rules.
columns = (
isinstance(box, boxes.BlockContainerBox) and (
box.style['column_width'] != 'auto' or
box.style['column_count'] != 'auto'))
if columns and box.style['column_rule_width']:
with stream.artifact():
draw_column_rules(stream, box)
# If there's a border image, that takes precedence.
if box.style['border_image_source'][0] != 'none' and box.border_image is not None:
with stream.artifact():
draw_border_image(
box, stream, box.border_image, box.style['border_image_slice'],
box.style['border_image_repeat'], box.style['border_image_outset'],
box.style['border_image_width'])
return
widths = [getattr(box, f'border_{side}_width') for side in SIDES]
if set(widths) == {0}:
# No border, return early.
return
colors = [get_color(box.style, f'border_{side}_color') for side in SIDES]
styles = [
colors[i].alpha and box.style[f'border_{side}_style']
for (i, side) in enumerate(SIDES)]
simple_style = set(styles) in ({'solid'}, {'double'}) # one style, simple lines
single_color = len(set(colors)) == 1 # one color
four_sides = 0 not in widths # no 0-width border, to avoid PDF artifacts
if simple_style and single_color and four_sides:
# Simple case, we only draw rounded rectangles.
with stream.artifact():
draw_rounded_border(stream, box, styles[0], colors[0])
return
# We're not smart enough to find a good way to draw the borders, we must
# draw them side by side. Order is not specified, but this one seems to be
# close to what other browsers do.
values = tuple(zip(SIDES, widths, colors, styles))
for index in (2, 3, 1, 0):
side, width, color, style = values[index]
if width == 0 or not color:
continue
with stream.artifact(), stream.stacked():
clip_border_segment(
stream, style, width, side, box.rounded_border_box()[:4],
widths, box.rounded_border_box()[4:])
draw_rounded_border(stream, box, style, styled_color(style, color, side))
def draw_border_image(box, stream, image, border_slice, border_repeat, border_outset,
border_width):
"""Draw ``image`` as a border image for ``box`` on ``stream`` as specified."""
# Shared by border-image-* and mask-border-*.
width, height, ratio = image.get_intrinsic_size(
box.style['image_resolution'], box.style['font_size'])
intrinsic_width, intrinsic_height = replaced.default_image_sizing(
width, height, ratio, specified_width=None, specified_height=None,
default_width=box.border_width(), default_height=box.border_height())
image_slice = border_slice[:4]
should_fill = border_slice[4]
def compute_slice_dimension(dimension, intrinsic):
if isinstance(dimension, (int, float)):
return min(dimension, intrinsic)
else:
assert dimension.unit == '%'
return min(100, dimension.value) / 100 * intrinsic
slice_top = compute_slice_dimension(image_slice[0], intrinsic_height)
slice_right = compute_slice_dimension(image_slice[1], intrinsic_width)
slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height)
slice_left = compute_slice_dimension(image_slice[3], intrinsic_width)
repeat_x, repeat_y = border_repeat
x, y, w, h, tl, tr, br, bl = box.rounded_border_box()
px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box()
border_left = px - x
border_top = py - y
border_right = w - pw - border_left
border_bottom = h - ph - border_top
def compute_outset_dimension(dimension, from_border):
if dimension.unit is None:
return dimension.value * from_border
else:
assert dimension.unit == 'px'
return dimension.value
outset_top = compute_outset_dimension(border_outset[0], border_top)
outset_right = compute_outset_dimension(border_outset[1], border_right)
outset_bottom = compute_outset_dimension(border_outset[2], border_bottom)
outset_left = compute_outset_dimension(border_outset[3], border_left)
x -= outset_left
y -= outset_top
w += outset_left + outset_right
h += outset_top + outset_bottom
def compute_width_adjustment(dimension, original, intrinsic,
area_dimension):
if dimension == 'auto':
return intrinsic
elif isinstance(dimension, (int, float)):
return dimension * original
elif dimension.unit == '%':
return dimension.value / 100 * area_dimension
else:
assert dimension.unit == 'px'
return dimension.value
# We make adjustments to the border_* variables after handling outsets
# because numerical outsets are relative to border-width, not
# border-image-width. Also, the border image area that is used
# for percentage-based border-image-width values includes any expanded
# area due to border-image-outset.
border_top = compute_width_adjustment(
border_width[0], border_top, slice_top, h)
border_right = compute_width_adjustment(
border_width[1], border_right, slice_right, w)
border_bottom = compute_width_adjustment(
border_width[2], border_bottom, slice_bottom, h)
border_left = compute_width_adjustment(
border_width[3], border_left, slice_left, w)
def draw_border_image_region(x, y, width, height, slice_x, slice_y, slice_width,
slice_height, repeat_x='stretch', repeat_y='stretch',
scale_x=None, scale_y=None):
if 0 in (intrinsic_width, width, slice_width):
scale_x = 0
else:
extra_dx = 0
if not scale_x:
scale_x = (height / slice_height) if height and slice_height else 1
if repeat_x == 'repeat':
n_repeats_x = ceil(width / slice_width / scale_x)
elif repeat_x == 'space':
n_repeats_x = floor(width / slice_width / scale_x)
# Space is before the first repeat and after the last,
# so there's one more space than repeat.
extra_dx = (
(width / scale_x - n_repeats_x * slice_width) / (n_repeats_x + 1))
elif repeat_x == 'round':
n_repeats_x = max(1, round(width / slice_width / scale_x))
scale_x = width / (n_repeats_x * slice_width)
else:
n_repeats_x = 1
scale_x = width / slice_width
if 0 in (intrinsic_height, height, slice_height):
scale_y = 0
else:
extra_dy = 0
if not scale_y:
scale_y = (width / slice_width) if width and slice_width else 1
if repeat_y == 'repeat':
n_repeats_y = ceil(height / slice_height / scale_y)
elif repeat_y == 'space':
n_repeats_y = floor(height / slice_height / scale_y)
# Space is before the first repeat and after the last,
# so there's one more space than repeat.
extra_dy = (
(height / scale_y - n_repeats_y * slice_height) / (n_repeats_y + 1))
elif repeat_y == 'round':
n_repeats_y = max(1, round(height / slice_height / scale_y))
scale_y = height / (n_repeats_y * slice_height)
else:
n_repeats_y = 1
scale_y = height / slice_height
if 0 in (scale_x, scale_y):
return scale_x, scale_y
rendered_width = intrinsic_width * scale_x
rendered_height = intrinsic_height * scale_y
offset_x = rendered_width * slice_x / intrinsic_width
offset_y = rendered_height * slice_y / intrinsic_height
with stream.stacked():
stream.rectangle(x, y, width, height)
stream.clip()
stream.end()
stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy)
stream.transform(a=scale_x, d=scale_y)
for i in range(n_repeats_x):
for j in range(n_repeats_y):
with stream.stacked():
translate_x = i * (slice_width + extra_dx)
translate_y = j * (slice_height + extra_dy)
stream.transform(e=translate_x, f=translate_y)
stream.rectangle(
offset_x / scale_x, offset_y / scale_y,
slice_width, slice_height)
stream.clip()
stream.end()
image.draw(
stream, intrinsic_width, intrinsic_height,
box.style['image_rendering'])
return scale_x, scale_y
# Top left.
scale_left, scale_top = draw_border_image_region(
x, y, border_left, border_top, 0, 0, slice_left, slice_top)
# Top right.
draw_border_image_region(
x + w - border_right, y, border_right, border_top,
intrinsic_width - slice_right, 0, slice_right, slice_top)
# Bottom right.
scale_right, scale_bottom = draw_border_image_region(
x + w - border_right, y + h - border_bottom, border_right, border_bottom,
intrinsic_width - slice_right, intrinsic_height - slice_bottom,
slice_right, slice_bottom)
# Bottom left.
draw_border_image_region(
x, y + h - border_bottom, border_left, border_bottom,
0, intrinsic_height - slice_bottom, slice_left, slice_bottom)
if x_middle := slice_left + slice_right < intrinsic_width:
# Top middle.
draw_border_image_region(
x + border_left, y, w - border_left - border_right, border_top,
slice_left, 0, intrinsic_width - slice_left - slice_right,
slice_top, repeat_x=repeat_x)
# Bottom middle.
draw_border_image_region(
x + border_left, y + h - border_bottom,
w - border_left - border_right, border_bottom,
slice_left, intrinsic_height - slice_bottom,
intrinsic_width - slice_left - slice_right, slice_bottom,
repeat_x=repeat_x)
if y_middle := slice_top + slice_bottom < intrinsic_height:
# Right middle.
draw_border_image_region(
x + w - border_right, y + border_top,
border_right, h - border_top - border_bottom,
intrinsic_width - slice_right, slice_top,
slice_right, intrinsic_height - slice_top - slice_bottom,
repeat_y=repeat_y)
# Left middle.
draw_border_image_region(
x, y + border_top, border_left, h - border_top - border_bottom,
0, slice_top, slice_left,
intrinsic_height - slice_top - slice_bottom,
repeat_y=repeat_y)
if should_fill and x_middle and y_middle:
# Fill middle.
draw_border_image_region(
x + border_left, y + border_top, w - border_left - border_right,
h - border_top - border_bottom, slice_left, slice_top,
intrinsic_width - slice_left - slice_right,
intrinsic_height - slice_top - slice_bottom,
repeat_x=repeat_x, repeat_y=repeat_y,
scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom)
def clip_border_segment(stream, style, width, side, border_box,
border_widths=None, radii=None):
"""Clip one segment of box border.
The strategy is to remove the zones not needed because of the style or the
side before painting.
"""
bbx, bby, bbw, bbh = border_box
(tlh, tlv), (trh, trv), (brh, brv), (blh, blv) = radii or 4 * ((0, 0),)
bt, br, bb, bl = border_widths or 4 * (width,)
def transition_point(x1, y1, x2, y2):
"""Get the point use for border transition.
The extra boolean returned is ``True`` if the point is in the padding
box (ie. the padding box is rounded).
This point is not specified. We must be sure to be inside the rounded
padding box, and in the zone defined in the "transition zone" allowed
by the specification. We chose the corner of the transition zone. It's
easy to get and gives quite good results, but it seems to be different
from what other browsers do.
"""
return (
((x1, y1), True) if abs(x1) > abs(x2) and abs(y1) > abs(y2)
else ((x2, y2), False))
def corner_half_length(a, b):
"""Return the length of the half of one ellipsis corner.
Inspired by [Ramanujan, S., "Modular Equations and Approximations to
pi" Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372],
wonderfully explained by Dr Rob.
https://mathforum.org/dr.math/faq/formulas/
"""
x = (a - b) / (a + b)
return pi / 8 * (a + b) * (
1 + 3 * x ** 2 / (10 + sqrt(4 - 3 * x ** 2)))
def draw_dash(cx, cy, width=0, height=0, r=0):
"""Draw a single dash or dot centered on cx, cy."""
if style == 'dotted':
ratio = r / sqrt(pi)
stream.move_to(cx + r, cy)
stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)
stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)
stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)
stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)
stream.close()
elif style == 'dashed':
stream.rectangle(cx - width / 2, cy - height / 2, width, height)
if side == 'top':
(px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)
(px2, py2), rounded2 = transition_point(-trh, trv, -br, bt)
width = bt
way = 1
angle = 1
main_offset = bby
elif side == 'right':
(px1, py1), rounded1 = transition_point(-trh, trv, -br, bt)
(px2, py2), rounded2 = transition_point(-brh, -brv, -br, -bb)
width = br
way = 1
angle = 2
main_offset = bbx + bbw
elif side == 'bottom':
(px1, py1), rounded1 = transition_point(blh, -blv, bl, -bb)
(px2, py2), rounded2 = transition_point(-brh, -brv, -br, -bb)
width = bb
way = -1
angle = 3
main_offset = bby + bbh
elif side == 'left':
(px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)
(px2, py2), rounded2 = transition_point(blh, -blv, bl, -bb)
width = bl
way = -1
angle = 4
main_offset = bbx
if side in ('top', 'bottom'):
a1, b1 = px1 - bl / 2, way * py1 - width / 2
a2, b2 = -px2 - br / 2, way * py2 - width / 2
line_length = bbw - px1 + px2
length = bbw
stream.move_to(bbx + bbw, main_offset)
stream.line_to(bbx, main_offset)
stream.line_to(bbx + px1, main_offset + py1)
stream.line_to(bbx + bbw + px2, main_offset + py2)
elif side in ('left', 'right'):
a1, b1 = -way * px1 - width / 2, py1 - bt / 2
a2, b2 = -way * px2 - width / 2, -py2 - bb / 2
line_length = bbh - py1 + py2
length = bbh
stream.move_to(main_offset, bby + bbh)
stream.line_to(main_offset, bby)
stream.line_to(main_offset + px1, bby + py1)
stream.line_to(main_offset + px2, bby + bbh + py2)
if style in ('dotted', 'dashed'):
dash = width if style == 'dotted' else 3 * width
stream.clip(even_odd=True)
stream.end()
if rounded1 or rounded2:
# At least one of the two corners is rounded.
chl1 = corner_half_length(a1, b1)
chl2 = corner_half_length(a2, b2)
length = line_length + chl1 + chl2
dash_length = round(length / dash)
if rounded1 and rounded2:
# 2x dashes.
dash = length / (dash_length + dash_length % 2)
else:
# 2x - 1/2 dashes.
dash = length / (dash_length + dash_length % 2 - 0.5)
dashes1 = ceil((chl1 - dash / 2) / dash)
dashes2 = ceil((chl2 - dash / 2) / dash)
line = floor(line_length / dash)
def draw_dashes(dashes, line, way, x, y, px, py, chl):
if style == 'dotted':
if dashes == 0:
return line + 1, -1
elif dashes == 1:
return line + 1, -0.5
for i in range(1, dashes, 2):
a = ((2 * angle - way) + i * way * dash / chl) / 4 * pi
cx = x if side in ('top', 'bottom') else main_offset
cy = y if side in ('left', 'right') else main_offset
draw_dash(
cx + px - (abs(px) - dash / 2) * cos(a),
cy + py - (abs(py) - dash / 2) * sin(a),
r=(dash / 2))
next_a = ((2 * angle - way) + (i + 2) * way * dash / chl) / 4 * pi
offset = next_a / pi * 2 - angle
if dashes % 2:
line += 1
return line, offset
if dashes == 0:
return line + 1, -1/3
for i in range(0, dashes, 2):
i += 0.5 # half dash
angle1 = (
((2 * angle - way) + i * way * dash / chl) /
4 * pi)
angle2 = (min if way > 0 else max)(
((2 * angle - way) + (i + 1) * way * dash / chl) /
4 * pi,
angle * pi / 2)
if side in ('top', 'bottom'):
stream.move_to(x + px, main_offset + py)
stream.line_to(
x + px - way * px * 1 / tan(angle2), main_offset)
stream.line_to(
x + px - way * px * 1 / tan(angle1), main_offset)
elif side in ('left', 'right'):
stream.move_to(main_offset + px, y + py)
stream.line_to(
main_offset, y + py + way * py * tan(angle2))
stream.line_to(
main_offset, y + py + way * py * tan(angle1))
if angle2 == angle * pi / 2:
offset = (angle1 - angle2) / ((
((2 * angle - way) + (i + 1) * way * dash / chl) /
4 * pi) - angle1)
line += 1
break
else:
offset = 1 - (
(angle * pi / 2 - angle2) / (angle2 - angle1))
return line, offset
line, offset = draw_dashes(dashes1, line, way, bbx, bby, px1, py1, chl1)
line = draw_dashes(
dashes2, line, -way, bbx + bbw, bby + bbh, px2, py2, chl2)[0]
if line_length > 1e-6:
for i in range(0, line, 2):
i += offset
if side in ('top', 'bottom'):
x1 = bbx + px1 + i * dash
x2 = bbx + px1 + (i + 1) * dash
y1 = main_offset - (width if way < 0 else 0)
y2 = y1 + width
elif side in ('left', 'right'):
y1 = bby + py1 + i * dash
y2 = bby + py1 + (i + 1) * dash
x1 = main_offset - (width if way > 0 else 0)
x2 = x1 + width
draw_dash(
x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2,
x2 - x1, y2 - y1, width / 2)
else:
# No rounded corner, dashes on corners and evenly spaced between.
number_of_spaces = floor(length / dash / 2)
number_of_dashes = number_of_spaces + 1
if style == 'dotted':
dash = width
if number_of_spaces:
space = (length - number_of_dashes * dash) / number_of_spaces
else:
space = 0 # no space, unused
elif style == 'dashed':
space = dash = length / (number_of_spaces + number_of_dashes) or 1
for i in range(0, number_of_dashes + 1):
advance = i * (space + dash)
if side == 'top':
cx, cy = bbx + advance + dash / 2, bby + width / 2
dash_width, dash_height = dash, width
elif side == 'right':
cx, cy = bbx + bbw - width / 2, bby + advance + dash / 2
dash_width, dash_height = width, dash
elif side == 'bottom':
cx, cy = bbx + advance + dash / 2, bby + bbh - width / 2
dash_width, dash_height = dash, width
elif side == 'left':
cx, cy = bbx + width / 2, bby + advance + dash / 2
dash_width, dash_height = width, dash
draw_dash(cx, cy, dash_width, dash_height, dash / 2)
stream.clip(even_odd=True)
stream.end()
def draw_rounded_border(stream, box, style, color):
if style in ('ridge', 'groove'):
stream.set_color(color[0])
rounded_box(stream, box.rounded_padding_box())
rounded_box(stream, box.rounded_box_ratio(1 / 2))
stream.fill(even_odd=True)
stream.set_color(color[1])
rounded_box(stream, box.rounded_box_ratio(1 / 2))
rounded_box(stream, box.rounded_border_box())
stream.fill(even_odd=True)
return
stream.set_color(color)
rounded_box(stream, box.rounded_padding_box())
if style == 'double':
rounded_box(stream, box.rounded_box_ratio(1 / 3))
rounded_box(stream, box.rounded_box_ratio(2 / 3))
rounded_box(stream, box.rounded_border_box())
stream.fill(even_odd=True)
def draw_rect_border(stream, box, widths, style, color):
bbx, bby, bbw, bbh = box
bt, br, bb, bl = widths
if style in ('ridge', 'groove'):
stream.set_color(color[0])
stream.rectangle(*box)
stream.rectangle(
bbx + bl / 2, bby + bt / 2,
bbw - (bl + br) / 2, bbh - (bt + bb) / 2)
stream.fill(even_odd=True)
stream.rectangle(
bbx + bl / 2, bby + bt / 2,
bbw - (bl + br) / 2, bbh - (bt + bb) / 2)
stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)
stream.set_color(color[1])
stream.fill(even_odd=True)
return
stream.set_color(color)
stream.rectangle(*box)
if style == 'double':
stream.rectangle(
bbx + bl / 3, bby + bt / 3,
bbw - (bl + br) / 3, bbh - (bt + bb) / 3)
stream.rectangle(
bbx + bl * 2 / 3, bby + bt * 2 / 3,
bbw - (bl + br) * 2 / 3, bbh - (bt + bb) * 2 / 3)
stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)
stream.fill(even_odd=True)
def draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):
assert x1 == x2 or y1 == y2 # Only works for vertical or horizontal lines
with stream.stacked():
if style not in ('ridge', 'groove'):
stream.set_color(color, stroke=True)
if style == 'dashed':
stream.set_dash([5 * thickness], offset)
elif style == 'dotted':
stream.set_line_cap(1)
stream.set_dash([0, 2 * thickness], offset)
if style == 'double':
stream.set_line_width(thickness / 3)
if x1 == x2:
stream.move_to(x1 - thickness / 3, y1)
stream.line_to(x2 - thickness / 3, y2)
stream.move_to(x1 + thickness / 3, y1)
stream.line_to(x2 + thickness / 3, y2)
elif y1 == y2:
stream.move_to(x1, y1 - thickness / 3)
stream.line_to(x2, y2 - thickness / 3)
stream.move_to(x1, y1 + thickness / 3)
stream.line_to(x2, y2 + thickness / 3)
elif style in ('ridge', 'groove'):
stream.set_line_width(thickness / 2)
stream.set_color(color[0], stroke=True)
if x1 == x2:
stream.move_to(x1 + thickness / 4, y1)
stream.line_to(x2 + thickness / 4, y2)
elif y1 == y2:
stream.move_to(x1, y1 + thickness / 4)
stream.line_to(x2, y2 + thickness / 4)
stream.stroke()
stream.set_color(color[1], stroke=True)
if x1 == x2:
stream.move_to(x1 - thickness / 4, y1)
stream.line_to(x2 - thickness / 4, y2)
elif y1 == y2:
stream.move_to(x1, y1 - thickness / 4)
stream.line_to(x2, y2 - thickness / 4)
elif style == 'wavy':
assert y1 == y2 # Only allowed for text decoration
up = 1
radius = 0.75 * thickness
stream.rectangle(x1, y1 - 2 * radius, x2 - x1, 4 * radius)
stream.clip()
stream.end()
x = x1 - offset
stream.move_to(x, y1)
while x < x2:
stream.set_line_width(thickness)
stream.curve_to(
x + radius / 2, y1 + up * radius,
x + 3 * radius / 2, y1 + up * radius,
x + 2 * radius, y1)
x += 2 * radius
up *= -1
else:
stream.set_line_width(thickness)
stream.move_to(x1, y1)
stream.line_to(x2, y2)
stream.stroke()
def draw_outline(stream, box):
width = box.style['outline_width']
offset = box.style['outline_offset']
color = get_color(box.style, 'outline_color')
style = box.style['outline_style']
if box.style['visibility'] == 'visible' and width and color.alpha:
outline_box = (
box.border_box_x() - width - offset,
box.border_box_y() - width - offset,
box.border_width() + 2 * width + 2 * offset,
box.border_height() + 2 * width + 2 * offset)
for side in SIDES:
with stream.artifact(), stream.stacked():
clip_border_segment(stream, style, width, side, outline_box)
draw_rect_border(
stream, outline_box, 4 * (width,), style,
styled_color(style, color, side))
for child in box.children:
if isinstance(child, boxes.Box):
draw_outline(stream, child)
def rounded_box(stream, radii):
"""Draw the path of the border radius box.
``widths`` is a tuple of the inner widths (top, right, bottom, left) from
the border box. Radii are adjusted from these values. Default is (0, 0, 0,
0).
"""
x, y, w, h, tl, tr, br, bl = radii
if all(0 in corner for corner in (tl, tr, br, bl)):
# No radius, draw a rectangle
stream.rectangle(x, y, w, h)
return
r = 0.45
stream.move_to(x + tl[0], y)
stream.line_to(x + w - tr[0], y)
stream.curve_to(
x + w - tr[0] * r, y, x + w, y + tr[1] * r, x + w, y + tr[1])
stream.line_to(x + w, y + h - br[1])
stream.curve_to(
x + w, y + h - br[1] * r, x + w - br[0] * r, y + h, x + w - br[0],
y + h)
stream.line_to(x + bl[0], y + h)
stream.curve_to(
x + bl[0] * r, y + h, x, y + h - bl[1] * r, x, y + h - bl[1])
stream.line_to(x, y + tl[1])
stream.curve_to(
x, y + tl[1] * r, x + tl[0] * r, y, x + tl[0], y)