weasyprint 64.1__py3-none-any.whl → 65.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 +2 -1
- weasyprint/css/computed_values.py +1 -1
- weasyprint/css/html5_ph.css +65 -178
- weasyprint/css/html5_ua.css +243 -747
- weasyprint/css/html5_ua_form.css +3 -13
- weasyprint/css/validation/descriptors.py +9 -0
- weasyprint/draw/border.py +4 -4
- weasyprint/draw/text.py +12 -8
- weasyprint/formatting_structure/build.py +17 -17
- weasyprint/images.py +2 -2
- weasyprint/layout/__init__.py +17 -0
- weasyprint/layout/absolute.py +1 -1
- weasyprint/layout/block.py +23 -20
- weasyprint/layout/column.py +2 -3
- weasyprint/layout/flex.py +390 -571
- weasyprint/layout/float.py +6 -4
- weasyprint/layout/inline.py +23 -27
- weasyprint/layout/page.py +2 -4
- weasyprint/layout/percent.py +41 -46
- weasyprint/layout/preferred.py +13 -16
- weasyprint/pdf/fonts.py +22 -23
- weasyprint/stacking.py +2 -2
- weasyprint/svg/__init__.py +6 -3
- weasyprint/text/constants.py +5 -0
- weasyprint/text/ffi.py +12 -0
- weasyprint/text/fonts.py +12 -3
- weasyprint/text/line_break.py +8 -7
- {weasyprint-64.1.dist-info → weasyprint-65.0.dist-info}/METADATA +2 -2
- {weasyprint-64.1.dist-info → weasyprint-65.0.dist-info}/RECORD +32 -32
- {weasyprint-64.1.dist-info → weasyprint-65.0.dist-info}/WHEEL +0 -0
- {weasyprint-64.1.dist-info → weasyprint-65.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-64.1.dist-info → weasyprint-65.0.dist-info}/licenses/LICENSE +0 -0
weasyprint/layout/float.py
CHANGED
|
@@ -68,7 +68,7 @@ def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes,
|
|
|
68
68
|
context, box, bottom_space=bottom_space,
|
|
69
69
|
skip_stack=skip_stack, containing_block=containing_block,
|
|
70
70
|
page_is_empty=True, absolute_boxes=absolute_boxes,
|
|
71
|
-
fixed_boxes=fixed_boxes)
|
|
71
|
+
fixed_boxes=fixed_boxes, discard=False)
|
|
72
72
|
elif isinstance(box, boxes.GridContainerBox):
|
|
73
73
|
box, resume_at, _, _, _ = grid_layout(
|
|
74
74
|
context, box, bottom_space=bottom_space,
|
|
@@ -216,9 +216,11 @@ def avoid_collisions(context, box, containing_block, outer=True):
|
|
|
216
216
|
# bound.
|
|
217
217
|
position_x = max_right_bound - box_width
|
|
218
218
|
else:
|
|
219
|
-
# The position of the right border of the replaced box
|
|
220
|
-
# the right bound.
|
|
221
|
-
assert
|
|
219
|
+
# The position of the right border of the replaced box or
|
|
220
|
+
# formatting context is at the right bound.
|
|
221
|
+
assert (
|
|
222
|
+
isinstance(box, boxes.BlockReplacedBox) or
|
|
223
|
+
box.establishes_formatting_context())
|
|
222
224
|
position_x = max_right_bound - box_width
|
|
223
225
|
|
|
224
226
|
available_width = max_right_bound - max_left_bound
|
weasyprint/layout/inline.py
CHANGED
|
@@ -218,7 +218,7 @@ def skip_first_whitespace(box, skip_stack):
|
|
|
218
218
|
return None
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
def remove_last_whitespace(context,
|
|
221
|
+
def remove_last_whitespace(context, line):
|
|
222
222
|
"""Remove in place space characters at the end of a line.
|
|
223
223
|
|
|
224
224
|
This also reduces the width and position of the inline parents of the
|
|
@@ -226,6 +226,7 @@ def remove_last_whitespace(context, box):
|
|
|
226
226
|
|
|
227
227
|
"""
|
|
228
228
|
ancestors = []
|
|
229
|
+
box = line
|
|
229
230
|
while isinstance(box, (boxes.LineBox, boxes.InlineBox)):
|
|
230
231
|
ancestors.append(box)
|
|
231
232
|
if not box.children:
|
|
@@ -244,25 +245,16 @@ def remove_last_whitespace(context, box):
|
|
|
244
245
|
assert resume is None
|
|
245
246
|
space_width = box.width - new_box.width
|
|
246
247
|
box.width = new_box.width
|
|
247
|
-
|
|
248
|
-
# RTL line, the trailing space is at the left of the box. We have to
|
|
249
|
-
# translate the box to align the stripped text with the right edge of
|
|
250
|
-
# the box.
|
|
251
|
-
if new_box.pango_layout.first_line_direction % 2:
|
|
252
|
-
box.position_x -= space_width
|
|
253
|
-
for ancestor in ancestors:
|
|
254
|
-
ancestor.position_x -= space_width
|
|
255
248
|
else:
|
|
256
249
|
space_width = box.width
|
|
257
250
|
box.width = 0
|
|
258
251
|
box.text = ''
|
|
259
252
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
line.translate(dx=-space_width, ignore_floats=True)
|
|
253
|
+
# RTL line, the trailing space is at the left of the box. We have to translate the
|
|
254
|
+
# box to align the stripped text with the right edge of the box.
|
|
255
|
+
if box.pango_layout.first_line_direction % 2:
|
|
256
|
+
for child in line.children:
|
|
257
|
+
child.translate(dx=-space_width, ignore_floats=True)
|
|
266
258
|
|
|
267
259
|
for ancestor in ancestors:
|
|
268
260
|
ancestor.width -= space_width
|
|
@@ -370,9 +362,12 @@ def atomic_box(context, box, position_x, skip_stack, containing_block,
|
|
|
370
362
|
if box.is_table_wrapper:
|
|
371
363
|
containing_size = (containing_block.width, containing_block.height)
|
|
372
364
|
table_wrapper_width(context, box, containing_size)
|
|
365
|
+
width, min_width, max_width = box.width, box.min_width, box.max_width
|
|
373
366
|
box = inline_block_box_layout(
|
|
374
367
|
context, box, position_x, skip_stack, containing_block,
|
|
375
368
|
absolute_boxes, fixed_boxes)
|
|
369
|
+
if box.is_table_wrapper:
|
|
370
|
+
box.width, box.min_width, box.max_width = width, min_width, max_width
|
|
376
371
|
else: # pragma: no cover
|
|
377
372
|
raise TypeError(f'Layout for {type(box).__name__} not handled yet')
|
|
378
373
|
return box
|
|
@@ -513,7 +508,7 @@ def split_inline_level(context, box, position_x, max_x, bottom_space,
|
|
|
513
508
|
setattr(box, f'margin_{side}', 0)
|
|
514
509
|
new_box, resume_at, _, _, _ = flex_layout(
|
|
515
510
|
context, box, -inf, skip_stack, containing_block, False,
|
|
516
|
-
absolute_boxes, fixed_boxes)
|
|
511
|
+
absolute_boxes, fixed_boxes, False)
|
|
517
512
|
preserved_line_break = False
|
|
518
513
|
first_letter = '\u2e80'
|
|
519
514
|
last_letter = '\u2e80'
|
|
@@ -1049,8 +1044,7 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
|
|
|
1049
1044
|
|
|
1050
1045
|
# the child’s `top` is `child.baseline` above (lower y) its baseline.
|
|
1051
1046
|
top = child_baseline_y - child.baseline
|
|
1052
|
-
box_types = (
|
|
1053
|
-
boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
|
|
1047
|
+
box_types = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
|
|
1054
1048
|
if isinstance(child, box_types):
|
|
1055
1049
|
# This also includes table wrappers for inline tables.
|
|
1056
1050
|
child.translate(dy=top - child.position_y)
|
|
@@ -1119,18 +1113,17 @@ def text_align(context, line, available_width, last):
|
|
|
1119
1113
|
def justify_line(context, line, extra_width):
|
|
1120
1114
|
# TODO: We should use a better algorithm here, see
|
|
1121
1115
|
# https://www.w3.org/TR/css-text-3/#justify-algos
|
|
1122
|
-
nb_spaces
|
|
1123
|
-
|
|
1124
|
-
return
|
|
1125
|
-
add_word_spacing(context, line, extra_width / nb_spaces, 0)
|
|
1116
|
+
if (nb_spaces := count_expandable_spaces(line)):
|
|
1117
|
+
add_word_spacing(context, line, extra_width / nb_spaces, 0)
|
|
1126
1118
|
|
|
1127
1119
|
|
|
1128
|
-
def
|
|
1120
|
+
def count_expandable_spaces(box):
|
|
1121
|
+
"""Count expandable spaces (space and nbsp) for justification."""
|
|
1129
1122
|
if isinstance(box, boxes.TextBox):
|
|
1130
1123
|
# TODO: remove trailing spaces correctly
|
|
1131
|
-
return box.text.count(' ')
|
|
1124
|
+
return box.text.count(' ') + box.text.count('\u00a0')
|
|
1132
1125
|
elif isinstance(box, (boxes.LineBox, boxes.InlineBox)):
|
|
1133
|
-
return sum(
|
|
1126
|
+
return sum(count_expandable_spaces(child) for child in box.children)
|
|
1134
1127
|
else:
|
|
1135
1128
|
return 0
|
|
1136
1129
|
|
|
@@ -1139,7 +1132,7 @@ def add_word_spacing(context, box, justification_spacing, x_advance):
|
|
|
1139
1132
|
if isinstance(box, boxes.TextBox):
|
|
1140
1133
|
box.justification_spacing = justification_spacing
|
|
1141
1134
|
box.position_x += x_advance
|
|
1142
|
-
nb_spaces =
|
|
1135
|
+
nb_spaces = count_expandable_spaces(box)
|
|
1143
1136
|
if nb_spaces > 0:
|
|
1144
1137
|
layout = create_layout(
|
|
1145
1138
|
box.text, box.style, context, max_width=None,
|
|
@@ -1152,7 +1145,10 @@ def add_word_spacing(context, box, justification_spacing, x_advance):
|
|
|
1152
1145
|
elif isinstance(box, (boxes.LineBox, boxes.InlineBox)):
|
|
1153
1146
|
box.position_x += x_advance
|
|
1154
1147
|
previous_x_advance = x_advance
|
|
1155
|
-
|
|
1148
|
+
children = box.children
|
|
1149
|
+
if box.style['direction'] == 'rtl':
|
|
1150
|
+
children = children[::-1]
|
|
1151
|
+
for child in children:
|
|
1156
1152
|
if child.is_in_normal_flow():
|
|
1157
1153
|
x_advance = add_word_spacing(
|
|
1158
1154
|
context, child, justification_spacing, x_advance)
|
weasyprint/layout/page.py
CHANGED
|
@@ -605,10 +605,8 @@ def make_page(context, root_box, page_type, resume_at, page_number,
|
|
|
605
605
|
previous_resume_at = resume_at
|
|
606
606
|
root_box = root_box.copy_with_children([])
|
|
607
607
|
|
|
608
|
-
#
|
|
609
|
-
|
|
610
|
-
assert isinstance(root_box, (
|
|
611
|
-
boxes.BlockBox, boxes.FlexContainerBox, boxes.GridContainerBox))
|
|
608
|
+
# https://www.w3.org/TR/css-display-4/#root
|
|
609
|
+
assert isinstance(root_box, boxes.BlockLevelBox)
|
|
612
610
|
context.create_block_formatting_context()
|
|
613
611
|
context.current_page = page_number
|
|
614
612
|
context.current_page_footnotes = []
|
weasyprint/layout/percent.py
CHANGED
|
@@ -21,8 +21,7 @@ def percentage(value, refer_to):
|
|
|
21
21
|
return refer_to * value.value / 100
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def resolve_one_percentage(box, property_name, refer_to
|
|
25
|
-
main_flex_direction=None):
|
|
24
|
+
def resolve_one_percentage(box, property_name, refer_to):
|
|
26
25
|
"""Set a used length value from a computed length value.
|
|
27
26
|
|
|
28
27
|
``refer_to`` is the length for 100%. If ``refer_to`` is not a number, it
|
|
@@ -35,9 +34,7 @@ def resolve_one_percentage(box, property_name, refer_to,
|
|
|
35
34
|
percent = percentage(value, refer_to)
|
|
36
35
|
setattr(box, property_name, percent)
|
|
37
36
|
if property_name in ('min_width', 'min_height') and percent == 'auto':
|
|
38
|
-
|
|
39
|
-
property_name != (f'min_{main_flex_direction}')):
|
|
40
|
-
setattr(box, property_name, 0)
|
|
37
|
+
setattr(box, property_name, 0)
|
|
41
38
|
|
|
42
39
|
|
|
43
40
|
def resolve_position_percentages(box, containing_block):
|
|
@@ -48,7 +45,7 @@ def resolve_position_percentages(box, containing_block):
|
|
|
48
45
|
resolve_one_percentage(box, 'bottom', cb_height)
|
|
49
46
|
|
|
50
47
|
|
|
51
|
-
def resolve_percentages(box, containing_block
|
|
48
|
+
def resolve_percentages(box, containing_block):
|
|
52
49
|
"""Set used values as attributes of the box object."""
|
|
53
50
|
if isinstance(containing_block, boxes.Box):
|
|
54
51
|
# cb is short for containing block
|
|
@@ -69,8 +66,8 @@ def resolve_percentages(box, containing_block, main_flex_direction=None):
|
|
|
69
66
|
resolve_one_percentage(box, 'padding_top', maybe_height)
|
|
70
67
|
resolve_one_percentage(box, 'padding_bottom', maybe_height)
|
|
71
68
|
resolve_one_percentage(box, 'width', cb_width)
|
|
72
|
-
resolve_one_percentage(box, 'min_width', cb_width
|
|
73
|
-
resolve_one_percentage(box, 'max_width', cb_width
|
|
69
|
+
resolve_one_percentage(box, 'min_width', cb_width)
|
|
70
|
+
resolve_one_percentage(box, 'max_width', cb_width)
|
|
74
71
|
|
|
75
72
|
# XXX later: top, bottom, left and right on positioned elements
|
|
76
73
|
|
|
@@ -83,14 +80,12 @@ def resolve_percentages(box, containing_block, main_flex_direction=None):
|
|
|
83
80
|
else:
|
|
84
81
|
assert height.unit == 'px'
|
|
85
82
|
box.height = height.value
|
|
86
|
-
resolve_one_percentage(box, 'min_height', 0
|
|
87
|
-
resolve_one_percentage(box, 'max_height', inf
|
|
83
|
+
resolve_one_percentage(box, 'min_height', 0)
|
|
84
|
+
resolve_one_percentage(box, 'max_height', inf)
|
|
88
85
|
else:
|
|
89
86
|
resolve_one_percentage(box, 'height', cb_height)
|
|
90
|
-
resolve_one_percentage(
|
|
91
|
-
|
|
92
|
-
resolve_one_percentage(
|
|
93
|
-
box, 'max_height', cb_height, main_flex_direction)
|
|
87
|
+
resolve_one_percentage(box, 'min_height', cb_height)
|
|
88
|
+
resolve_one_percentage(box, 'max_height', cb_height)
|
|
94
89
|
|
|
95
90
|
collapse = box.style['border_collapse'] == 'collapse'
|
|
96
91
|
# Used value == computed value
|
|
@@ -102,38 +97,8 @@ def resolve_percentages(box, containing_block, main_flex_direction=None):
|
|
|
102
97
|
setattr(box, prop, box.style[prop])
|
|
103
98
|
|
|
104
99
|
# Shrink *content* widths and heights according to box-sizing
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if box.style['box_sizing'] == 'border-box':
|
|
108
|
-
horizontal_delta = (
|
|
109
|
-
box.padding_left + box.padding_right +
|
|
110
|
-
box.border_left_width + box.border_right_width)
|
|
111
|
-
vertical_delta = (
|
|
112
|
-
box.padding_top + box.padding_bottom +
|
|
113
|
-
box.border_top_width + box.border_bottom_width)
|
|
114
|
-
elif box.style['box_sizing'] == 'padding-box':
|
|
115
|
-
horizontal_delta = box.padding_left + box.padding_right
|
|
116
|
-
vertical_delta = box.padding_top + box.padding_bottom
|
|
117
|
-
else:
|
|
118
|
-
assert box.style['box_sizing'] == 'content-box'
|
|
119
|
-
horizontal_delta = 0
|
|
120
|
-
vertical_delta = 0
|
|
121
|
-
|
|
122
|
-
# Keep at least min_* >= 0 to prevent funny output in case box.width or
|
|
123
|
-
# box.height become negative.
|
|
124
|
-
# Restricting max_* seems reasonable, too.
|
|
125
|
-
if horizontal_delta > 0:
|
|
126
|
-
if box.width != 'auto':
|
|
127
|
-
box.width = max(0, box.width - horizontal_delta)
|
|
128
|
-
box.max_width = max(0, box.max_width - horizontal_delta)
|
|
129
|
-
if box.min_width != 'auto':
|
|
130
|
-
box.min_width = max(0, box.min_width - horizontal_delta)
|
|
131
|
-
if vertical_delta > 0:
|
|
132
|
-
if box.height != 'auto':
|
|
133
|
-
box.height = max(0, box.height - vertical_delta)
|
|
134
|
-
box.max_height = max(0, box.max_height - vertical_delta)
|
|
135
|
-
if box.min_height != 'auto':
|
|
136
|
-
box.min_height = max(0, box.min_height - vertical_delta)
|
|
100
|
+
adjust_box_sizing(box, 'width')
|
|
101
|
+
adjust_box_sizing(box, 'height')
|
|
137
102
|
|
|
138
103
|
|
|
139
104
|
def resolve_radii_percentages(box):
|
|
@@ -154,3 +119,33 @@ def resolve_radii_percentages(box):
|
|
|
154
119
|
rx = percentage(rx, box.border_width())
|
|
155
120
|
ry = percentage(ry, box.border_height())
|
|
156
121
|
setattr(box, property_name, (rx, ry))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def adjust_box_sizing(box, axis):
|
|
125
|
+
if box.style['box_sizing'] == 'border-box':
|
|
126
|
+
if axis == 'width':
|
|
127
|
+
delta = (
|
|
128
|
+
box.padding_left + box.padding_right +
|
|
129
|
+
box.border_left_width + box.border_right_width)
|
|
130
|
+
else:
|
|
131
|
+
delta = (
|
|
132
|
+
box.padding_top + box.padding_bottom +
|
|
133
|
+
box.border_top_width + box.border_bottom_width)
|
|
134
|
+
elif box.style['box_sizing'] == 'padding-box':
|
|
135
|
+
if axis == 'width':
|
|
136
|
+
delta = box.padding_left + box.padding_right
|
|
137
|
+
else:
|
|
138
|
+
delta = box.padding_top + box.padding_bottom
|
|
139
|
+
else:
|
|
140
|
+
assert box.style['box_sizing'] == 'content-box'
|
|
141
|
+
delta = 0
|
|
142
|
+
|
|
143
|
+
# Keep at least min_* >= 0 to prevent funny output in case box.width or
|
|
144
|
+
# box.height become negative.
|
|
145
|
+
# Restricting max_* seems reasonable, too.
|
|
146
|
+
if delta > 0:
|
|
147
|
+
if getattr(box, axis) != 'auto':
|
|
148
|
+
setattr(box, axis, max(0, getattr(box, axis) - delta))
|
|
149
|
+
setattr(box, f'max_{axis}', max(0, getattr(box, f'max_{axis}') - delta))
|
|
150
|
+
if getattr(box, f'min_{axis}') != 'auto':
|
|
151
|
+
setattr(box, f'min_{axis}', max(0, getattr(box, f'min_{axis}') - delta))
|
weasyprint/layout/preferred.py
CHANGED
|
@@ -43,14 +43,12 @@ def min_content_width(context, box, outer=True):
|
|
|
43
43
|
return table_and_columns_preferred_widths(context, box, outer)[0]
|
|
44
44
|
elif isinstance(box, boxes.TableCellBox):
|
|
45
45
|
return table_cell_min_content_width(context, box, outer)
|
|
46
|
-
elif isinstance(box, (
|
|
47
|
-
boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
|
|
46
|
+
elif isinstance(box, (boxes.BlockContainerBox, boxes.TableColumnBox)):
|
|
48
47
|
return block_min_content_width(context, box, outer)
|
|
49
48
|
elif isinstance(box, boxes.TableColumnGroupBox):
|
|
50
49
|
return column_group_content_width(context, box)
|
|
51
50
|
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
|
52
|
-
return inline_min_content_width(
|
|
53
|
-
context, box, outer, is_line_start=True)
|
|
51
|
+
return inline_min_content_width(context, box, outer, is_line_start=True)
|
|
54
52
|
elif isinstance(box, boxes.ReplacedBox):
|
|
55
53
|
return replaced_min_content_width(box, outer)
|
|
56
54
|
elif isinstance(box, boxes.FlexContainerBox):
|
|
@@ -59,8 +57,7 @@ def min_content_width(context, box, outer=True):
|
|
|
59
57
|
# TODO: Get real grid size.
|
|
60
58
|
return block_min_content_width(context, box, outer)
|
|
61
59
|
else:
|
|
62
|
-
raise TypeError(
|
|
63
|
-
f'min-content width for {type(box).__name__} not handled yet')
|
|
60
|
+
raise TypeError(f'min-content width for {type(box).__name__} not handled yet')
|
|
64
61
|
|
|
65
62
|
|
|
66
63
|
def max_content_width(context, box, outer=True):
|
|
@@ -73,14 +70,12 @@ def max_content_width(context, box, outer=True):
|
|
|
73
70
|
return table_and_columns_preferred_widths(context, box, outer)[1]
|
|
74
71
|
elif isinstance(box, boxes.TableCellBox):
|
|
75
72
|
return table_cell_min_max_content_width(context, box, outer)[1]
|
|
76
|
-
elif isinstance(box, (
|
|
77
|
-
boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
|
|
73
|
+
elif isinstance(box, (boxes.BlockContainerBox, boxes.TableColumnBox)):
|
|
78
74
|
return block_max_content_width(context, box, outer)
|
|
79
75
|
elif isinstance(box, boxes.TableColumnGroupBox):
|
|
80
76
|
return column_group_content_width(context, box)
|
|
81
77
|
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
|
82
|
-
return inline_max_content_width(
|
|
83
|
-
context, box, outer, is_line_start=True)
|
|
78
|
+
return inline_max_content_width(context, box, outer, is_line_start=True)
|
|
84
79
|
elif isinstance(box, boxes.ReplacedBox):
|
|
85
80
|
return replaced_max_content_width(box, outer)
|
|
86
81
|
elif isinstance(box, boxes.FlexContainerBox):
|
|
@@ -89,8 +84,7 @@ def max_content_width(context, box, outer=True):
|
|
|
89
84
|
# TODO: Get real grid size.
|
|
90
85
|
return block_max_content_width(context, box, outer)
|
|
91
86
|
else:
|
|
92
|
-
raise TypeError(
|
|
93
|
-
f'max-content width for {type(box).__name__} not handled yet')
|
|
87
|
+
raise TypeError(f'max-content width for {type(box).__name__} not handled yet')
|
|
94
88
|
|
|
95
89
|
|
|
96
90
|
def _block_content_width(context, box, function, outer):
|
|
@@ -344,7 +338,7 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
|
|
|
344
338
|
# "By default, there is a break opportunity
|
|
345
339
|
# both before and after any inline object."
|
|
346
340
|
if minimum:
|
|
347
|
-
lines = [0,
|
|
341
|
+
lines = [0, min_content_width(context, child), 0]
|
|
348
342
|
else:
|
|
349
343
|
lines = [max_content_width(context, child)]
|
|
350
344
|
# The first text line goes on the current line
|
|
@@ -673,9 +667,12 @@ def replaced_min_content_width(box, outer=True):
|
|
|
673
667
|
intrinsic_width, intrinsic_height, intrinsic_ratio = (
|
|
674
668
|
image.get_intrinsic_size(
|
|
675
669
|
box.style['image_resolution'], box.style['font_size']))
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
670
|
+
if intrinsic_ratio and not intrinsic_width and not intrinsic_height:
|
|
671
|
+
width = 0
|
|
672
|
+
else:
|
|
673
|
+
width, _ = default_image_sizing(
|
|
674
|
+
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
|
|
675
|
+
height, default_width=300, default_height=150)
|
|
679
676
|
elif box.style['width'].unit == '%':
|
|
680
677
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
681
678
|
width = 0
|
weasyprint/pdf/fonts.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Fonts integration in PDF."""
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
import re
|
|
4
5
|
from hashlib import md5
|
|
5
6
|
from logging import WARNING
|
|
6
7
|
from math import ceil
|
|
@@ -34,7 +35,7 @@ class Font:
|
|
|
34
35
|
part.split('=')[0]: float(part.split('=')[1])
|
|
35
36
|
for part in ffi.string(variations).decode().split(',')}
|
|
36
37
|
if weight := self.variations.get('weight'):
|
|
37
|
-
self.weight =
|
|
38
|
+
self.weight = round(weight)
|
|
38
39
|
pango.pango_font_description_set_weight(description, weight)
|
|
39
40
|
else:
|
|
40
41
|
self.weight = pango.pango_font_description_get_weight(description)
|
|
@@ -58,24 +59,18 @@ class Font:
|
|
|
58
59
|
in md5(description_string, usedforsecurity=False).digest()[:6])
|
|
59
60
|
|
|
60
61
|
# Set font name.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fields.pop() # Remove variations
|
|
64
|
-
if fields:
|
|
65
|
-
fields.pop() # Remove font size
|
|
66
|
-
else:
|
|
67
|
-
fields = [b'Unknown']
|
|
68
|
-
self.name = b'/' + self.hash.encode() + b'+' + b'-'.join(fields)
|
|
62
|
+
name = re.split(b' [#@]', description_string)[0]
|
|
63
|
+
self.name = b'/' + self.hash.encode() + b'+' + name.replace(b' ', b'-')
|
|
69
64
|
|
|
70
65
|
# Set ascent and descent.
|
|
71
66
|
if self.font_size:
|
|
72
67
|
pango_metrics = pango.pango_font_get_metrics(pango_font, ffi.NULL)
|
|
73
|
-
self.ascent =
|
|
68
|
+
self.ascent = round(
|
|
74
69
|
pango.pango_font_metrics_get_ascent(pango_metrics) * FROM_UNITS /
|
|
75
|
-
self.font_size * 1000)
|
|
76
|
-
self.descent = -
|
|
70
|
+
self.font_size * 1000)
|
|
71
|
+
self.descent = -round(
|
|
77
72
|
pango.pango_font_metrics_get_descent(pango_metrics) * FROM_UNITS /
|
|
78
|
-
self.font_size * 1000)
|
|
73
|
+
self.font_size * 1000)
|
|
79
74
|
else:
|
|
80
75
|
self.ascent = self.descent = 0
|
|
81
76
|
|
|
@@ -112,7 +107,7 @@ class Font:
|
|
|
112
107
|
self.flags = 2 ** (3 - 1) # Symbolic, custom character set
|
|
113
108
|
if self.style:
|
|
114
109
|
self.flags += 2 ** (7 - 1) # Italic
|
|
115
|
-
if b'Serif' in
|
|
110
|
+
if b'Serif' in name.split(b' '):
|
|
116
111
|
self.flags += 2 ** (2 - 1) # Serif
|
|
117
112
|
|
|
118
113
|
def clean(self, cmap, hinting):
|
|
@@ -342,15 +337,19 @@ def build_fonts_dictionary(pdf, fonts, compress, subset, options):
|
|
|
342
337
|
b'/CMapType 2 def',
|
|
343
338
|
b'1 begincodespacerange',
|
|
344
339
|
b'<0000> <ffff>',
|
|
345
|
-
b'endcodespacerange',
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
to_unicode.stream.append(
|
|
351
|
-
|
|
340
|
+
b'endcodespacerange'], compress=compress)
|
|
341
|
+
cmap_length = len(cmap)
|
|
342
|
+
cmap_items = tuple(cmap.items())
|
|
343
|
+
for i in range(ceil(cmap_length / 100)):
|
|
344
|
+
batch_length = min(100, cmap_length - i * 100)
|
|
345
|
+
to_unicode.stream.append(f'{batch_length} beginbfchar'.encode())
|
|
346
|
+
for glyph, text in cmap_items[i*100:(i+1)*100]:
|
|
347
|
+
unicode_codepoints = ''.join(
|
|
348
|
+
f'{letter.encode("utf-16-be").hex()}' for letter in text)
|
|
349
|
+
to_unicode.stream.append(
|
|
350
|
+
f'<{glyph:04x}> <{unicode_codepoints}>'.encode())
|
|
351
|
+
to_unicode.stream.append(b'endbfchar')
|
|
352
352
|
to_unicode.stream.extend([
|
|
353
|
-
b'endbfchar',
|
|
354
353
|
b'endcmap',
|
|
355
354
|
b'CMapName currentdict /CMap defineresource pop',
|
|
356
355
|
b'end',
|
|
@@ -561,7 +560,7 @@ def _build_vector_font_dictionary(font_dictionary, pdf, font, widths, compress,
|
|
|
561
560
|
})
|
|
562
561
|
if str(pdf_version) <= '1.4': # Cast for bytes and None
|
|
563
562
|
cids = sorted(font.widths)
|
|
564
|
-
padded_width =
|
|
563
|
+
padded_width = ceil((cids[-1] + 1) / 8)
|
|
565
564
|
bits = ['0'] * padded_width * 8
|
|
566
565
|
for cid in cids:
|
|
567
566
|
bits[cid] = '1'
|
weasyprint/stacking.py
CHANGED
|
@@ -82,6 +82,7 @@ def _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):
|
|
|
82
82
|
child_contexts.append(StackingContext.from_box(box, page))
|
|
83
83
|
return
|
|
84
84
|
|
|
85
|
+
stacking_classes = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
|
|
85
86
|
if style['position'] != 'static':
|
|
86
87
|
assert style['z_index'] == 'auto'
|
|
87
88
|
# "Fake" context: sub-contexts will go in this `child_contexts` list.
|
|
@@ -91,8 +92,7 @@ def _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):
|
|
|
91
92
|
child_contexts.insert(index, stacking_context)
|
|
92
93
|
elif box.is_floated():
|
|
93
94
|
floats.append(StackingContext.from_box(box, page, child_contexts))
|
|
94
|
-
elif isinstance(box,
|
|
95
|
-
boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)):
|
|
95
|
+
elif isinstance(box, stacking_classes):
|
|
96
96
|
# Have this fake stacking context be part of the "normal" box tree,
|
|
97
97
|
# because we need its position in the middle of a tree of inline boxes.
|
|
98
98
|
return StackingContext.from_box(box, page, child_contexts)
|
weasyprint/svg/__init__.py
CHANGED
|
@@ -467,11 +467,13 @@ class SVG:
|
|
|
467
467
|
# Draw node children
|
|
468
468
|
if node.display and node.tag not in DEF_TYPES:
|
|
469
469
|
for child in node:
|
|
470
|
-
|
|
470
|
+
new_chunk = text_anchor_shift and (
|
|
471
|
+
child.tag == 'text' or 'x' in child.attrib or 'y' in child.attrib)
|
|
472
|
+
if new_chunk:
|
|
471
473
|
new_stream = self.stream
|
|
472
474
|
self.stream = original_streams[-1]
|
|
473
475
|
self.draw_node(child, font_size, fill_stroke)
|
|
474
|
-
if
|
|
476
|
+
if new_chunk:
|
|
475
477
|
self.stream = new_stream
|
|
476
478
|
visible_text_child = (
|
|
477
479
|
TAGS.get(node.tag) == text and
|
|
@@ -502,7 +504,8 @@ class SVG:
|
|
|
502
504
|
x - font_size, y - font_size,
|
|
503
505
|
x + width + font_size, y + height + font_size)
|
|
504
506
|
x_align = width / 2 if text_anchor == 'middle' else width
|
|
505
|
-
|
|
507
|
+
if node.tag == 'text' or 'x' in node.attrib or 'y' in node.attrib:
|
|
508
|
+
self.stream.transform(e=-x_align)
|
|
506
509
|
self.stream.draw_x_object(group_id)
|
|
507
510
|
self.stream.pop_state()
|
|
508
511
|
|
weasyprint/text/constants.py
CHANGED
|
@@ -49,6 +49,11 @@ PANGO_VARIANT = {
|
|
|
49
49
|
'titling-caps': pango.PANGO_VARIANT_TITLE_CAPS,
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
PANGO_DIRECTION = {
|
|
53
|
+
'ltr': pango.PANGO_DIRECTION_LTR,
|
|
54
|
+
'rtl': pango.PANGO_DIRECTION_RTL,
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
# Language system tags
|
|
53
58
|
# From https://docs.microsoft.com/typography/opentype/spec/languagetags
|
|
54
59
|
LST_TO_ISO = {
|
weasyprint/text/ffi.py
CHANGED
|
@@ -166,6 +166,16 @@ ffi.cdef('''
|
|
|
166
166
|
PANGO_ELLIPSIZE_END
|
|
167
167
|
} PangoEllipsizeMode;
|
|
168
168
|
|
|
169
|
+
typedef enum {
|
|
170
|
+
PANGO_DIRECTION_LTR,
|
|
171
|
+
PANGO_DIRECTION_RTL,
|
|
172
|
+
PANGO_DIRECTION_TTB_LTR,
|
|
173
|
+
PANGO_DIRECTION_TTB_RTL,
|
|
174
|
+
PANGO_DIRECTION_WEAK_LTR,
|
|
175
|
+
PANGO_DIRECTION_WEAK_RTL,
|
|
176
|
+
PANGO_DIRECTION_NEUTRAL
|
|
177
|
+
} PangoDirection;
|
|
178
|
+
|
|
169
179
|
typedef struct GSList {
|
|
170
180
|
gpointer data;
|
|
171
181
|
struct GSList *next;
|
|
@@ -278,6 +288,7 @@ ffi.cdef('''
|
|
|
278
288
|
void pango_layout_set_wrap (PangoLayout *layout, PangoWrapMode wrap);
|
|
279
289
|
void pango_layout_set_single_paragraph_mode (PangoLayout *layout, gboolean setting);
|
|
280
290
|
void pango_layout_set_ellipsize (PangoLayout *layout, PangoEllipsizeMode ellipsize);
|
|
291
|
+
void pango_layout_set_auto_dir (PangoLayout *layout, gboolean auto_dir);
|
|
281
292
|
int pango_layout_get_baseline (PangoLayout *layout);
|
|
282
293
|
void pango_layout_line_get_extents (
|
|
283
294
|
PangoLayoutLine *line, PangoRectangle *ink_rect, PangoRectangle *logical_rect);
|
|
@@ -360,6 +371,7 @@ ffi.cdef('''
|
|
|
360
371
|
PangoLanguage * pango_language_from_string (const char *language);
|
|
361
372
|
PangoLanguage * pango_language_get_default (void);
|
|
362
373
|
void pango_context_set_language (PangoContext *context, PangoLanguage *language);
|
|
374
|
+
void pango_context_set_base_dir (PangoContext *context, PangoDirection direction);
|
|
363
375
|
|
|
364
376
|
void pango_get_log_attrs (
|
|
365
377
|
const char *text, int length, int level, PangoLanguage *language,
|
weasyprint/text/fonts.py
CHANGED
|
@@ -208,14 +208,23 @@ class FontConfiguration:
|
|
|
208
208
|
match = SubElement(root, 'match', target='font')
|
|
209
209
|
test = SubElement(match, 'test', name='file', compare='eq')
|
|
210
210
|
SubElement(test, 'string').text = str(font_path)
|
|
211
|
-
edit = SubElement(match, 'edit', name='fontfeatures', mode=mode)
|
|
212
211
|
descriptors = {
|
|
213
212
|
rules[0][0].replace('-', '_'): rules[0][1] for rules in
|
|
214
213
|
rule_descriptors.get('font_variant', [])}
|
|
215
214
|
settings = rule_descriptors.get('font_feature_settings', 'normal')
|
|
216
215
|
features = font_features(font_feature_settings=settings, **descriptors)
|
|
217
|
-
|
|
218
|
-
SubElement(
|
|
216
|
+
if features:
|
|
217
|
+
edit = SubElement(match, 'edit', name='fontfeatures', mode=mode)
|
|
218
|
+
for key, value in features.items():
|
|
219
|
+
SubElement(edit, 'string').text = f'{key} {value}'
|
|
220
|
+
if unicode_ranges := rule_descriptors.get('unicode_range'):
|
|
221
|
+
edit = SubElement(match, 'edit', name='charset', mode=mode)
|
|
222
|
+
plus = SubElement(edit, 'plus')
|
|
223
|
+
for unicode_range in unicode_ranges:
|
|
224
|
+
charset = SubElement(plus, 'charset')
|
|
225
|
+
range_ = SubElement(charset, 'range')
|
|
226
|
+
for value in (unicode_range.start, unicode_range.end):
|
|
227
|
+
SubElement(range_, 'int').text = f'0x{value:x}'
|
|
219
228
|
header = (
|
|
220
229
|
b'<?xml version="1.0"?>',
|
|
221
230
|
b'<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">')
|
weasyprint/text/line_break.py
CHANGED
|
@@ -5,7 +5,7 @@ from math import inf
|
|
|
5
5
|
|
|
6
6
|
import pyphen
|
|
7
7
|
|
|
8
|
-
from .constants import LST_TO_ISO, PANGO_WRAP_MODE
|
|
8
|
+
from .constants import LST_TO_ISO, PANGO_DIRECTION, PANGO_WRAP_MODE
|
|
9
9
|
from .ffi import FROM_UNITS, TO_UNITS, ffi, gobject, pango, pangoft2, unicode_to_char_p
|
|
10
10
|
from .fonts import font_features, get_font_description
|
|
11
11
|
|
|
@@ -76,6 +76,8 @@ class Layout:
|
|
|
76
76
|
pango.pango_font_map_create_context(font_map),
|
|
77
77
|
gobject.g_object_unref)
|
|
78
78
|
pango.pango_context_set_round_glyph_positions(pango_context, False)
|
|
79
|
+
pango.pango_context_set_base_dir(
|
|
80
|
+
pango_context, PANGO_DIRECTION[style['direction']])
|
|
79
81
|
|
|
80
82
|
if style['font_language_override'] != 'normal':
|
|
81
83
|
lang_p, lang = unicode_to_char_p(LST_TO_ISO.get(
|
|
@@ -96,6 +98,7 @@ class Layout:
|
|
|
96
98
|
self.layout = ffi.gc(
|
|
97
99
|
pango.pango_layout_new(pango_context),
|
|
98
100
|
gobject.g_object_unref)
|
|
101
|
+
pango.pango_layout_set_auto_dir(self.layout, False)
|
|
99
102
|
pango.pango_layout_set_font_description(self.layout, font_description)
|
|
100
103
|
|
|
101
104
|
text_decoration = style['text_decoration_line']
|
|
@@ -187,13 +190,11 @@ class Layout:
|
|
|
187
190
|
pango.pango_layout_set_text(self.layout, text, -1)
|
|
188
191
|
|
|
189
192
|
space_spacing = int(word_spacing * TO_UNITS + letter_spacing)
|
|
190
|
-
position = bytestring.find(b' ')
|
|
191
193
|
# Pango gives only half of word-spacing on boundaries
|
|
192
194
|
boundary_positions = (0, len(bytestring) - 1)
|
|
193
|
-
|
|
194
|
-
factor = 1 + (
|
|
195
|
-
add_attr(
|
|
196
|
-
position = bytestring.find(b' ', position + 1)
|
|
195
|
+
for match in re.finditer(' |\u00a0'.encode(), bytestring):
|
|
196
|
+
factor = 1 + (match.start() in boundary_positions)
|
|
197
|
+
add_attr(match.start(), match.end(), factor * space_spacing)
|
|
197
198
|
|
|
198
199
|
if word_breaking:
|
|
199
200
|
attr = pango.pango_attr_insert_hyphens_new(False)
|
|
@@ -213,7 +214,7 @@ class Layout:
|
|
|
213
214
|
layout.set_text(' ' * self.style['tab_size'])
|
|
214
215
|
line, _ = layout.get_first_line()
|
|
215
216
|
width, _ = line_size(line, self.style)
|
|
216
|
-
width =
|
|
217
|
+
width = round(width)
|
|
217
218
|
else:
|
|
218
219
|
width = int(self.style['tab_size'].value)
|
|
219
220
|
# 0 is not handled correctly by Pango
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weasyprint
|
|
3
|
-
Version:
|
|
3
|
+
Version: 65.0
|
|
4
4
|
Summary: The Awesome Document Factory
|
|
5
5
|
Keywords: html,css,pdf,converter
|
|
6
6
|
Author-email: Simon Sapin <simon.sapin@exyr.org>
|
|
@@ -30,7 +30,7 @@ Requires-Dist: pydyf >=0.11.0
|
|
|
30
30
|
Requires-Dist: cffi >=0.6
|
|
31
31
|
Requires-Dist: tinyhtml5 >=2.0.0b1
|
|
32
32
|
Requires-Dist: tinycss2 >=1.4.0
|
|
33
|
-
Requires-Dist: cssselect2 >=0.
|
|
33
|
+
Requires-Dist: cssselect2 >=0.8.0
|
|
34
34
|
Requires-Dist: Pyphen >=0.9.1
|
|
35
35
|
Requires-Dist: Pillow >=9.1.0
|
|
36
36
|
Requires-Dist: fonttools[woff] >=4.0.0
|