weasyprint 65.1__py3-none-any.whl → 67.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 +17 -7
- weasyprint/__main__.py +21 -10
- weasyprint/anchors.py +4 -4
- weasyprint/css/__init__.py +732 -67
- weasyprint/css/computed_values.py +65 -170
- weasyprint/css/counters.py +1 -1
- weasyprint/css/functions.py +206 -0
- weasyprint/css/html5_ua.css +3 -7
- weasyprint/css/html5_ua_form.css +2 -2
- weasyprint/css/media_queries.py +3 -1
- weasyprint/css/properties.py +6 -2
- weasyprint/css/{utils.py → tokens.py} +306 -397
- weasyprint/css/units.py +91 -0
- weasyprint/css/validation/__init__.py +1 -1
- weasyprint/css/validation/descriptors.py +47 -19
- weasyprint/css/validation/expanders.py +7 -8
- weasyprint/css/validation/properties.py +341 -357
- weasyprint/document.py +20 -19
- weasyprint/draw/__init__.py +56 -63
- weasyprint/draw/border.py +121 -69
- weasyprint/draw/color.py +1 -1
- weasyprint/draw/text.py +60 -41
- weasyprint/formatting_structure/boxes.py +24 -5
- weasyprint/formatting_structure/build.py +33 -45
- weasyprint/images.py +76 -62
- weasyprint/layout/__init__.py +32 -26
- weasyprint/layout/absolute.py +7 -6
- weasyprint/layout/background.py +7 -7
- weasyprint/layout/block.py +195 -152
- weasyprint/layout/column.py +19 -24
- weasyprint/layout/flex.py +54 -26
- weasyprint/layout/float.py +12 -7
- weasyprint/layout/grid.py +284 -90
- weasyprint/layout/inline.py +121 -68
- weasyprint/layout/page.py +45 -12
- weasyprint/layout/percent.py +14 -10
- weasyprint/layout/preferred.py +105 -63
- weasyprint/layout/replaced.py +9 -6
- weasyprint/layout/table.py +16 -9
- weasyprint/pdf/__init__.py +58 -18
- weasyprint/pdf/anchors.py +3 -4
- weasyprint/pdf/fonts.py +126 -69
- weasyprint/pdf/metadata.py +36 -4
- weasyprint/pdf/pdfa.py +19 -3
- weasyprint/pdf/pdfua.py +7 -115
- weasyprint/pdf/pdfx.py +83 -0
- weasyprint/pdf/stream.py +57 -49
- weasyprint/pdf/tags.py +307 -0
- weasyprint/stacking.py +14 -15
- weasyprint/svg/__init__.py +59 -32
- weasyprint/svg/bounding_box.py +4 -2
- weasyprint/svg/defs.py +4 -9
- weasyprint/svg/images.py +11 -3
- weasyprint/svg/text.py +11 -2
- weasyprint/svg/utils.py +15 -8
- weasyprint/text/constants.py +1 -1
- weasyprint/text/ffi.py +4 -3
- weasyprint/text/fonts.py +13 -5
- weasyprint/text/line_break.py +146 -43
- weasyprint/urls.py +41 -13
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/METADATA +5 -6
- weasyprint-67.0.dist-info/RECORD +77 -0
- weasyprint/draw/stack.py +0 -13
- weasyprint-65.1.dist-info/RECORD +0 -74
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/WHEEL +0 -0
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/licenses/LICENSE +0 -0
weasyprint/layout/preferred.py
CHANGED
|
@@ -12,6 +12,9 @@ import sys
|
|
|
12
12
|
from functools import cache
|
|
13
13
|
from math import inf
|
|
14
14
|
|
|
15
|
+
from ..css import resolve_math
|
|
16
|
+
from ..css.functions import check_math
|
|
17
|
+
from ..css.validation import validate_non_shorthand
|
|
15
18
|
from ..formatting_structure import boxes
|
|
16
19
|
from ..text.line_break import can_break_text, split_first_line
|
|
17
20
|
from .replaced import default_image_sizing
|
|
@@ -90,7 +93,7 @@ def max_content_width(context, box, outer=True):
|
|
|
90
93
|
def _block_content_width(context, box, function, outer):
|
|
91
94
|
"""Helper to create ``block_*_content_width.``"""
|
|
92
95
|
width = box.style['width']
|
|
93
|
-
if width == 'auto' or width.unit == '%':
|
|
96
|
+
if width == 'auto' or check_math(width) or width.unit == '%':
|
|
94
97
|
# "percentages on the following properties are treated instead as
|
|
95
98
|
# though they were the following: width: auto"
|
|
96
99
|
# https://dbaron.org/css/intrinsic/#outer-intrinsic
|
|
@@ -99,7 +102,7 @@ def _block_content_width(context, box, function, outer):
|
|
|
99
102
|
if not child.is_absolutely_positioned()]
|
|
100
103
|
width = max(children_widths) if children_widths else 0
|
|
101
104
|
else:
|
|
102
|
-
assert width.unit == 'px'
|
|
105
|
+
assert width.unit.lower() == 'px'
|
|
103
106
|
width = width.value
|
|
104
107
|
|
|
105
108
|
return adjust(box, outer, width)
|
|
@@ -109,11 +112,13 @@ def min_max(box, width):
|
|
|
109
112
|
"""Get box width from given width and box min- and max-widths."""
|
|
110
113
|
min_width = box.style['min_width']
|
|
111
114
|
max_width = box.style['max_width']
|
|
112
|
-
|
|
115
|
+
min_pending = check_math(min_width)
|
|
116
|
+
max_pending = check_math(max_width)
|
|
117
|
+
if min_width == 'auto' or min_pending or min_width.unit == '%':
|
|
113
118
|
min_width = 0
|
|
114
119
|
else:
|
|
115
120
|
min_width = min_width.value
|
|
116
|
-
if max_width == 'auto' or max_width.unit == '%':
|
|
121
|
+
if max_width == 'auto' or max_pending or max_width.unit == '%':
|
|
117
122
|
max_width = inf
|
|
118
123
|
else:
|
|
119
124
|
max_width = max_width.value
|
|
@@ -123,10 +128,12 @@ def min_max(box, width):
|
|
|
123
128
|
1, box.style['font_size'])
|
|
124
129
|
if ratio is not None:
|
|
125
130
|
min_height = box.style['min_height']
|
|
126
|
-
if min_height != 'auto' and min_height.unit != '%':
|
|
127
|
-
min_width = max(min_width, min_height.value * ratio)
|
|
128
131
|
max_height = box.style['max_height']
|
|
129
|
-
|
|
132
|
+
min_pending = check_math(min_height)
|
|
133
|
+
max_pending = check_math(max_height)
|
|
134
|
+
if min_height != 'auto' and not min_pending and min_height.unit != '%':
|
|
135
|
+
min_width = max(min_width, min_height.value * ratio)
|
|
136
|
+
if max_height != 'auto' and not min_pending and max_height.unit != '%':
|
|
130
137
|
max_width = min(max_width, max_height.value * ratio)
|
|
131
138
|
|
|
132
139
|
return max(min_width, min(width, max_width))
|
|
@@ -146,7 +153,7 @@ def margin_width(box, width, left=True, right=True):
|
|
|
146
153
|
):
|
|
147
154
|
style_value = box.style[value]
|
|
148
155
|
if style_value != 'auto':
|
|
149
|
-
if style_value.unit == 'px':
|
|
156
|
+
if style_value.unit.lower() == 'px':
|
|
150
157
|
width += style_value.value
|
|
151
158
|
else:
|
|
152
159
|
assert style_value.unit == '%'
|
|
@@ -236,7 +243,7 @@ def column_group_content_width(context, box):
|
|
|
236
243
|
if width == 'auto' or width.unit == '%':
|
|
237
244
|
width = 0
|
|
238
245
|
else:
|
|
239
|
-
assert width.unit == 'px'
|
|
246
|
+
assert width.unit.lower() == 'px'
|
|
240
247
|
width = width.value
|
|
241
248
|
|
|
242
249
|
return adjust(box, False, width)
|
|
@@ -270,37 +277,51 @@ def table_cell_min_max_content_width(context, box, outer=True):
|
|
|
270
277
|
|
|
271
278
|
def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=None,
|
|
272
279
|
first_line=False):
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
"""Yield line width for each line."""
|
|
281
|
+
|
|
282
|
+
# Set text indent.
|
|
283
|
+
text_indent = 0
|
|
284
|
+
if isinstance(box, boxes.LineBox):
|
|
285
|
+
indent_token = box.style['text_indent']
|
|
286
|
+
if check_math(indent_token):
|
|
287
|
+
# Ignore percentages by setting refer_to to 0.
|
|
288
|
+
result = resolve_math(indent_token, box.style, 'text_indent', refer_to=0)
|
|
289
|
+
value = validate_non_shorthand((result,), 'text-indent')[0][1]
|
|
290
|
+
if value and value.unit != '%':
|
|
291
|
+
text_indent = value.value
|
|
292
|
+
elif indent_token.unit != '%':
|
|
293
|
+
text_indent = box.style['text_indent'].value
|
|
294
|
+
|
|
295
|
+
# Yield widths for each line.
|
|
278
296
|
current_line = 0
|
|
279
297
|
if skip_stack is None:
|
|
280
298
|
skip = 0
|
|
281
299
|
else:
|
|
282
300
|
(skip, skip_stack), = skip_stack.items()
|
|
283
301
|
for child in box.children[skip:]:
|
|
302
|
+
# Skip absolutely positioned elements.
|
|
284
303
|
if child.is_absolutely_positioned():
|
|
285
|
-
continue
|
|
304
|
+
continue
|
|
286
305
|
|
|
306
|
+
# None is used in "lines" to track line breaks, transformed to 0 when yielded.
|
|
287
307
|
if isinstance(child, boxes.InlineBox):
|
|
308
|
+
# Inline box, call function recursively.
|
|
288
309
|
lines = inline_line_widths(
|
|
289
|
-
context, child, outer, is_line_start, minimum, skip_stack,
|
|
290
|
-
first_line)
|
|
310
|
+
context, child, outer, is_line_start, minimum, skip_stack, first_line)
|
|
291
311
|
if first_line:
|
|
292
|
-
lines = [next(lines)]
|
|
312
|
+
lines = [next(lines) or None]
|
|
293
313
|
else:
|
|
294
|
-
lines =
|
|
314
|
+
lines = [line or None for line in lines]
|
|
295
315
|
if len(lines) == 1:
|
|
296
|
-
lines[0] = adjust(child, outer, lines[0])
|
|
316
|
+
lines[0] = adjust(child, outer, lines[0] or 0)
|
|
297
317
|
else:
|
|
298
|
-
lines[0] = adjust(child, outer, lines[0], right=False)
|
|
299
|
-
lines[-1] = adjust(child, outer, lines[-1], left=False)
|
|
318
|
+
lines[0] = adjust(child, outer, lines[0] or 0, right=False) or None
|
|
319
|
+
lines[-1] = adjust(child, outer, lines[-1] or 0, left=False) or None
|
|
300
320
|
elif isinstance(child, boxes.TextBox):
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
321
|
+
# Text box, split into lines.
|
|
322
|
+
white_space = child.style['white_space']
|
|
323
|
+
space_collapse = white_space in ('normal', 'nowrap', 'pre-line')
|
|
324
|
+
text_wrap = white_space in ('normal', 'pre-wrap', 'pre-line')
|
|
304
325
|
if skip_stack is None:
|
|
305
326
|
skip = 0
|
|
306
327
|
else:
|
|
@@ -318,18 +339,21 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
|
|
|
318
339
|
child_text[resume_index:].decode(), child.style, context, max_width,
|
|
319
340
|
child.justification_spacing, is_line_start=is_line_start,
|
|
320
341
|
minimum=True)
|
|
321
|
-
lines.append(width)
|
|
342
|
+
lines.append(width or None)
|
|
322
343
|
if first_line:
|
|
323
344
|
break
|
|
324
345
|
if first_line and new_resume_index:
|
|
325
|
-
|
|
346
|
+
# We only need the first line, break early.
|
|
347
|
+
current_line += lines[0] or 0
|
|
326
348
|
break
|
|
327
349
|
# TODO: use the real next character instead of 'a' to detect line breaks.
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
last_letter = child_text.decode()[-1:]
|
|
351
|
+
can_break = can_break_text(last_letter + 'a', child.style['lang'])
|
|
330
352
|
if minimum and text_wrap and can_break:
|
|
331
|
-
|
|
353
|
+
# Add all possible line breaks for minimal width.
|
|
354
|
+
lines.append(None)
|
|
332
355
|
else:
|
|
356
|
+
# Replaced elements, inline blocks…
|
|
333
357
|
# https://www.w3.org/TR/css-text-3/#overflow-wrap
|
|
334
358
|
# "The line breaking behavior of a replaced element
|
|
335
359
|
# or other atomic inline is equivalent to that
|
|
@@ -338,20 +362,20 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
|
|
|
338
362
|
# "By default, there is a break opportunity
|
|
339
363
|
# both before and after any inline object."
|
|
340
364
|
if minimum:
|
|
341
|
-
lines = [
|
|
365
|
+
lines = [None, min_content_width(context, child), None]
|
|
342
366
|
else:
|
|
343
367
|
lines = [max_content_width(context, child)]
|
|
344
|
-
# The first text line goes on the current line
|
|
345
|
-
current_line += lines[0]
|
|
368
|
+
# The first text line goes on the current line.
|
|
369
|
+
current_line += lines[0] or 0
|
|
346
370
|
if len(lines) > 1:
|
|
347
|
-
# Forced line break
|
|
371
|
+
# Forced line break(s).
|
|
348
372
|
yield current_line + text_indent
|
|
349
373
|
text_indent = 0
|
|
350
374
|
if len(lines) > 2:
|
|
351
375
|
for line in lines[1:-1]:
|
|
352
|
-
yield line
|
|
353
|
-
current_line = lines[-1]
|
|
354
|
-
is_line_start = lines[-1]
|
|
376
|
+
yield line or 0
|
|
377
|
+
current_line = lines[-1] or 0
|
|
378
|
+
is_line_start = lines[-1] is None
|
|
355
379
|
skip_stack = None
|
|
356
380
|
yield current_line + text_indent
|
|
357
381
|
|
|
@@ -362,15 +386,18 @@ def _percentage_contribution(box):
|
|
|
362
386
|
https://dbaron.org/css/intrinsic/#pct-contrib
|
|
363
387
|
|
|
364
388
|
"""
|
|
389
|
+
min_width = box.style['min_width']
|
|
365
390
|
min_width = (
|
|
366
|
-
|
|
367
|
-
|
|
391
|
+
min_width.value if min_width != 'auto' and
|
|
392
|
+
not check_math(min_width) and min_width.unit == '%' else 0)
|
|
393
|
+
max_width = box.style['max_width']
|
|
368
394
|
max_width = (
|
|
369
|
-
|
|
370
|
-
|
|
395
|
+
max_width.value if max_width != 'auto' and
|
|
396
|
+
not check_math(max_width) and max_width.unit == '%' else inf)
|
|
397
|
+
width = box.style['width']
|
|
371
398
|
width = (
|
|
372
|
-
|
|
373
|
-
|
|
399
|
+
width.value if width != 'auto' and
|
|
400
|
+
not check_math(width) and width.unit == '%' else 0)
|
|
374
401
|
return max(min_width, min(width, max_width))
|
|
375
402
|
|
|
376
403
|
|
|
@@ -548,6 +575,7 @@ def table_and_columns_preferred_widths(context, box, outer=True):
|
|
|
548
575
|
for cell in zipped_grid[i]:
|
|
549
576
|
if (cell and cell.colspan == 1 and
|
|
550
577
|
cell.style['width'] != 'auto' and
|
|
578
|
+
not check_math(cell.style['width']) and
|
|
551
579
|
cell.style['width'].unit != '%'):
|
|
552
580
|
constrainedness[i] = True
|
|
553
581
|
break
|
|
@@ -618,7 +646,7 @@ def table_and_columns_preferred_widths(context, box, outer=True):
|
|
|
618
646
|
sum(max_content_widths), large_percentage_contribution,
|
|
619
647
|
*small_percentage_contributions]))
|
|
620
648
|
|
|
621
|
-
if table.style['width'] != 'auto' and table.style['width'].unit == 'px':
|
|
649
|
+
if table.style['width'] != 'auto' and table.style['width'].unit.lower() == 'px':
|
|
622
650
|
# "percentages on the following properties are treated instead as
|
|
623
651
|
# though they were the following: width: auto"
|
|
624
652
|
# https://dbaron.org/css/intrinsic/#outer-intrinsic
|
|
@@ -656,10 +684,9 @@ def replaced_min_content_width(box, outer=True):
|
|
|
656
684
|
if height == 'auto' or height.unit == '%':
|
|
657
685
|
height = 'auto'
|
|
658
686
|
else:
|
|
659
|
-
assert height.unit == 'px'
|
|
687
|
+
assert height.unit.lower() == 'px'
|
|
660
688
|
height = height.value
|
|
661
|
-
if
|
|
662
|
-
box.style['max_width'].unit == '%'):
|
|
689
|
+
if box.style['max_width'] != 'auto' and box.style['max_width'].unit == '%':
|
|
663
690
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
664
691
|
width = 0
|
|
665
692
|
else:
|
|
@@ -667,17 +694,14 @@ def replaced_min_content_width(box, outer=True):
|
|
|
667
694
|
intrinsic_width, intrinsic_height, intrinsic_ratio = (
|
|
668
695
|
image.get_intrinsic_size(
|
|
669
696
|
box.style['image_resolution'], box.style['font_size']))
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
width, _ = default_image_sizing(
|
|
674
|
-
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
|
|
675
|
-
height, default_width=300, default_height=150)
|
|
697
|
+
width, _ = default_image_sizing(
|
|
698
|
+
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
|
|
699
|
+
height, default_width=0, default_height=0)
|
|
676
700
|
elif box.style['width'].unit == '%':
|
|
677
701
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
678
702
|
width = 0
|
|
679
703
|
else:
|
|
680
|
-
assert width.unit == 'px'
|
|
704
|
+
assert width.unit.lower() == 'px'
|
|
681
705
|
width = width.value
|
|
682
706
|
return adjust(box, outer, width)
|
|
683
707
|
|
|
@@ -690,7 +714,7 @@ def replaced_max_content_width(box, outer=True):
|
|
|
690
714
|
if height == 'auto' or height.unit == '%':
|
|
691
715
|
height = 'auto'
|
|
692
716
|
else:
|
|
693
|
-
assert height.unit == 'px'
|
|
717
|
+
assert height.unit.lower() == 'px'
|
|
694
718
|
height = height.value
|
|
695
719
|
image = box.replacement
|
|
696
720
|
intrinsic_width, intrinsic_height, intrinsic_ratio = (
|
|
@@ -703,7 +727,7 @@ def replaced_max_content_width(box, outer=True):
|
|
|
703
727
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
704
728
|
width = 0
|
|
705
729
|
else:
|
|
706
|
-
assert width.unit == 'px'
|
|
730
|
+
assert width.unit.lower() == 'px'
|
|
707
731
|
width = width.value
|
|
708
732
|
return adjust(box, outer, width)
|
|
709
733
|
|
|
@@ -743,17 +767,30 @@ def trailing_whitespace_size(context, box):
|
|
|
743
767
|
"""Return the size of the trailing whitespace of ``box``."""
|
|
744
768
|
from .inline import split_first_line, split_text_box
|
|
745
769
|
|
|
770
|
+
# Find last box child, keep last parent to remove nested trailing spaces.
|
|
771
|
+
last_parent = None
|
|
746
772
|
while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
|
747
773
|
if not box.children:
|
|
748
774
|
return 0
|
|
749
|
-
box = box.children[-1]
|
|
750
|
-
|
|
751
|
-
|
|
775
|
+
last_parent, box = box, box.children[-1]
|
|
776
|
+
|
|
777
|
+
# Return early if possible.
|
|
778
|
+
if not isinstance(box, boxes.TextBox) or not box.text:
|
|
779
|
+
# There’s no text in last child.
|
|
752
780
|
return 0
|
|
753
|
-
|
|
754
|
-
|
|
781
|
+
elif box.style['white_space'] not in ('normal', 'nowrap', 'pre-line'):
|
|
782
|
+
# Spaces don’t collapse.
|
|
755
783
|
return 0
|
|
756
|
-
|
|
784
|
+
elif box.style['font_size'] == 0:
|
|
785
|
+
# Trailing spaces take no space.
|
|
786
|
+
return 0
|
|
787
|
+
elif not box.text.endswith(' '):
|
|
788
|
+
# No trailing space.
|
|
789
|
+
return 0
|
|
790
|
+
|
|
791
|
+
# Strip text.
|
|
792
|
+
if stripped_text := box.text.rstrip(' '):
|
|
793
|
+
# Stripped text is not empty, calculate width difference.
|
|
757
794
|
resume = 0
|
|
758
795
|
while resume is not None:
|
|
759
796
|
old_resume = resume
|
|
@@ -763,12 +800,17 @@ def trailing_whitespace_size(context, box):
|
|
|
763
800
|
stripped_box, resume, _ = split_text_box(
|
|
764
801
|
context, stripped_box, None, old_resume)
|
|
765
802
|
if stripped_box is None:
|
|
766
|
-
#
|
|
803
|
+
# Old box is split just before the trailing spaces.
|
|
767
804
|
return old_box.width
|
|
768
805
|
else:
|
|
806
|
+
# Return difference between old width and stripped width.
|
|
769
807
|
assert resume is None
|
|
770
808
|
return old_box.width - stripped_box.width
|
|
771
809
|
else:
|
|
810
|
+
# Stripped text is empty, render spaces to get width.
|
|
772
811
|
_, _, _, width, _, _ = split_first_line(
|
|
773
812
|
box.text, box.style, context, None, box.justification_spacing)
|
|
813
|
+
# Remove possible trailing spaces from previous child.
|
|
814
|
+
if last_parent and len(last_parent.children) >= 2:
|
|
815
|
+
width += trailing_whitespace_size(context, last_parent.children[-2])
|
|
774
816
|
return width
|
weasyprint/layout/replaced.py
CHANGED
|
@@ -117,8 +117,8 @@ def replacedbox_layout(box):
|
|
|
117
117
|
ref_x = box.width - draw_width
|
|
118
118
|
ref_y = box.height - draw_height
|
|
119
119
|
|
|
120
|
-
position_x = percentage(position_x, ref_x)
|
|
121
|
-
position_y = percentage(position_y, ref_y)
|
|
120
|
+
position_x = percentage(position_x, box.style, ref_x)
|
|
121
|
+
position_y = percentage(position_y, box.style, ref_y)
|
|
122
122
|
if origin_x == 'right':
|
|
123
123
|
position_x = ref_x - position_x
|
|
124
124
|
if origin_y == 'bottom':
|
|
@@ -281,10 +281,13 @@ def block_replaced_box_layout(context, box, containing_block):
|
|
|
281
281
|
block_replaced_width(box, containing_block)
|
|
282
282
|
replaced_box_height(box)
|
|
283
283
|
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
# TODO: flex items shouldn't be block boxes, this condition
|
|
285
|
+
# would then be useless when this is fixed.
|
|
286
|
+
if not box.is_flex_item:
|
|
287
|
+
# Don't collide with floats
|
|
288
|
+
# https://www.w3.org/TR/CSS21/visuren.html#floats
|
|
289
|
+
box.position_x, box.position_y, _ = avoid_collisions(
|
|
290
|
+
context, box, containing_block, outer=False)
|
|
288
291
|
resume_at = None
|
|
289
292
|
next_page = {'break': 'any', 'page': None}
|
|
290
293
|
adjoining_margins = []
|
weasyprint/layout/table.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from math import inf
|
|
4
4
|
|
|
5
|
-
import tinycss2.
|
|
5
|
+
import tinycss2.color5
|
|
6
6
|
|
|
7
7
|
from ..formatting_structure import boxes
|
|
8
8
|
from ..logger import LOGGER
|
|
@@ -184,14 +184,16 @@ def table_layout(context, table, bottom_space, skip_stack, containing_block,
|
|
|
184
184
|
context, cell, bottom_space, cell_skip_stack,
|
|
185
185
|
page_is_empty=page_is_empty, absolute_boxes=absolute_boxes,
|
|
186
186
|
fixed_boxes=fixed_boxes, adjoining_margins=None,
|
|
187
|
-
|
|
187
|
+
first_letter_style=None, first_line_style=None, discard=False,
|
|
188
|
+
max_lines=None)
|
|
188
189
|
cell.style = original_style
|
|
189
190
|
if new_cell is None:
|
|
190
191
|
cell = cell.copy_with_children([])
|
|
191
192
|
cell, _, _, _, _, _ = block_container_layout(
|
|
192
193
|
context, cell, bottom_space, cell_skip_stack,
|
|
193
194
|
page_is_empty=True, absolute_boxes=[], fixed_boxes=[],
|
|
194
|
-
adjoining_margins=None,
|
|
195
|
+
adjoining_margins=None, first_letter_style=None,
|
|
196
|
+
first_line_style=None, discard=False, max_lines=None)
|
|
195
197
|
cell_resume_at = {0: None}
|
|
196
198
|
else:
|
|
197
199
|
cell = new_cell
|
|
@@ -414,9 +416,13 @@ def table_layout(context, table, bottom_space, skip_stack, containing_block,
|
|
|
414
416
|
if avoid_page_break(page_break, context):
|
|
415
417
|
earlier_page_break = find_earlier_page_break(
|
|
416
418
|
context, new_table_children, absolute_boxes, fixed_boxes)
|
|
417
|
-
if earlier_page_break is
|
|
418
|
-
|
|
419
|
-
|
|
419
|
+
if earlier_page_break is None:
|
|
420
|
+
remove_placeholders(
|
|
421
|
+
context, new_table_children, absolute_boxes,
|
|
422
|
+
fixed_boxes)
|
|
423
|
+
return None, None, next_page, position_y
|
|
424
|
+
new_table_children, resume_at = earlier_page_break
|
|
425
|
+
break
|
|
420
426
|
resume_at = {index_group: None}
|
|
421
427
|
else:
|
|
422
428
|
return None, None, next_page, position_y
|
|
@@ -538,7 +544,8 @@ def table_layout(context, table, bottom_space, skip_stack, containing_block,
|
|
|
538
544
|
# We could not fit any content, drop the footer.
|
|
539
545
|
footer = None
|
|
540
546
|
|
|
541
|
-
assert not
|
|
547
|
+
assert not header
|
|
548
|
+
assert not footer
|
|
542
549
|
new_table_children, resume_at, next_page, end_position_y = (
|
|
543
550
|
body_groups_layout(skip_stack, position_y, bottom_space, page_is_empty))
|
|
544
551
|
return header, new_table_children, footer, end_position_y, resume_at, next_page
|
|
@@ -782,7 +789,7 @@ def auto_table_layout(context, box, containing_block):
|
|
|
782
789
|
|
|
783
790
|
if assignable_width < sum(max_content_guess):
|
|
784
791
|
# Default values shouldn't be used, but we never know.
|
|
785
|
-
# See
|
|
792
|
+
# See issue #770.
|
|
786
793
|
lower_guess = guesses[0]
|
|
787
794
|
upper_guess = guesses[-1]
|
|
788
795
|
|
|
@@ -933,7 +940,7 @@ def distribute_excess_width(context, grid, excess_width, column_widths, constrai
|
|
|
933
940
|
column_widths[i] += excess_width / len(columns)
|
|
934
941
|
|
|
935
942
|
|
|
936
|
-
TRANSPARENT = tinycss2.
|
|
943
|
+
TRANSPARENT = tinycss2.color5.parse_color('transparent')
|
|
937
944
|
|
|
938
945
|
|
|
939
946
|
def collapse_table_borders(table, grid_width, grid_height):
|
weasyprint/pdf/__init__.py
CHANGED
|
@@ -3,22 +3,24 @@
|
|
|
3
3
|
from importlib.resources import files
|
|
4
4
|
|
|
5
5
|
import pydyf
|
|
6
|
-
from tinycss2.
|
|
6
|
+
from tinycss2.color5 import D50, D65
|
|
7
7
|
|
|
8
8
|
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 . import debug, pdfa, pdfua
|
|
12
|
+
from . import debug, pdfa, pdfua, pdfx
|
|
13
13
|
from .fonts import build_fonts_dictionary
|
|
14
14
|
from .stream import Stream
|
|
15
|
+
from .tags import add_tags
|
|
15
16
|
|
|
16
17
|
from .anchors import ( # isort:skip
|
|
17
18
|
add_annotations, add_forms, add_links, add_outlines, resolve_links,
|
|
18
19
|
write_pdf_attachment)
|
|
19
20
|
|
|
20
21
|
VARIANTS = {
|
|
21
|
-
name: data
|
|
22
|
+
name: data
|
|
23
|
+
for variants in (pdfa.VARIANTS, pdfua.VARIANTS, pdfx.VARIANTS, debug.VARIANTS)
|
|
22
24
|
for (name, data) in variants.items()}
|
|
23
25
|
|
|
24
26
|
|
|
@@ -52,16 +54,16 @@ def _w3c_date_to_pdf(string, attr_name):
|
|
|
52
54
|
return f'D:{pdf_date}'
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
def _reference_resources(pdf, resources, images, fonts):
|
|
57
|
+
def _reference_resources(pdf, resources, images, fonts, color_profiles):
|
|
56
58
|
if 'Font' in resources:
|
|
57
59
|
assert resources['Font'] is None
|
|
58
60
|
resources['Font'] = fonts
|
|
59
|
-
_use_references(pdf, resources, images)
|
|
61
|
+
_use_references(pdf, resources, images, color_profiles)
|
|
60
62
|
pdf.add_object(resources)
|
|
61
63
|
return resources.reference
|
|
62
64
|
|
|
63
65
|
|
|
64
|
-
def _use_references(pdf, resources, images):
|
|
66
|
+
def _use_references(pdf, resources, images, color_profiles):
|
|
65
67
|
# XObjects
|
|
66
68
|
for key, x_object in resources.get('XObject', {}).items():
|
|
67
69
|
# Images
|
|
@@ -90,7 +92,8 @@ def _use_references(pdf, resources, images):
|
|
|
90
92
|
# Resources
|
|
91
93
|
if 'Resources' in x_object.extra:
|
|
92
94
|
x_object.extra['Resources'] = _reference_resources(
|
|
93
|
-
pdf, x_object.extra['Resources'], images, resources['Font']
|
|
95
|
+
pdf, x_object.extra['Resources'], images, resources['Font'],
|
|
96
|
+
color_profiles)
|
|
94
97
|
|
|
95
98
|
# Patterns
|
|
96
99
|
for key, pattern in resources.get('Pattern', {}).items():
|
|
@@ -98,7 +101,8 @@ def _use_references(pdf, resources, images):
|
|
|
98
101
|
resources['Pattern'][key] = pattern.reference
|
|
99
102
|
if 'Resources' in pattern.extra:
|
|
100
103
|
pattern.extra['Resources'] = _reference_resources(
|
|
101
|
-
pdf, pattern.extra['Resources'], images, resources['Font']
|
|
104
|
+
pdf, pattern.extra['Resources'], images, resources['Font'],
|
|
105
|
+
color_profiles)
|
|
102
106
|
|
|
103
107
|
# Shadings
|
|
104
108
|
for key, shading in resources.get('Shading', {}).items():
|
|
@@ -117,16 +121,18 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
117
121
|
|
|
118
122
|
PROGRESS_LOGGER.info('Step 6 - Creating PDF')
|
|
119
123
|
|
|
124
|
+
compress = not options['uncompressed_pdf']
|
|
125
|
+
|
|
120
126
|
# Set properties according to PDF variants
|
|
121
|
-
mark = False
|
|
122
127
|
srgb = options['srgb']
|
|
128
|
+
pdf_tags = options['pdf_tags']
|
|
123
129
|
variant = options['pdf_variant']
|
|
124
130
|
if variant:
|
|
125
131
|
variant_function, properties = VARIANTS[variant]
|
|
126
|
-
if 'mark' in properties:
|
|
127
|
-
mark = properties['mark']
|
|
128
132
|
if 'srgb' in properties:
|
|
129
133
|
srgb = properties['srgb']
|
|
134
|
+
if 'pdf_tags' in properties:
|
|
135
|
+
pdf_tags = properties['pdf_tags']
|
|
130
136
|
|
|
131
137
|
pdf = pydyf.PDF()
|
|
132
138
|
images = {}
|
|
@@ -140,6 +146,17 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
140
146
|
'Range': pydyf.Array((-125, 125, -125, 125)),
|
|
141
147
|
}))),
|
|
142
148
|
})
|
|
149
|
+
# Custom color profiles
|
|
150
|
+
for key, color_profile in document.color_profiles.items():
|
|
151
|
+
if key == 'device-cmyk':
|
|
152
|
+
# Device CMYK profile is stored as OutputIntent.
|
|
153
|
+
continue
|
|
154
|
+
profile = pydyf.Stream(
|
|
155
|
+
[color_profile.content],
|
|
156
|
+
pydyf.Dictionary({'N': len(color_profile.components)}),
|
|
157
|
+
compress=compress)
|
|
158
|
+
pdf.add_object(profile)
|
|
159
|
+
color_space[key] = pydyf.Array(('/ICCBased', profile.reference))
|
|
143
160
|
pdf.add_object(color_space)
|
|
144
161
|
resources = pydyf.Dictionary({
|
|
145
162
|
'ExtGState': pydyf.Dictionary(),
|
|
@@ -156,9 +173,10 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
156
173
|
|
|
157
174
|
annot_files = {}
|
|
158
175
|
pdf_pages, page_streams = [], []
|
|
159
|
-
compress = not options['uncompressed_pdf']
|
|
160
176
|
for page_number, (page, links_and_anchors) in enumerate(
|
|
161
177
|
zip(document.pages, page_links_and_anchors)):
|
|
178
|
+
tags = {} if pdf_tags else None
|
|
179
|
+
|
|
162
180
|
# Draw from the top-left corner
|
|
163
181
|
matrix = Matrix(scale, 0, 0, -scale, 0, page.height * scale)
|
|
164
182
|
|
|
@@ -175,7 +193,8 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
175
193
|
left / scale, top / scale,
|
|
176
194
|
(right - left) / scale, (bottom - top) / scale)
|
|
177
195
|
stream = Stream(
|
|
178
|
-
document.fonts, page_rectangle, resources, images,
|
|
196
|
+
document.fonts, page_rectangle, resources, images, tags,
|
|
197
|
+
document.color_profiles, compress=compress)
|
|
179
198
|
stream.transform(d=-1, f=(page.height * scale))
|
|
180
199
|
pdf.add_object(stream)
|
|
181
200
|
page_streams.append(stream)
|
|
@@ -187,13 +206,13 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
187
206
|
'Contents': stream.reference,
|
|
188
207
|
'Resources': resources.reference,
|
|
189
208
|
})
|
|
190
|
-
if
|
|
209
|
+
if pdf_tags:
|
|
191
210
|
pdf_page['Tabs'] = '/S'
|
|
192
211
|
pdf_page['StructParents'] = page_number
|
|
193
212
|
pdf.add_page(pdf_page)
|
|
194
213
|
pdf_pages.append(pdf_page)
|
|
195
214
|
|
|
196
|
-
add_links(links_and_anchors, matrix, pdf, pdf_page, pdf_names,
|
|
215
|
+
add_links(links_and_anchors, matrix, pdf, pdf_page, pdf_names, tags)
|
|
197
216
|
add_annotations(
|
|
198
217
|
links_and_anchors[0], matrix, document, pdf, pdf_page, annot_files,
|
|
199
218
|
compress)
|
|
@@ -293,7 +312,7 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
293
312
|
pdf.add_object(dingbats)
|
|
294
313
|
pdf_fonts['ZaDb'] = dingbats.reference
|
|
295
314
|
resources['Font'] = pdf_fonts.reference
|
|
296
|
-
_use_references(pdf, resources, images)
|
|
315
|
+
_use_references(pdf, resources, images, document.color_profiles)
|
|
297
316
|
|
|
298
317
|
# Anchors
|
|
299
318
|
if pdf_names:
|
|
@@ -307,8 +326,25 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
307
326
|
pdf.catalog['Names'] = pydyf.Dictionary()
|
|
308
327
|
pdf.catalog['Names']['Dests'] = dests
|
|
309
328
|
|
|
310
|
-
|
|
311
|
-
|
|
329
|
+
# Add output ICC profile.
|
|
330
|
+
# TODO: we should allow the user or the PDF variant code to set custom values in
|
|
331
|
+
# OutputIntents and remove the "srgb" option. See PDF 2.0 chapter 14.11.5, and
|
|
332
|
+
# https://www.color.org/chardata/drsection1.xalter for a list of "standard
|
|
333
|
+
# production conditions".
|
|
334
|
+
if 'device-cmyk' in document.color_profiles:
|
|
335
|
+
color_profile = document.color_profiles['device-cmyk']
|
|
336
|
+
profile = pydyf.Stream(
|
|
337
|
+
[color_profile.content], pydyf.Dictionary({'N': 4}), compress=compress)
|
|
338
|
+
pdf.add_object(profile)
|
|
339
|
+
pdf.catalog['OutputIntents'] = pydyf.Array([
|
|
340
|
+
pydyf.Dictionary({
|
|
341
|
+
'Type': '/OutputIntent',
|
|
342
|
+
'S': '/GTS_PDFX',
|
|
343
|
+
'OutputConditionIdentifier': pydyf.String(color_profile.name),
|
|
344
|
+
'DestOutputProfile': profile.reference,
|
|
345
|
+
}),
|
|
346
|
+
])
|
|
347
|
+
elif srgb:
|
|
312
348
|
profile = pydyf.Stream(
|
|
313
349
|
[(files(__package__) / 'sRGB2014.icc').read_bytes()],
|
|
314
350
|
pydyf.Dictionary({'N': 3, 'Alternate': '/DeviceRGB'}),
|
|
@@ -323,6 +359,10 @@ def generate_pdf(document, target, zoom, **options):
|
|
|
323
359
|
}),
|
|
324
360
|
])
|
|
325
361
|
|
|
362
|
+
# Add tags
|
|
363
|
+
if pdf_tags:
|
|
364
|
+
add_tags(pdf, document, page_streams)
|
|
365
|
+
|
|
326
366
|
# Apply PDF variants functions
|
|
327
367
|
if variant:
|
|
328
368
|
variant_function(
|