weasyprint 67.0__py3-none-any.whl → 68.1__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 +35 -103
- weasyprint/__main__.py +107 -80
- weasyprint/css/__init__.py +7 -14
- weasyprint/css/functions.py +5 -0
- weasyprint/css/html5_ua.css +1 -1
- weasyprint/css/tokens.py +4 -1
- weasyprint/css/validation/properties.py +4 -4
- weasyprint/document.py +4 -64
- weasyprint/draw/text.py +12 -10
- weasyprint/formatting_structure/boxes.py +4 -1
- weasyprint/formatting_structure/build.py +111 -37
- weasyprint/images.py +27 -32
- weasyprint/layout/__init__.py +2 -1
- weasyprint/layout/block.py +22 -16
- weasyprint/layout/grid.py +25 -14
- weasyprint/layout/page.py +4 -4
- weasyprint/layout/preferred.py +63 -24
- weasyprint/pdf/__init__.py +12 -1
- weasyprint/pdf/anchors.py +10 -16
- weasyprint/pdf/fonts.py +12 -3
- weasyprint/pdf/metadata.py +153 -98
- weasyprint/pdf/pdfa.py +1 -3
- weasyprint/pdf/pdfua.py +1 -3
- weasyprint/pdf/pdfx.py +1 -3
- weasyprint/svg/__init__.py +52 -32
- weasyprint/svg/css.py +21 -4
- weasyprint/svg/defs.py +5 -9
- weasyprint/svg/text.py +4 -3
- weasyprint/text/fonts.py +2 -3
- weasyprint/text/line_break.py +4 -5
- weasyprint/urls.py +290 -97
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/METADATA +2 -1
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/RECORD +36 -36
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/WHEEL +0 -0
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/entry_points.txt +0 -0
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/licenses/LICENSE +0 -0
weasyprint/layout/block.py
CHANGED
|
@@ -301,7 +301,7 @@ def _out_of_flow_layout(context, box, index, child, new_children,
|
|
|
301
301
|
return stop, resume_at, new_child, out_of_flow_resume_at
|
|
302
302
|
|
|
303
303
|
|
|
304
|
-
def _break_line(context, box, line, new_children,
|
|
304
|
+
def _break_line(context, box, line, new_children, needed, page_is_empty, index,
|
|
305
305
|
skip_stack, resume_at, absolute_boxes, fixed_boxes):
|
|
306
306
|
"""Break line where allowed by orphans and widows.
|
|
307
307
|
|
|
@@ -316,7 +316,6 @@ def _break_line(context, box, line, new_children, next_lines, page_is_empty, ind
|
|
|
316
316
|
return True, False, resume_at
|
|
317
317
|
# How many lines we need on the next page to satisfy widows
|
|
318
318
|
# -1 for the current line.
|
|
319
|
-
needed = max(box.style['widows'] - 1 - next_lines, 0)
|
|
320
319
|
if needed > over_orphans and not page_is_empty:
|
|
321
320
|
# Total number of lines < orphans + widows
|
|
322
321
|
remove_placeholders(context, line.children, absolute_boxes, fixed_boxes)
|
|
@@ -377,15 +376,21 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
|
|
377
376
|
# If we couldn’t break the line before but can break now, first try to
|
|
378
377
|
# report footnotes and see if we don’t overflow.
|
|
379
378
|
could_break_before = can_break_now = True
|
|
380
|
-
|
|
379
|
+
needed = box.style['widows'] - 1
|
|
380
|
+
for _ in lines_iterator:
|
|
381
|
+
needed -= 1
|
|
382
|
+
# Don’t iterate over all lines as it can be long.
|
|
383
|
+
if needed == -1:
|
|
384
|
+
break
|
|
381
385
|
if len(new_children) + 1 < box.style['orphans']:
|
|
382
386
|
can_break_now = False
|
|
383
|
-
elif
|
|
387
|
+
elif needed >= 0:
|
|
384
388
|
can_break_now = False
|
|
385
389
|
if len(new_children) < box.style['orphans']:
|
|
386
390
|
could_break_before = False
|
|
387
|
-
elif
|
|
391
|
+
elif needed > 0:
|
|
388
392
|
could_break_before = False
|
|
393
|
+
needed = max(0, needed)
|
|
389
394
|
report = not context.in_column and can_break_now and not could_break_before
|
|
390
395
|
reported_footnotes = 0
|
|
391
396
|
while report and context.current_page_footnotes:
|
|
@@ -397,9 +402,8 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
|
|
397
402
|
break
|
|
398
403
|
else:
|
|
399
404
|
abort, stop, resume_at = _break_line(
|
|
400
|
-
context, box, line, new_children,
|
|
401
|
-
|
|
402
|
-
fixed_boxes)
|
|
405
|
+
context, box, line, new_children, needed, page_is_empty, index,
|
|
406
|
+
skip_stack, resume_at, absolute_boxes, fixed_boxes)
|
|
403
407
|
|
|
404
408
|
# Revert reported footnotes, as they’ve been reported starting from the last
|
|
405
409
|
# one.
|
|
@@ -414,8 +418,7 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
|
|
414
418
|
# "When an unforced page break occurs here, both the adjoining
|
|
415
419
|
# ‘margin-top’ and ‘margin-bottom’ are set to zero."
|
|
416
420
|
# See issue #115.
|
|
417
|
-
elif page_is_empty and context.overflows_page(
|
|
418
|
-
bottom_space, new_position_y):
|
|
421
|
+
elif page_is_empty and context.overflows_page(bottom_space, new_position_y):
|
|
419
422
|
# Remove the top border when a page is empty and the box is
|
|
420
423
|
# too high to be drawn in one page
|
|
421
424
|
new_position_y -= box.margin_top
|
|
@@ -433,8 +436,7 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
|
|
433
436
|
overflow = (
|
|
434
437
|
overflow or
|
|
435
438
|
context.reported_footnotes or
|
|
436
|
-
context.overflows_page(
|
|
437
|
-
bottom_space, new_position_y + offset_y))
|
|
439
|
+
context.overflows_page(bottom_space, new_position_y + offset_y))
|
|
438
440
|
if overflow:
|
|
439
441
|
context.report_footnote(footnote)
|
|
440
442
|
# If we've put other content on this page, then we may want
|
|
@@ -443,11 +445,15 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
|
|
443
445
|
# even try.
|
|
444
446
|
if new_children or not page_is_empty:
|
|
445
447
|
if footnote.style['footnote_policy'] == 'line':
|
|
446
|
-
|
|
448
|
+
if needed := box.style['widows'] - 1:
|
|
449
|
+
for _ in lines_iterator:
|
|
450
|
+
needed -= 1
|
|
451
|
+
# Don’t iterate over all lines as it can be long.
|
|
452
|
+
if needed == 0:
|
|
453
|
+
break
|
|
447
454
|
abort, stop, resume_at = _break_line(
|
|
448
|
-
context, box, line, new_children,
|
|
449
|
-
|
|
450
|
-
skip_stack, resume_at, absolute_boxes,
|
|
455
|
+
context, box, line, new_children, needed, page_is_empty,
|
|
456
|
+
index, skip_stack, resume_at, absolute_boxes,
|
|
451
457
|
fixed_boxes)
|
|
452
458
|
break_linebox = True
|
|
453
459
|
break
|
weasyprint/layout/grid.py
CHANGED
|
@@ -1123,6 +1123,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1123
1123
|
# Find resume_at row.
|
|
1124
1124
|
this_page_children = []
|
|
1125
1125
|
resume_row = None
|
|
1126
|
+
extra_skip_height = 0
|
|
1126
1127
|
if skip_stack:
|
|
1127
1128
|
from .block import block_level_layout
|
|
1128
1129
|
first_skip_row = min(skip_stack)
|
|
@@ -1130,7 +1131,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1130
1131
|
skip_height = (
|
|
1131
1132
|
sum(size for size, _ in rows_sizes[:last_skip_row]) +
|
|
1132
1133
|
(len(rows_sizes[:last_skip_row]) - 1) * row_gap)
|
|
1133
|
-
extra_skip_height = 0
|
|
1134
1134
|
for child, (x, y, width, height) in children_positions.items():
|
|
1135
1135
|
if (advancement := box.advancements.get((x, y))) is None:
|
|
1136
1136
|
continue
|
|
@@ -1271,13 +1271,16 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1271
1271
|
if child.margin_left == 'auto':
|
|
1272
1272
|
child.margin_left = 0
|
|
1273
1273
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1274
|
+
child_border_width = width - (child.margin_left + child.margin_right)
|
|
1275
|
+
child_content_width = child_border_width - (
|
|
1276
|
+
child.border_left_width + child.padding_left +
|
|
1277
|
+
child.border_right_width + child.padding_right)
|
|
1278
|
+
child_border_height = height - child.margin_bottom
|
|
1279
|
+
child_content_height = child_border_height - (
|
|
1280
|
+
child.border_bottom_width + child.padding_bottom)
|
|
1279
1281
|
if not child_skip_stack or child.style['box_decoration_break'] == 'clone':
|
|
1280
|
-
|
|
1282
|
+
child_border_height -= child.margin_top
|
|
1283
|
+
child_content_height -= (
|
|
1281
1284
|
child.margin_top + child.border_top_width + child.padding_top)
|
|
1282
1285
|
|
|
1283
1286
|
justify_self = set(child.style['justify_self'])
|
|
@@ -1286,6 +1289,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1286
1289
|
if justify_self & {'normal', 'stretch'}:
|
|
1287
1290
|
if child.style['width'] == 'auto':
|
|
1288
1291
|
child.style = child.style.copy()
|
|
1292
|
+
child_width = (
|
|
1293
|
+
child_content_width if child.style['box_sizing'] == 'content-box'
|
|
1294
|
+
else child_border_width)
|
|
1289
1295
|
child.style['width'] = Dimension(child_width, 'px')
|
|
1290
1296
|
align_self = set(child.style['align_self'])
|
|
1291
1297
|
if align_self & {'auto'}:
|
|
@@ -1293,6 +1299,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1293
1299
|
if align_self & {'normal', 'stretch'}:
|
|
1294
1300
|
if child.style['height'] == 'auto':
|
|
1295
1301
|
child.style = child.style.copy()
|
|
1302
|
+
child_height = (
|
|
1303
|
+
child_content_height if child.style['box_sizing'] == 'content-box'
|
|
1304
|
+
else child_border_height)
|
|
1296
1305
|
child.style['height'] = Dimension(child_height, 'px')
|
|
1297
1306
|
|
|
1298
1307
|
# TODO: Find a better solution for the layout.
|
|
@@ -1338,11 +1347,11 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1338
1347
|
continue
|
|
1339
1348
|
|
|
1340
1349
|
if justify_self & {'normal', 'stretch'}:
|
|
1341
|
-
new_child.width = max(
|
|
1350
|
+
new_child.width = max(child_content_width, new_child.width)
|
|
1342
1351
|
else:
|
|
1343
1352
|
if new_child.style['width'] == 'auto':
|
|
1344
1353
|
new_child.width = max_content_width(context, new_child, outer=False)
|
|
1345
|
-
diff =
|
|
1354
|
+
diff = child_content_width - new_child.width
|
|
1346
1355
|
if justify_self & {'center'}:
|
|
1347
1356
|
new_child.translate(diff / 2, 0)
|
|
1348
1357
|
elif justify_self & {'right', 'end', 'flex-end', 'self-end'}:
|
|
@@ -1350,9 +1359,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1350
1359
|
|
|
1351
1360
|
# TODO: Apply auto margins.
|
|
1352
1361
|
if align_self & {'normal', 'stretch'}:
|
|
1353
|
-
new_child.height = max(
|
|
1362
|
+
new_child.height = max(child_content_height, new_child.height)
|
|
1354
1363
|
else:
|
|
1355
|
-
diff =
|
|
1364
|
+
diff = child_content_height - new_child.height
|
|
1356
1365
|
if align_self & {'center'}:
|
|
1357
1366
|
new_child.translate(0, diff / 2)
|
|
1358
1367
|
elif align_self & {'end', 'flex-end', 'self-end'}:
|
|
@@ -1368,8 +1377,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1368
1377
|
context.finish_block_formatting_context(box)
|
|
1369
1378
|
return None, None, {'break': 'any', 'page': None}, [], False
|
|
1370
1379
|
|
|
1371
|
-
old_advancements = box.advancements
|
|
1372
|
-
advancements = box.advancements
|
|
1380
|
+
old_advancements = box.advancements.copy()
|
|
1381
|
+
advancements = box.advancements
|
|
1382
|
+
advancements.clear()
|
|
1373
1383
|
box = box.copy_with_children(new_children)
|
|
1374
1384
|
if isinstance(box, boxes.InlineGridBox):
|
|
1375
1385
|
# TODO: Synthetize a real baseline value.
|
|
@@ -1420,7 +1430,8 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
|
|
|
1420
1430
|
else:
|
|
1421
1431
|
# Child fully drawn, save the extra height added to reach the bottom of
|
|
1422
1432
|
# the page to substract it from the advancements.
|
|
1423
|
-
extra_advancement = max(
|
|
1433
|
+
extra_advancement = max(
|
|
1434
|
+
extra_advancement, child.height - child_content_height)
|
|
1424
1435
|
|
|
1425
1436
|
# Substract the extra height added to reach the bottom of the page from all the
|
|
1426
1437
|
# advancements.
|
weasyprint/layout/page.py
CHANGED
|
@@ -389,7 +389,7 @@ def make_margin_boxes(context, page, state):
|
|
|
389
389
|
if box.is_generated:
|
|
390
390
|
# @margins mustn't manipulate page-context counters
|
|
391
391
|
margin_state = copy.deepcopy(state)
|
|
392
|
-
quote_depth, counter_values, counter_scopes = margin_state
|
|
392
|
+
quote_depth, counter_values, counter_scopes, _page_groups = margin_state
|
|
393
393
|
# TODO: check this, probably useless
|
|
394
394
|
counter_scopes.append(set())
|
|
395
395
|
build.update_counters(margin_state, box.style)
|
|
@@ -901,7 +901,7 @@ def _update_page_groups(page_groups, resume_at, next_page, root_box, blank):
|
|
|
901
901
|
return next_page['page']
|
|
902
902
|
|
|
903
903
|
|
|
904
|
-
def remake_page(index,
|
|
904
|
+
def remake_page(index, context, root_box, html):
|
|
905
905
|
"""Return one laid out page without margin boxes.
|
|
906
906
|
|
|
907
907
|
Start with the initial values from ``context.page_maker[index]``.
|
|
@@ -932,6 +932,7 @@ def remake_page(index, page_groups, context, root_box, html):
|
|
|
932
932
|
(next_page_side == 'right' and not right_page) or
|
|
933
933
|
(context.reported_footnotes and resume_at is None))
|
|
934
934
|
side = 'right' if right_page else 'left'
|
|
935
|
+
page_groups = page_state[3]
|
|
935
936
|
name = _update_page_groups(page_groups, resume_at, next_page, root_box, blank)
|
|
936
937
|
groups = tuple((name, index) for name, index, _ in page_groups)
|
|
937
938
|
page_type = PageType(side, blank, name, index, groups)
|
|
@@ -990,7 +991,6 @@ def make_all_pages(context, root_box, html, pages):
|
|
|
990
991
|
"""
|
|
991
992
|
i = 0
|
|
992
993
|
reported_footnotes = None
|
|
993
|
-
page_groups = []
|
|
994
994
|
while True:
|
|
995
995
|
remake_state = context.page_maker[i][-1]
|
|
996
996
|
if (len(pages) == 0 or
|
|
@@ -1002,7 +1002,7 @@ def make_all_pages(context, root_box, html, pages):
|
|
|
1002
1002
|
remake_state['pages_wanted'] = False
|
|
1003
1003
|
remake_state['anchors'] = []
|
|
1004
1004
|
remake_state['content_lookups'] = []
|
|
1005
|
-
page, resume_at = remake_page(i,
|
|
1005
|
+
page, resume_at = remake_page(i, context, root_box, html)
|
|
1006
1006
|
reported_footnotes = context.reported_footnotes
|
|
1007
1007
|
yield page
|
|
1008
1008
|
else:
|
weasyprint/layout/preferred.py
CHANGED
|
@@ -101,9 +101,32 @@ def _block_content_width(context, box, function, outer):
|
|
|
101
101
|
function(context, child, outer=True) for child in box.children
|
|
102
102
|
if not child.is_absolutely_positioned()]
|
|
103
103
|
width = max(children_widths) if children_widths else 0
|
|
104
|
+
elif box.style['box_sizing'] == 'content-box':
|
|
105
|
+
width = width.value
|
|
104
106
|
else:
|
|
105
|
-
assert width.unit.lower() == 'px'
|
|
106
107
|
width = width.value
|
|
108
|
+
percentages = 0
|
|
109
|
+
|
|
110
|
+
for value in ('padding_left', 'padding_right'):
|
|
111
|
+
style_value = box.style[value]
|
|
112
|
+
if style_value != 'auto' and not check_math(style_value):
|
|
113
|
+
if style_value.unit.lower() == 'px':
|
|
114
|
+
width -= style_value.value
|
|
115
|
+
else:
|
|
116
|
+
assert style_value.unit == '%'
|
|
117
|
+
percentages += style_value.value
|
|
118
|
+
|
|
119
|
+
# Same as margin_width().
|
|
120
|
+
collapse = box.style['border_collapse'] == 'collapse'
|
|
121
|
+
if collapse and hasattr(box, 'border_left_width'):
|
|
122
|
+
width -= box.border_left_width
|
|
123
|
+
else:
|
|
124
|
+
width -= box.style['border_left_width']
|
|
125
|
+
if collapse and hasattr(box, 'border_right_width'):
|
|
126
|
+
width -= box.border_right_width
|
|
127
|
+
else:
|
|
128
|
+
width -= box.style['border_right_width']
|
|
129
|
+
width = (100 - min(100, percentages)) * max(0, width) / 100
|
|
107
130
|
|
|
108
131
|
return adjust(box, outer, width)
|
|
109
132
|
|
|
@@ -152,7 +175,7 @@ def margin_width(box, width, left=True, right=True):
|
|
|
152
175
|
(['margin_right', 'padding_right'] if right else [])
|
|
153
176
|
):
|
|
154
177
|
style_value = box.style[value]
|
|
155
|
-
if style_value != 'auto':
|
|
178
|
+
if style_value != 'auto' and not check_math(style_value):
|
|
156
179
|
if style_value.unit.lower() == 'px':
|
|
157
180
|
width += style_value.value
|
|
158
181
|
else:
|
|
@@ -240,7 +263,7 @@ def inline_max_content_width(context, box, outer=True, is_line_start=False):
|
|
|
240
263
|
def column_group_content_width(context, box):
|
|
241
264
|
"""Return the *-content width for a ``TableColumnGroupBox``."""
|
|
242
265
|
width = box.style['width']
|
|
243
|
-
if width == 'auto' or width.unit == '%':
|
|
266
|
+
if width == 'auto' or check_math(width) or width.unit == '%':
|
|
244
267
|
width = 0
|
|
245
268
|
else:
|
|
246
269
|
assert width.unit.lower() == 'px'
|
|
@@ -362,7 +385,17 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
|
|
|
362
385
|
# "By default, there is a break opportunity
|
|
363
386
|
# both before and after any inline object."
|
|
364
387
|
if minimum:
|
|
365
|
-
|
|
388
|
+
# "For soft wrap opportunities defined by the boundary between two
|
|
389
|
+
# characters or atomic inlines, the white-space property on the nearest
|
|
390
|
+
# common ancestor of the two characters controls breaking; which
|
|
391
|
+
# elements’ line-break, word-break, and overflow-wrap properties control
|
|
392
|
+
# the determination of soft wrap opportunities at such boundaries is
|
|
393
|
+
# undefined in this level." We choose to always follow the parent’s
|
|
394
|
+
# value here, other parts of the line-breaking algorithm do the same.
|
|
395
|
+
if box.style['white_space'] in ('normal', 'pre-wrap', 'pre-line'):
|
|
396
|
+
lines = [None, min_content_width(context, child), None]
|
|
397
|
+
else:
|
|
398
|
+
lines = [min_content_width(context, child)]
|
|
366
399
|
else:
|
|
367
400
|
lines = [max_content_width(context, child)]
|
|
368
401
|
# The first text line goes on the current line.
|
|
@@ -564,21 +597,22 @@ def table_and_columns_preferred_widths(context, box, outer=True):
|
|
|
564
597
|
# Define constrainedness
|
|
565
598
|
constrainedness = [False for i in range(grid_width)]
|
|
566
599
|
for i in range(grid_width):
|
|
567
|
-
if
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
continue
|
|
571
|
-
if (columns[i] and columns[i].style['width'] != 'auto' and
|
|
572
|
-
columns[i].style['width'].unit != '%'):
|
|
573
|
-
constrainedness[i] = True
|
|
574
|
-
continue
|
|
575
|
-
for cell in zipped_grid[i]:
|
|
576
|
-
if (cell and cell.colspan == 1 and
|
|
577
|
-
cell.style['width'] != 'auto' and
|
|
578
|
-
not check_math(cell.style['width']) and
|
|
579
|
-
cell.style['width'].unit != '%'):
|
|
600
|
+
if column_groups[i]:
|
|
601
|
+
width = column_groups[i].style['width']
|
|
602
|
+
if width != 'auto' and not check_math(width) and width.unit != '%':
|
|
580
603
|
constrainedness[i] = True
|
|
581
|
-
|
|
604
|
+
continue
|
|
605
|
+
if columns[i]:
|
|
606
|
+
width = columns[i].style['width']
|
|
607
|
+
if width != 'auto' and not check_math(width) and width.unit != '%':
|
|
608
|
+
constrainedness[i] = True
|
|
609
|
+
continue
|
|
610
|
+
for cell in zipped_grid[i]:
|
|
611
|
+
if cell and cell.colspan == 1:
|
|
612
|
+
width = cell.style['width']
|
|
613
|
+
if width != 'auto' and not check_math(width) and width.unit != '%':
|
|
614
|
+
constrainedness[i] = True
|
|
615
|
+
break
|
|
582
616
|
|
|
583
617
|
intrinsic_percentages = [
|
|
584
618
|
min(percentage, 100 - sum(intrinsic_percentages[:i]))
|
|
@@ -646,7 +680,8 @@ def table_and_columns_preferred_widths(context, box, outer=True):
|
|
|
646
680
|
sum(max_content_widths), large_percentage_contribution,
|
|
647
681
|
*small_percentage_contributions]))
|
|
648
682
|
|
|
649
|
-
|
|
683
|
+
width = table.style['width']
|
|
684
|
+
if width != 'auto' and not check_math(width) and width.unit.lower() == 'px':
|
|
650
685
|
# "percentages on the following properties are treated instead as
|
|
651
686
|
# though they were the following: width: auto"
|
|
652
687
|
# https://dbaron.org/css/intrinsic/#outer-intrinsic
|
|
@@ -681,12 +716,16 @@ def replaced_min_content_width(box, outer=True):
|
|
|
681
716
|
width = box.style['width']
|
|
682
717
|
if width == 'auto':
|
|
683
718
|
height = box.style['height']
|
|
684
|
-
if height == 'auto' or height.unit == '%':
|
|
719
|
+
if height == 'auto' or check_math(height) or height.unit == '%':
|
|
685
720
|
height = 'auto'
|
|
686
721
|
else:
|
|
687
722
|
assert height.unit.lower() == 'px'
|
|
688
723
|
height = height.value
|
|
689
|
-
|
|
724
|
+
unknown_max_width = (
|
|
725
|
+
box.style['max_width'] != 'auto' and
|
|
726
|
+
not check_math(box.style['max_width']) and
|
|
727
|
+
box.style['max_width'].unit == '%')
|
|
728
|
+
if unknown_max_width:
|
|
690
729
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
691
730
|
width = 0
|
|
692
731
|
else:
|
|
@@ -697,7 +736,7 @@ def replaced_min_content_width(box, outer=True):
|
|
|
697
736
|
width, _ = default_image_sizing(
|
|
698
737
|
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
|
|
699
738
|
height, default_width=0, default_height=0)
|
|
700
|
-
elif box.style['width'].unit == '%':
|
|
739
|
+
elif check_math(box.style['width']) or box.style['width'].unit == '%':
|
|
701
740
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
702
741
|
width = 0
|
|
703
742
|
else:
|
|
@@ -711,7 +750,7 @@ def replaced_max_content_width(box, outer=True):
|
|
|
711
750
|
width = box.style['width']
|
|
712
751
|
if width == 'auto':
|
|
713
752
|
height = box.style['height']
|
|
714
|
-
if height == 'auto' or height.unit == '%':
|
|
753
|
+
if height == 'auto' or check_math(height) or height.unit == '%':
|
|
715
754
|
height = 'auto'
|
|
716
755
|
else:
|
|
717
756
|
assert height.unit.lower() == 'px'
|
|
@@ -723,7 +762,7 @@ def replaced_max_content_width(box, outer=True):
|
|
|
723
762
|
width, _ = default_image_sizing(
|
|
724
763
|
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto', height,
|
|
725
764
|
default_width=300, default_height=150)
|
|
726
|
-
elif box.style['width'].unit == '%':
|
|
765
|
+
elif check_math(box.style['width']) or box.style['width'].unit == '%':
|
|
727
766
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
728
767
|
width = 0
|
|
729
768
|
else:
|
weasyprint/pdf/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .. import VERSION, Attachment
|
|
|
9
9
|
from ..html import W3C_DATE_RE
|
|
10
10
|
from ..logger import LOGGER, PROGRESS_LOGGER
|
|
11
11
|
from ..matrix import Matrix
|
|
12
|
+
from ..urls import select_source
|
|
12
13
|
from . import debug, pdfa, pdfua, pdfx
|
|
13
14
|
from .fonts import build_fonts_dictionary
|
|
14
15
|
from .stream import Stream
|
|
@@ -273,14 +274,24 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
273
274
|
key = key.encode('ascii', errors='ignore').decode()
|
|
274
275
|
if key:
|
|
275
276
|
pdf.info[key] = pydyf.String(value)
|
|
277
|
+
if options['xmp_metadata']:
|
|
278
|
+
for url in options['xmp_metadata']:
|
|
279
|
+
result = select_source(url)
|
|
280
|
+
with result as (file_obj, base_url, charset, _):
|
|
281
|
+
xmp_metadata = file_obj.read()
|
|
282
|
+
if charset:
|
|
283
|
+
xmp_metadata = xmp_metadata.decode(charset).encode()
|
|
284
|
+
metadata.xmp_metadata.append(xmp_metadata)
|
|
276
285
|
|
|
277
286
|
# Embedded files
|
|
278
287
|
attachments = metadata.attachments.copy()
|
|
279
288
|
if options['attachments']:
|
|
289
|
+
relationships = iter(options['attachment_relationships'] or [])
|
|
280
290
|
for attachment in options['attachments']:
|
|
281
291
|
if not isinstance(attachment, Attachment):
|
|
282
292
|
attachment = Attachment(
|
|
283
|
-
attachment, url_fetcher=document.url_fetcher
|
|
293
|
+
attachment, url_fetcher=document.url_fetcher,
|
|
294
|
+
relationship=next(relationships, 'Unspecified'))
|
|
284
295
|
attachments.append(attachment)
|
|
285
296
|
pdf_attachments = []
|
|
286
297
|
for attachment in attachments:
|
weasyprint/pdf/anchors.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Insert anchors, links, bookmarks and inputs in PDFs."""
|
|
2
2
|
|
|
3
3
|
import collections
|
|
4
|
-
import io
|
|
5
4
|
import mimetypes
|
|
6
5
|
from hashlib import md5
|
|
7
6
|
from os.path import basename
|
|
@@ -330,18 +329,12 @@ def write_pdf_attachment(pdf, attachment, compress):
|
|
|
330
329
|
"""Write an attachment to the PDF stream."""
|
|
331
330
|
# Attachments from document links like <link> or <a> can only be URLs.
|
|
332
331
|
# They're passed in as tuples
|
|
333
|
-
url = None
|
|
334
|
-
uncompressed_length = 0
|
|
335
|
-
stream = b''
|
|
332
|
+
url = mime_type = None
|
|
336
333
|
try:
|
|
337
|
-
with attachment.source as (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
source = io.BytesIO(source)
|
|
342
|
-
for data in iter(lambda: source.read(4096), b''):
|
|
343
|
-
uncompressed_length += len(data)
|
|
344
|
-
stream += data
|
|
334
|
+
with attachment.source as (file_obj, url, _, mime_type):
|
|
335
|
+
stream = file_obj.read()
|
|
336
|
+
if isinstance(stream, str):
|
|
337
|
+
stream = stream.encode()
|
|
345
338
|
except URLFetchingError as exception:
|
|
346
339
|
LOGGER.error('Failed to load attachment: %s', exception)
|
|
347
340
|
LOGGER.debug('Error while loading attachment:', exc_info=exception)
|
|
@@ -356,9 +349,10 @@ def write_pdf_attachment(pdf, attachment, compress):
|
|
|
356
349
|
filename = basename(unquote(urlsplit(url).path))
|
|
357
350
|
else:
|
|
358
351
|
filename = 'attachment.bin'
|
|
359
|
-
mime_type =
|
|
360
|
-
|
|
361
|
-
|
|
352
|
+
mime_type = (
|
|
353
|
+
mime_type or
|
|
354
|
+
mimetypes.guess_type(filename, strict=False)[0] or
|
|
355
|
+
'application/octet-stream')
|
|
362
356
|
|
|
363
357
|
creation = pydyf.String(attachment.created.strftime('D:%Y%m%d%H%M%SZ'))
|
|
364
358
|
mod = pydyf.String(attachment.modified.strftime('D:%Y%m%d%H%M%SZ'))
|
|
@@ -367,7 +361,7 @@ def write_pdf_attachment(pdf, attachment, compress):
|
|
|
367
361
|
'Subtype': f'/{mime_type.replace("/", "#2f")}',
|
|
368
362
|
'Params': pydyf.Dictionary({
|
|
369
363
|
'CheckSum': f'<{attachment.md5}>',
|
|
370
|
-
'Size':
|
|
364
|
+
'Size': len(stream),
|
|
371
365
|
'CreationDate': creation,
|
|
372
366
|
'ModDate': mod,
|
|
373
367
|
})
|
weasyprint/pdf/fonts.py
CHANGED
|
@@ -618,14 +618,23 @@ def _build_vector_font_dictionary(font_dictionary, pdf, font, widths, compress,
|
|
|
618
618
|
if font.missing:
|
|
619
619
|
# Add CMap that doesn’t include missing glyphs, so that they can be replaced by
|
|
620
620
|
# .notdef.
|
|
621
|
+
cmap_extra = pydyf.Dictionary({
|
|
622
|
+
'Type': '/CMap',
|
|
623
|
+
'CMapName': '/WP-Encod-0',
|
|
624
|
+
'CIDSystemInfo': pydyf.Dictionary({
|
|
625
|
+
'Registry': pydyf.String('Adobe'),
|
|
626
|
+
'Ordering': pydyf.String('Identity'),
|
|
627
|
+
'Supplement': 0,
|
|
628
|
+
}),
|
|
629
|
+
})
|
|
621
630
|
encoding = pydyf.Stream([
|
|
622
631
|
b'/CIDInit /ProcSet findresource begin',
|
|
623
632
|
b'12 dict begin',
|
|
624
633
|
b'begincmap',
|
|
625
634
|
b'/CIDSystemInfo',
|
|
626
635
|
b'3 dict dup begin',
|
|
627
|
-
b'/Registry (
|
|
628
|
-
b'/Ordering (
|
|
636
|
+
b'/Registry (Adobe) def',
|
|
637
|
+
b'/Ordering (Identity) def',
|
|
629
638
|
b'/Supplement 0 def',
|
|
630
639
|
b'end def',
|
|
631
640
|
b'/CMapName /WP-Encod-0 def',
|
|
@@ -633,7 +642,7 @@ def _build_vector_font_dictionary(font_dictionary, pdf, font, widths, compress,
|
|
|
633
642
|
b'1 begincodespacerange',
|
|
634
643
|
b'<0000> <ffff>',
|
|
635
644
|
b'endcodespacerange',
|
|
636
|
-
], compress=compress)
|
|
645
|
+
], cmap_extra, compress=compress)
|
|
637
646
|
available = tuple(font.to_unicode)
|
|
638
647
|
available_length = len(available)
|
|
639
648
|
for i in range(ceil(available_length / 100)):
|