weasyprint 65.0__py3-none-any.whl → 66.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weasyprint/__init__.py +4 -1
- weasyprint/__main__.py +2 -0
- weasyprint/css/__init__.py +12 -4
- weasyprint/css/computed_values.py +8 -2
- weasyprint/css/html5_ua.css +10 -11
- weasyprint/css/html5_ua_form.css +1 -1
- weasyprint/css/utils.py +1 -1
- weasyprint/document.py +2 -10
- weasyprint/draw/__init__.py +51 -57
- weasyprint/draw/border.py +120 -66
- weasyprint/draw/text.py +1 -2
- weasyprint/formatting_structure/boxes.py +3 -2
- weasyprint/formatting_structure/build.py +32 -42
- weasyprint/images.py +8 -15
- weasyprint/layout/__init__.py +5 -2
- weasyprint/layout/absolute.py +4 -1
- weasyprint/layout/block.py +60 -29
- weasyprint/layout/column.py +1 -0
- weasyprint/layout/flex.py +55 -29
- weasyprint/layout/float.py +8 -1
- weasyprint/layout/grid.py +1 -1
- weasyprint/layout/inline.py +7 -8
- weasyprint/layout/page.py +43 -15
- weasyprint/layout/preferred.py +59 -32
- weasyprint/layout/table.py +8 -4
- weasyprint/pdf/__init__.py +13 -6
- weasyprint/pdf/anchors.py +2 -2
- weasyprint/pdf/pdfua.py +7 -115
- weasyprint/pdf/stream.py +40 -49
- weasyprint/pdf/tags.py +305 -0
- weasyprint/stacking.py +14 -15
- weasyprint/svg/__init__.py +22 -11
- weasyprint/svg/bounding_box.py +4 -2
- weasyprint/svg/defs.py +4 -9
- weasyprint/svg/utils.py +9 -5
- weasyprint/text/fonts.py +1 -1
- weasyprint/text/line_break.py +45 -26
- weasyprint/urls.py +21 -10
- {weasyprint-65.0.dist-info → weasyprint-66.0.dist-info}/METADATA +1 -1
- weasyprint-66.0.dist-info/RECORD +74 -0
- {weasyprint-65.0.dist-info → weasyprint-66.0.dist-info}/WHEEL +1 -1
- weasyprint/draw/stack.py +0 -13
- weasyprint-65.0.dist-info/RECORD +0 -74
- {weasyprint-65.0.dist-info → weasyprint-66.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-65.0.dist-info → weasyprint-66.0.dist-info}/licenses/LICENSE +0 -0
weasyprint/draw/border.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""Draw borders."""
|
|
2
2
|
|
|
3
|
-
from math import ceil, floor, pi, sqrt, tan
|
|
3
|
+
from math import ceil, cos, floor, pi, sin, sqrt, tan
|
|
4
4
|
|
|
5
5
|
from ..formatting_structure import boxes
|
|
6
6
|
from ..layout import replaced
|
|
7
7
|
from ..layout.percent import percentage
|
|
8
8
|
from ..matrix import Matrix
|
|
9
9
|
from .color import get_color, styled_color
|
|
10
|
-
from .stack import stacked
|
|
11
10
|
|
|
12
11
|
SIDES = ('top', 'right', 'bottom', 'left')
|
|
13
12
|
|
|
@@ -26,6 +25,34 @@ def set_mask_border(stream, box):
|
|
|
26
25
|
box.style['mask_border_width'])
|
|
27
26
|
|
|
28
27
|
|
|
28
|
+
def draw_column_rules(stream, box):
|
|
29
|
+
"""Draw the column rules to a ``pdf.stream.Stream``."""
|
|
30
|
+
border_widths = (0, 0, 0, box.style['column_rule_width'])
|
|
31
|
+
skip_next = True
|
|
32
|
+
for child in box.children:
|
|
33
|
+
if child.style['column_span'] == 'all':
|
|
34
|
+
skip_next = True
|
|
35
|
+
continue
|
|
36
|
+
elif skip_next:
|
|
37
|
+
skip_next = False
|
|
38
|
+
continue
|
|
39
|
+
with stream.stacked():
|
|
40
|
+
rule_width = box.style['column_rule_width']
|
|
41
|
+
rule_style = box.style['column_rule_style']
|
|
42
|
+
if box.style['column_gap'] == 'normal':
|
|
43
|
+
gap = box.style['font_size'] # normal equals 1em
|
|
44
|
+
else:
|
|
45
|
+
gap = percentage(box.style['column_gap'], box.width)
|
|
46
|
+
position_x = (
|
|
47
|
+
child.position_x - (box.style['column_rule_width'] + gap) / 2)
|
|
48
|
+
border_box = position_x, child.position_y, rule_width, child.height
|
|
49
|
+
clip_border_segment(
|
|
50
|
+
stream, rule_style, rule_width, 'left', border_box, border_widths)
|
|
51
|
+
color = styled_color(
|
|
52
|
+
rule_style, get_color(box.style, 'column_rule_color'), 'left')
|
|
53
|
+
draw_rect_border(stream, border_box, border_widths, rule_style, color)
|
|
54
|
+
|
|
55
|
+
|
|
29
56
|
def draw_border(stream, box):
|
|
30
57
|
"""Draw the box borders and column rules to a ``pdf.stream.Stream``."""
|
|
31
58
|
|
|
@@ -33,43 +60,22 @@ def draw_border(stream, box):
|
|
|
33
60
|
if box.style['visibility'] != 'visible':
|
|
34
61
|
return
|
|
35
62
|
|
|
36
|
-
# Draw column
|
|
63
|
+
# Draw column rules.
|
|
37
64
|
columns = (
|
|
38
65
|
isinstance(box, boxes.BlockContainerBox) and (
|
|
39
66
|
box.style['column_width'] != 'auto' or
|
|
40
67
|
box.style['column_count'] != 'auto'))
|
|
41
68
|
if columns and box.style['column_rule_width']:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for child in box.children:
|
|
45
|
-
if child.style['column_span'] == 'all':
|
|
46
|
-
skip_next = True
|
|
47
|
-
continue
|
|
48
|
-
elif skip_next:
|
|
49
|
-
skip_next = False
|
|
50
|
-
continue
|
|
51
|
-
with stacked(stream):
|
|
52
|
-
rule_width = box.style['column_rule_width']
|
|
53
|
-
rule_style = box.style['column_rule_style']
|
|
54
|
-
if box.style['column_gap'] == 'normal':
|
|
55
|
-
gap = box.style['font_size'] # normal equals 1em
|
|
56
|
-
else:
|
|
57
|
-
gap = percentage(box.style['column_gap'], box.width)
|
|
58
|
-
position_x = (
|
|
59
|
-
child.position_x - (box.style['column_rule_width'] + gap) / 2)
|
|
60
|
-
border_box = (position_x, child.position_y, rule_width, child.height)
|
|
61
|
-
clip_border_segment(
|
|
62
|
-
stream, rule_style, rule_width, 'left', border_box, border_widths)
|
|
63
|
-
color = styled_color(
|
|
64
|
-
rule_style, get_color(box.style, 'column_rule_color'), 'left')
|
|
65
|
-
draw_rect_border(stream, border_box, border_widths, rule_style, color)
|
|
69
|
+
with stream.artifact():
|
|
70
|
+
draw_column_rules(stream, box)
|
|
66
71
|
|
|
67
72
|
# If there's a border image, that takes precedence.
|
|
68
73
|
if box.style['border_image_source'][0] != 'none' and box.border_image is not None:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
with stream.artifact():
|
|
75
|
+
draw_border_image(
|
|
76
|
+
box, stream, box.border_image, box.style['border_image_slice'],
|
|
77
|
+
box.style['border_image_repeat'], box.style['border_image_outset'],
|
|
78
|
+
box.style['border_image_width'])
|
|
73
79
|
return
|
|
74
80
|
|
|
75
81
|
widths = [getattr(box, f'border_{side}_width') for side in SIDES]
|
|
@@ -88,7 +94,8 @@ def draw_border(stream, box):
|
|
|
88
94
|
four_sides = 0 not in widths # no 0-width border, to avoid PDF artifacts
|
|
89
95
|
if simple_style and single_color and four_sides:
|
|
90
96
|
# Simple case, we only draw rounded rectangles.
|
|
91
|
-
|
|
97
|
+
with stream.artifact():
|
|
98
|
+
draw_rounded_border(stream, box, styles[0], colors[0])
|
|
92
99
|
return
|
|
93
100
|
|
|
94
101
|
# We're not smart enough to find a good way to draw the borders, we must
|
|
@@ -99,12 +106,11 @@ def draw_border(stream, box):
|
|
|
99
106
|
side, width, color, style = values[index]
|
|
100
107
|
if width == 0 or not color:
|
|
101
108
|
continue
|
|
102
|
-
with stacked(
|
|
109
|
+
with stream.artifact(), stream.stacked():
|
|
103
110
|
clip_border_segment(
|
|
104
111
|
stream, style, width, side, box.rounded_border_box()[:4],
|
|
105
112
|
widths, box.rounded_border_box()[4:])
|
|
106
|
-
draw_rounded_border(
|
|
107
|
-
stream, box, style, styled_color(style, color, side))
|
|
113
|
+
draw_rounded_border(stream, box, style, styled_color(style, color, side))
|
|
108
114
|
|
|
109
115
|
|
|
110
116
|
def draw_border_image(box, stream, image, border_slice, border_repeat, border_outset,
|
|
@@ -237,7 +243,7 @@ def draw_border_image(box, stream, image, border_slice, border_repeat, border_ou
|
|
|
237
243
|
offset_x = rendered_width * slice_x / intrinsic_width
|
|
238
244
|
offset_y = rendered_height * slice_y / intrinsic_height
|
|
239
245
|
|
|
240
|
-
with stacked(
|
|
246
|
+
with stream.stacked():
|
|
241
247
|
stream.rectangle(x, y, width, height)
|
|
242
248
|
stream.clip()
|
|
243
249
|
stream.end()
|
|
@@ -245,7 +251,7 @@ def draw_border_image(box, stream, image, border_slice, border_repeat, border_ou
|
|
|
245
251
|
stream.transform(a=scale_x, d=scale_y)
|
|
246
252
|
for i in range(n_repeats_x):
|
|
247
253
|
for j in range(n_repeats_y):
|
|
248
|
-
with stacked(
|
|
254
|
+
with stream.stacked():
|
|
249
255
|
translate_x = i * (slice_width + extra_dx)
|
|
250
256
|
translate_y = j * (slice_height + extra_dy)
|
|
251
257
|
stream.transform(e=translate_x, f=translate_y)
|
|
@@ -357,6 +363,19 @@ def clip_border_segment(stream, style, width, side, border_box,
|
|
|
357
363
|
return pi / 8 * (a + b) * (
|
|
358
364
|
1 + 3 * x ** 2 / (10 + sqrt(4 - 3 * x ** 2)))
|
|
359
365
|
|
|
366
|
+
def draw_dash(cx, cy, width=0, height=0, r=0):
|
|
367
|
+
"""Draw a single dash or dot centered on cx, cy."""
|
|
368
|
+
if style == 'dotted':
|
|
369
|
+
ratio = r / sqrt(pi)
|
|
370
|
+
stream.move_to(cx + r, cy)
|
|
371
|
+
stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)
|
|
372
|
+
stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)
|
|
373
|
+
stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)
|
|
374
|
+
stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)
|
|
375
|
+
stream.close()
|
|
376
|
+
elif style == 'dashed':
|
|
377
|
+
stream.rectangle(cx - width / 2, cy - height / 2, width, height)
|
|
378
|
+
|
|
360
379
|
if side == 'top':
|
|
361
380
|
(px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)
|
|
362
381
|
(px2, py2), rounded2 = transition_point(-trh, trv, -br, bt)
|
|
@@ -407,25 +426,48 @@ def clip_border_segment(stream, style, width, side, border_box,
|
|
|
407
426
|
|
|
408
427
|
if style in ('dotted', 'dashed'):
|
|
409
428
|
dash = width if style == 'dotted' else 3 * width
|
|
429
|
+
stream.clip(even_odd=True)
|
|
430
|
+
stream.end()
|
|
410
431
|
if rounded1 or rounded2:
|
|
411
|
-
# At least one of the two corners is rounded
|
|
432
|
+
# At least one of the two corners is rounded.
|
|
412
433
|
chl1 = corner_half_length(a1, b1)
|
|
413
434
|
chl2 = corner_half_length(a2, b2)
|
|
414
435
|
length = line_length + chl1 + chl2
|
|
415
436
|
dash_length = round(length / dash)
|
|
416
437
|
if rounded1 and rounded2:
|
|
417
|
-
# 2x dashes
|
|
438
|
+
# 2x dashes.
|
|
418
439
|
dash = length / (dash_length + dash_length % 2)
|
|
419
440
|
else:
|
|
420
|
-
# 2x - 1/2 dashes
|
|
441
|
+
# 2x - 1/2 dashes.
|
|
421
442
|
dash = length / (dash_length + dash_length % 2 - 0.5)
|
|
422
443
|
dashes1 = ceil((chl1 - dash / 2) / dash)
|
|
423
444
|
dashes2 = ceil((chl2 - dash / 2) / dash)
|
|
424
445
|
line = floor(line_length / dash)
|
|
425
446
|
|
|
426
|
-
def
|
|
427
|
-
if
|
|
428
|
-
|
|
447
|
+
def draw_dashes(dashes, line, way, x, y, px, py, chl):
|
|
448
|
+
if style == 'dotted':
|
|
449
|
+
if dashes == 0:
|
|
450
|
+
return line + 1, -1
|
|
451
|
+
elif dashes == 1:
|
|
452
|
+
return line + 1, -0.5
|
|
453
|
+
|
|
454
|
+
for i in range(1, dashes, 2):
|
|
455
|
+
a = ((2 * angle - way) + i * way * dash / chl) / 4 * pi
|
|
456
|
+
cx = x if side in ('top', 'bottom') else main_offset
|
|
457
|
+
cy = y if side in ('left', 'right') else main_offset
|
|
458
|
+
draw_dash(
|
|
459
|
+
cx + px - (abs(px) - dash / 2) * cos(a),
|
|
460
|
+
cy + py - (abs(py) - dash / 2) * sin(a),
|
|
461
|
+
r=(dash / 2))
|
|
462
|
+
next_a = ((2 * angle - way) + (i + 2) * way * dash / chl) / 4 * pi
|
|
463
|
+
offset = next_a / pi * 2 - angle
|
|
464
|
+
if dashes % 2:
|
|
465
|
+
line += 1
|
|
466
|
+
return line, offset
|
|
467
|
+
|
|
468
|
+
if dashes == 0:
|
|
469
|
+
return line + 1, -1/3
|
|
470
|
+
|
|
429
471
|
for i in range(0, dashes, 2):
|
|
430
472
|
i += 0.5 # half dash
|
|
431
473
|
angle1 = (
|
|
@@ -458,42 +500,53 @@ def clip_border_segment(stream, style, width, side, border_box,
|
|
|
458
500
|
(angle * pi / 2 - angle2) / (angle2 - angle1))
|
|
459
501
|
return line, offset
|
|
460
502
|
|
|
461
|
-
line, offset =
|
|
462
|
-
|
|
463
|
-
line = draw_dots(
|
|
503
|
+
line, offset = draw_dashes(dashes1, line, way, bbx, bby, px1, py1, chl1)
|
|
504
|
+
line = draw_dashes(
|
|
464
505
|
dashes2, line, -way, bbx + bbw, bby + bbh, px2, py2, chl2)[0]
|
|
465
506
|
|
|
466
507
|
if line_length > 1e-6:
|
|
467
508
|
for i in range(0, line, 2):
|
|
468
509
|
i += offset
|
|
469
510
|
if side in ('top', 'bottom'):
|
|
470
|
-
x1 =
|
|
471
|
-
x2 =
|
|
511
|
+
x1 = bbx + px1 + i * dash
|
|
512
|
+
x2 = bbx + px1 + (i + 1) * dash
|
|
472
513
|
y1 = main_offset - (width if way < 0 else 0)
|
|
473
514
|
y2 = y1 + width
|
|
474
515
|
elif side in ('left', 'right'):
|
|
475
|
-
y1 =
|
|
476
|
-
y2 =
|
|
516
|
+
y1 = bby + py1 + i * dash
|
|
517
|
+
y2 = bby + py1 + (i + 1) * dash
|
|
477
518
|
x1 = main_offset - (width if way > 0 else 0)
|
|
478
519
|
x2 = x1 + width
|
|
479
|
-
|
|
520
|
+
draw_dash(
|
|
521
|
+
x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2,
|
|
522
|
+
x2 - x1, y2 - y1, width / 2)
|
|
480
523
|
else:
|
|
481
|
-
#
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
524
|
+
# No rounded corner, dashes on corners and evenly spaced between.
|
|
525
|
+
number_of_spaces = floor(length / dash / 2)
|
|
526
|
+
number_of_dashes = number_of_spaces + 1
|
|
527
|
+
if style == 'dotted':
|
|
528
|
+
dash = width
|
|
529
|
+
if number_of_spaces:
|
|
530
|
+
space = (length - number_of_dashes * dash) / number_of_spaces
|
|
531
|
+
else:
|
|
532
|
+
space = 0 # no space, unused
|
|
533
|
+
elif style == 'dashed':
|
|
534
|
+
space = dash = length / (number_of_spaces + number_of_dashes) or 1
|
|
535
|
+
for i in range(0, number_of_dashes + 1):
|
|
536
|
+
advance = i * (space + dash)
|
|
487
537
|
if side == 'top':
|
|
488
|
-
|
|
538
|
+
cx, cy = bbx + advance + dash / 2, bby + width / 2
|
|
539
|
+
dash_width, dash_height = dash, width
|
|
489
540
|
elif side == 'right':
|
|
490
|
-
|
|
491
|
-
|
|
541
|
+
cx, cy = bbx + bbw - width / 2, bby + advance + dash / 2
|
|
542
|
+
dash_width, dash_height = width, dash
|
|
492
543
|
elif side == 'bottom':
|
|
493
|
-
|
|
494
|
-
|
|
544
|
+
cx, cy = bbx + advance + dash / 2, bby + bbh - width / 2
|
|
545
|
+
dash_width, dash_height = dash, width
|
|
495
546
|
elif side == 'left':
|
|
496
|
-
|
|
547
|
+
cx, cy = bbx + width / 2, bby + advance + dash / 2
|
|
548
|
+
dash_width, dash_height = width, dash
|
|
549
|
+
draw_dash(cx, cy, dash_width, dash_height, dash / 2)
|
|
497
550
|
stream.clip(even_odd=True)
|
|
498
551
|
stream.end()
|
|
499
552
|
|
|
@@ -551,14 +604,15 @@ def draw_rect_border(stream, box, widths, style, color):
|
|
|
551
604
|
def draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):
|
|
552
605
|
assert x1 == x2 or y1 == y2 # Only works for vertical or horizontal lines
|
|
553
606
|
|
|
554
|
-
with stacked(
|
|
607
|
+
with stream.stacked():
|
|
555
608
|
if style not in ('ridge', 'groove'):
|
|
556
609
|
stream.set_color(color, stroke=True)
|
|
557
610
|
|
|
558
611
|
if style == 'dashed':
|
|
559
612
|
stream.set_dash([5 * thickness], offset)
|
|
560
613
|
elif style == 'dotted':
|
|
561
|
-
stream.
|
|
614
|
+
stream.set_line_cap(1)
|
|
615
|
+
stream.set_dash([0, 2 * thickness], offset)
|
|
562
616
|
|
|
563
617
|
if style == 'double':
|
|
564
618
|
stream.set_line_width(thickness / 3)
|
|
@@ -627,7 +681,7 @@ def draw_outline(stream, box):
|
|
|
627
681
|
box.border_width() + 2 * width + 2 * offset,
|
|
628
682
|
box.border_height() + 2 * width + 2 * offset)
|
|
629
683
|
for side in SIDES:
|
|
630
|
-
with stacked(
|
|
684
|
+
with stream.artifact(), stream.stacked():
|
|
631
685
|
clip_border_segment(stream, style, width, side, outline_box)
|
|
632
686
|
draw_rect_border(
|
|
633
687
|
stream, outline_box, 4 * (width,), style,
|
weasyprint/draw/text.py
CHANGED
|
@@ -12,7 +12,6 @@ from ..text.fonts import get_hb_object_data
|
|
|
12
12
|
from ..text.line_break import get_last_word_end
|
|
13
13
|
from .border import draw_line
|
|
14
14
|
from .color import get_color
|
|
15
|
-
from .stack import stacked
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis):
|
|
@@ -78,7 +77,7 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis):
|
|
|
78
77
|
def draw_emojis(stream, font_size, x, y, emojis):
|
|
79
78
|
"""Draw list of emojis."""
|
|
80
79
|
for image, font, a, d, e, f in emojis:
|
|
81
|
-
with stacked(
|
|
80
|
+
with stream.stacked():
|
|
82
81
|
stream.transform(a=a, d=d, e=x + e * font_size, f=y + f)
|
|
83
82
|
image.draw(stream, font_size, font_size, None)
|
|
84
83
|
|
|
@@ -72,6 +72,7 @@ class Box:
|
|
|
72
72
|
is_for_root_element = False
|
|
73
73
|
is_column = False
|
|
74
74
|
is_leader = False
|
|
75
|
+
is_outside_marker = False
|
|
75
76
|
|
|
76
77
|
# Other properties
|
|
77
78
|
transformation_matrix = None
|
|
@@ -80,6 +81,8 @@ class Box:
|
|
|
80
81
|
footnote = None
|
|
81
82
|
cached_counter_values = None
|
|
82
83
|
missing_link = None
|
|
84
|
+
link_annotation = None
|
|
85
|
+
force_fragmentation = False
|
|
83
86
|
|
|
84
87
|
# Default, overriden on some subclasses
|
|
85
88
|
def all_children(self):
|
|
@@ -502,8 +505,6 @@ class InlineBox(InlineLevelBox, ParentBox):
|
|
|
502
505
|
inline box.
|
|
503
506
|
|
|
504
507
|
"""
|
|
505
|
-
link_annotation = None
|
|
506
|
-
|
|
507
508
|
def hit_area(self):
|
|
508
509
|
"""Return the (x, y, w, h) rectangle where the box is clickable."""
|
|
509
510
|
# Use line-height (margin_height) rather than border_height
|
|
@@ -195,9 +195,10 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
195
195
|
footnote = child_boxes[0]
|
|
196
196
|
footnote.style['float'] = 'none'
|
|
197
197
|
footnotes.append(footnote)
|
|
198
|
-
call_style = style_for(element, 'footnote-call')
|
|
198
|
+
call_style = style_for(footnote.element, 'footnote-call')
|
|
199
199
|
footnote_call = make_box(
|
|
200
|
-
f'{element.tag}::footnote-call', call_style, [],
|
|
200
|
+
f'{footnote.element.tag}::footnote-call', call_style, [],
|
|
201
|
+
footnote.element)
|
|
201
202
|
footnote_call.children = content_to_boxes(
|
|
202
203
|
call_style, footnote_call, quote_depth, counter_values,
|
|
203
204
|
get_image_from_uri, target_collector, counter_style)
|
|
@@ -250,7 +251,7 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
250
251
|
marker = make_box(
|
|
251
252
|
f'{element.tag}::footnote-marker', marker_style, [], element)
|
|
252
253
|
marker.children = content_to_boxes(
|
|
253
|
-
marker_style,
|
|
254
|
+
marker_style, marker, quote_depth, counter_values, get_image_from_uri,
|
|
254
255
|
target_collector, counter_style)
|
|
255
256
|
box.children.insert(0, marker)
|
|
256
257
|
|
|
@@ -344,7 +345,6 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
|
|
|
344
345
|
if not children and style['list_style_type'] != 'none':
|
|
345
346
|
counter_value = counter_values.get('list-item', [0])[-1]
|
|
346
347
|
counter_type = style['list_style_type']
|
|
347
|
-
# TODO: rtl numbered list has the dot on the left
|
|
348
348
|
if marker_text := counter_style.render_marker(counter_type, counter_value):
|
|
349
349
|
box = boxes.TextBox.anonymous_from(box, marker_text)
|
|
350
350
|
box.style['white_space'] = 'pre-wrap'
|
|
@@ -358,13 +358,7 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
|
|
|
358
358
|
# We can safely edit everything that can't be changed by user style
|
|
359
359
|
# See https://drafts.csswg.org/css-pseudo-4/#marker-pseudo
|
|
360
360
|
marker_box.style['position'] = 'absolute'
|
|
361
|
-
|
|
362
|
-
translate_x = properties.Dimension(-100, '%')
|
|
363
|
-
else:
|
|
364
|
-
translate_x = properties.Dimension(100, '%')
|
|
365
|
-
translate_y = properties.ZERO_PIXELS
|
|
366
|
-
marker_box.style['transform'] = (
|
|
367
|
-
('translate', (translate_x, translate_y)),)
|
|
361
|
+
marker_box.is_outside_marker = True
|
|
368
362
|
else:
|
|
369
363
|
marker_box = boxes.InlineBox.anonymous_from(box, children)
|
|
370
364
|
yield marker_box
|
|
@@ -421,7 +415,7 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
|
|
421
415
|
elif type_ == 'url' and get_image_from_uri is not None:
|
|
422
416
|
origin, uri = value
|
|
423
417
|
if origin != 'external':
|
|
424
|
-
# Embedding internal references is impossible
|
|
418
|
+
# Embedding internal references is impossible.
|
|
425
419
|
continue
|
|
426
420
|
image = get_image_from_uri(
|
|
427
421
|
url=uri, orientation=parent_box.style['image_orientation'])
|
|
@@ -431,12 +425,12 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
|
|
431
425
|
elif type_ == 'content()':
|
|
432
426
|
added_text = extract_text(value, parent_box)
|
|
433
427
|
# Simulate the step of white space processing
|
|
434
|
-
# (normally done during the layout)
|
|
428
|
+
# (normally done during the layout).
|
|
435
429
|
add_text(added_text.strip())
|
|
436
430
|
elif type_ == 'string()':
|
|
437
431
|
if not in_page_context:
|
|
438
|
-
# string() is currently only valid in @page context
|
|
439
|
-
# See
|
|
432
|
+
# string() is currently only valid in @page context.
|
|
433
|
+
# See issue #723.
|
|
440
434
|
LOGGER.warning(
|
|
441
435
|
'"string(%s)" is only allowed in page margins',
|
|
442
436
|
' '.join(value))
|
|
@@ -810,9 +804,9 @@ def table_boxes_children(box, children):
|
|
|
810
804
|
children = [
|
|
811
805
|
child
|
|
812
806
|
for prev_child, child, next_child in zip(
|
|
813
|
-
[None
|
|
807
|
+
[None, *children[:-1]],
|
|
814
808
|
children,
|
|
815
|
-
children[1:]
|
|
809
|
+
[*children[1:], None]
|
|
816
810
|
)
|
|
817
811
|
if not (
|
|
818
812
|
# Ignore some whitespace: rule 1.4
|
|
@@ -990,6 +984,25 @@ def wrap_table(box, children):
|
|
|
990
984
|
return wrapper
|
|
991
985
|
|
|
992
986
|
|
|
987
|
+
def blockify(box, layout):
|
|
988
|
+
"""Turn an inline box into a block box."""
|
|
989
|
+
# See https://drafts.csswg.org/css-display-4/#blockify.
|
|
990
|
+
if isinstance(box, boxes.InlineBlockBox):
|
|
991
|
+
anonymous = boxes.BlockBox.anonymous_from(box, box.children)
|
|
992
|
+
elif isinstance(box, boxes.InlineReplacedBox):
|
|
993
|
+
replacement = box.replacement
|
|
994
|
+
anonymous = boxes.BlockReplacedBox.anonymous_from(box, replacement)
|
|
995
|
+
elif isinstance(box, boxes.InlineLevelBox):
|
|
996
|
+
anonymous = boxes.BlockBox.anonymous_from(box, [box])
|
|
997
|
+
setattr(box, f'is_{layout}_item', False)
|
|
998
|
+
else:
|
|
999
|
+
return box
|
|
1000
|
+
anonymous.style = box.style
|
|
1001
|
+
setattr(anonymous, f'is_{layout}_item', True)
|
|
1002
|
+
return anonymous
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
|
|
993
1006
|
def flex_boxes(box):
|
|
994
1007
|
"""Remove and add boxes according to the flex model.
|
|
995
1008
|
|
|
@@ -1019,18 +1032,7 @@ def flex_children(box, children):
|
|
|
1019
1032
|
# affected by the white-space property"
|
|
1020
1033
|
# https://www.w3.org/TR/css-flexbox-1/#flex-items
|
|
1021
1034
|
continue
|
|
1022
|
-
|
|
1023
|
-
anonymous = boxes.BlockBox.anonymous_from(child, child.children)
|
|
1024
|
-
anonymous.style = child.style
|
|
1025
|
-
anonymous.is_flex_item = True
|
|
1026
|
-
flex_children.append(anonymous)
|
|
1027
|
-
elif isinstance(child, boxes.InlineLevelBox):
|
|
1028
|
-
anonymous = boxes.BlockBox.anonymous_from(child, [child])
|
|
1029
|
-
anonymous.style = child.style
|
|
1030
|
-
anonymous.is_flex_item = True
|
|
1031
|
-
flex_children.append(anonymous)
|
|
1032
|
-
else:
|
|
1033
|
-
flex_children.append(child)
|
|
1035
|
+
flex_children.append(blockify(child, 'flex'))
|
|
1034
1036
|
return flex_children
|
|
1035
1037
|
else:
|
|
1036
1038
|
return children
|
|
@@ -1064,19 +1066,7 @@ def grid_children(box, children):
|
|
|
1064
1066
|
# affected by the white-space property"
|
|
1065
1067
|
# https://drafts.csswg.org/css-grid-2/#grid-item
|
|
1066
1068
|
continue
|
|
1067
|
-
|
|
1068
|
-
anonymous = boxes.BlockBox.anonymous_from(child, child.children)
|
|
1069
|
-
anonymous.style = child.style
|
|
1070
|
-
anonymous.is_grid_item = True
|
|
1071
|
-
grid_children.append(anonymous)
|
|
1072
|
-
elif isinstance(child, boxes.InlineLevelBox):
|
|
1073
|
-
anonymous = boxes.BlockBox.anonymous_from(child, [child])
|
|
1074
|
-
anonymous.style = child.style
|
|
1075
|
-
child.is_grid_item = False
|
|
1076
|
-
anonymous.is_grid_item = True
|
|
1077
|
-
grid_children.append(anonymous)
|
|
1078
|
-
else:
|
|
1079
|
-
grid_children.append(child)
|
|
1069
|
+
grid_children.append(blockify(child, 'grid'))
|
|
1080
1070
|
return grid_children
|
|
1081
1071
|
else:
|
|
1082
1072
|
return children
|
weasyprint/images.py
CHANGED
|
@@ -8,8 +8,6 @@ from io import BytesIO
|
|
|
8
8
|
from itertools import cycle
|
|
9
9
|
from math import inf
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from urllib.parse import urlparse
|
|
12
|
-
from urllib.request import url2pathname
|
|
13
11
|
from xml.etree import ElementTree
|
|
14
12
|
|
|
15
13
|
import pydyf
|
|
@@ -70,7 +68,7 @@ class RasterImage:
|
|
|
70
68
|
|
|
71
69
|
# The presence of the APP14 segment indicates an Adobe image with
|
|
72
70
|
# inverted CMYK data. Specify a Decode Array to invert it again back to
|
|
73
|
-
# normal. See
|
|
71
|
+
# normal. See PR #2179.
|
|
74
72
|
app14 = getattr(original_pillow_image, 'app', {}).get('APP14')
|
|
75
73
|
self.invert_colors = self.mode == 'CMYK' and app14 is not None
|
|
76
74
|
|
|
@@ -299,11 +297,6 @@ def get_image_from_uri(cache, url_fetcher, options, url, forced_mime_type=None,
|
|
|
299
297
|
|
|
300
298
|
try:
|
|
301
299
|
with fetch(url_fetcher, url) as result:
|
|
302
|
-
parsed_url = urlparse(result.get('redirected_url'))
|
|
303
|
-
if parsed_url.scheme == 'file':
|
|
304
|
-
filename = url2pathname(parsed_url.path)
|
|
305
|
-
else:
|
|
306
|
-
filename = None
|
|
307
300
|
if 'string' in result:
|
|
308
301
|
string = result['string']
|
|
309
302
|
else:
|
|
@@ -337,9 +330,9 @@ def get_image_from_uri(cache, url_fetcher, options, url, forced_mime_type=None,
|
|
|
337
330
|
else:
|
|
338
331
|
# Store image id to enable cache in Stream.add_image
|
|
339
332
|
image_id = md5(url.encode(), usedforsecurity=False).hexdigest()
|
|
333
|
+
path = result.get('path')
|
|
340
334
|
image = RasterImage(
|
|
341
|
-
pillow_image, image_id, string,
|
|
342
|
-
orientation, options)
|
|
335
|
+
pillow_image, image_id, string, path, cache, orientation, options)
|
|
343
336
|
|
|
344
337
|
except (URLFetchingError, ImageLoadingError) as exception:
|
|
345
338
|
LOGGER.error('Failed to load image at %r: %s', url, exception)
|
|
@@ -733,8 +726,8 @@ class RadialGradient(Gradient):
|
|
|
733
726
|
intermediate_color = gradient_average_color(
|
|
734
727
|
[previous_color, previous_color, color, color],
|
|
735
728
|
[previous_position, 0, 0, position])
|
|
736
|
-
colors = [intermediate_color
|
|
737
|
-
positions = [0
|
|
729
|
+
colors = [intermediate_color, *colors[i:]]
|
|
730
|
+
positions = [0, *positions[i:]]
|
|
738
731
|
break
|
|
739
732
|
first, last, positions = normalize_stop_positions(positions)
|
|
740
733
|
|
|
@@ -775,7 +768,7 @@ class RadialGradient(Gradient):
|
|
|
775
768
|
colors *= repeat
|
|
776
769
|
positions = [
|
|
777
770
|
i + position for i in range(repeat) for position in positions]
|
|
778
|
-
points = points[:5]
|
|
771
|
+
points = (*points[:5], points[5] + gradient_length * repeat_after)
|
|
779
772
|
|
|
780
773
|
if points[2] == 0:
|
|
781
774
|
# Inner circle has 0 radius, no need to repeat inside, return
|
|
@@ -785,7 +778,7 @@ class RadialGradient(Gradient):
|
|
|
785
778
|
repeat_before = points[2] / gradient_length
|
|
786
779
|
|
|
787
780
|
# Set the inner circle size to 0
|
|
788
|
-
points = points[:2]
|
|
781
|
+
points = (*points[:2], 0, *points[3:])
|
|
789
782
|
|
|
790
783
|
# Find how many times the whole gradient can be repeated
|
|
791
784
|
full_repeat = int(repeat_before)
|
|
@@ -830,7 +823,7 @@ class RadialGradient(Gradient):
|
|
|
830
823
|
average_positions = [position, ratio, ratio, next_position]
|
|
831
824
|
zero_color = gradient_average_color(
|
|
832
825
|
average_colors, average_positions)
|
|
833
|
-
colors = [zero_color
|
|
826
|
+
colors = [zero_color, *original_colors[-(i - 1):], *colors]
|
|
834
827
|
new_positions = [
|
|
835
828
|
position - 1 - full_repeat for position
|
|
836
829
|
in original_positions[-(i - 1):]]
|
weasyprint/layout/__init__.py
CHANGED
|
@@ -389,10 +389,13 @@ class LayoutContext:
|
|
|
389
389
|
if not self.in_column:
|
|
390
390
|
self.page_bottom -= footnote_area.margin_height()
|
|
391
391
|
last_child = footnote_area.children[-1]
|
|
392
|
-
|
|
393
|
-
last_child.position_y + last_child.margin_height()
|
|
392
|
+
last_child_bottom = (
|
|
393
|
+
last_child.position_y + last_child.margin_height() -
|
|
394
|
+
last_child.margin_bottom)
|
|
395
|
+
footnote_area_bottom = (
|
|
394
396
|
footnote_area.position_y + footnote_area.margin_height() -
|
|
395
397
|
footnote_area.margin_bottom)
|
|
398
|
+
overflow = last_child_bottom > footnote_area_bottom
|
|
396
399
|
return overflow
|
|
397
400
|
else:
|
|
398
401
|
self.current_footnote_area.height = 0
|
weasyprint/layout/absolute.py
CHANGED
|
@@ -67,7 +67,9 @@ def absolute_width(box, context, cb_x, cb_y, cb_width, cb_height):
|
|
|
67
67
|
available_width = cb_width - (
|
|
68
68
|
paddings_borders + box.margin_left + box.margin_right)
|
|
69
69
|
box.width = shrink_to_fit(context, box, available_width)
|
|
70
|
-
if
|
|
70
|
+
if box.is_outside_marker:
|
|
71
|
+
translate_box_width = ltr
|
|
72
|
+
elif not ltr:
|
|
71
73
|
translate_box_width = True
|
|
72
74
|
translate_x = default_translate_x + available_width
|
|
73
75
|
elif box.left != 'auto' and box.right != 'auto' and box.width != 'auto':
|
|
@@ -268,6 +270,7 @@ def absolute_box_layout(context, box, containing_block, fixed_boxes,
|
|
|
268
270
|
context, box, containing_block, fixed_boxes, bottom_space,
|
|
269
271
|
skip_stack, cb_x, cb_y, cb_width, cb_height)
|
|
270
272
|
context.finish_block_formatting_context(new_box)
|
|
273
|
+
|
|
271
274
|
return new_box, resume_at
|
|
272
275
|
|
|
273
276
|
|