- 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>
725 lines
30 KiB
Python
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)
|