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.
Files changed (67) hide show
  1. weasyprint/__init__.py +17 -7
  2. weasyprint/__main__.py +21 -10
  3. weasyprint/anchors.py +4 -4
  4. weasyprint/css/__init__.py +732 -67
  5. weasyprint/css/computed_values.py +65 -170
  6. weasyprint/css/counters.py +1 -1
  7. weasyprint/css/functions.py +206 -0
  8. weasyprint/css/html5_ua.css +3 -7
  9. weasyprint/css/html5_ua_form.css +2 -2
  10. weasyprint/css/media_queries.py +3 -1
  11. weasyprint/css/properties.py +6 -2
  12. weasyprint/css/{utils.py → tokens.py} +306 -397
  13. weasyprint/css/units.py +91 -0
  14. weasyprint/css/validation/__init__.py +1 -1
  15. weasyprint/css/validation/descriptors.py +47 -19
  16. weasyprint/css/validation/expanders.py +7 -8
  17. weasyprint/css/validation/properties.py +341 -357
  18. weasyprint/document.py +20 -19
  19. weasyprint/draw/__init__.py +56 -63
  20. weasyprint/draw/border.py +121 -69
  21. weasyprint/draw/color.py +1 -1
  22. weasyprint/draw/text.py +60 -41
  23. weasyprint/formatting_structure/boxes.py +24 -5
  24. weasyprint/formatting_structure/build.py +33 -45
  25. weasyprint/images.py +76 -62
  26. weasyprint/layout/__init__.py +32 -26
  27. weasyprint/layout/absolute.py +7 -6
  28. weasyprint/layout/background.py +7 -7
  29. weasyprint/layout/block.py +195 -152
  30. weasyprint/layout/column.py +19 -24
  31. weasyprint/layout/flex.py +54 -26
  32. weasyprint/layout/float.py +12 -7
  33. weasyprint/layout/grid.py +284 -90
  34. weasyprint/layout/inline.py +121 -68
  35. weasyprint/layout/page.py +45 -12
  36. weasyprint/layout/percent.py +14 -10
  37. weasyprint/layout/preferred.py +105 -63
  38. weasyprint/layout/replaced.py +9 -6
  39. weasyprint/layout/table.py +16 -9
  40. weasyprint/pdf/__init__.py +58 -18
  41. weasyprint/pdf/anchors.py +3 -4
  42. weasyprint/pdf/fonts.py +126 -69
  43. weasyprint/pdf/metadata.py +36 -4
  44. weasyprint/pdf/pdfa.py +19 -3
  45. weasyprint/pdf/pdfua.py +7 -115
  46. weasyprint/pdf/pdfx.py +83 -0
  47. weasyprint/pdf/stream.py +57 -49
  48. weasyprint/pdf/tags.py +307 -0
  49. weasyprint/stacking.py +14 -15
  50. weasyprint/svg/__init__.py +59 -32
  51. weasyprint/svg/bounding_box.py +4 -2
  52. weasyprint/svg/defs.py +4 -9
  53. weasyprint/svg/images.py +11 -3
  54. weasyprint/svg/text.py +11 -2
  55. weasyprint/svg/utils.py +15 -8
  56. weasyprint/text/constants.py +1 -1
  57. weasyprint/text/ffi.py +4 -3
  58. weasyprint/text/fonts.py +13 -5
  59. weasyprint/text/line_break.py +146 -43
  60. weasyprint/urls.py +41 -13
  61. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/METADATA +5 -6
  62. weasyprint-67.0.dist-info/RECORD +77 -0
  63. weasyprint/draw/stack.py +0 -13
  64. weasyprint-65.1.dist-info/RECORD +0 -74
  65. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/WHEEL +0 -0
  66. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/entry_points.txt +0 -0
  67. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,25 +3,27 @@
3
3
  import unicodedata
4
4
  from math import inf
5
5
 
6
- from ..css import computed_from_cascaded
7
- from ..css.computed_values import character_ratio, strut_layout
6
+ from ..css import Pending
7
+ from ..css.properties import INHERITED
8
8
  from ..formatting_structure import boxes, build
9
- from ..text.line_break import can_break_text, create_layout, split_first_line
10
9
  from .absolute import AbsolutePlaceholder, absolute_layout
11
10
  from .flex import flex_layout
12
11
  from .float import avoid_collisions, float_layout
13
12
  from .grid import grid_layout
14
13
  from .leader import handle_leader
15
14
  from .min_max import handle_min_max_width
16
- from .percent import resolve_one_percentage, resolve_percentages
15
+ from .percent import percentage, resolve_one_percentage, resolve_percentages
17
16
  from .preferred import inline_min_content_width, shrink_to_fit, trailing_whitespace_size
18
17
  from .replaced import inline_replaced_box_layout
19
18
  from .table import find_in_flow_baseline, table_wrapper_width
20
19
 
20
+ from ..text.line_break import ( # isort:skip
21
+ can_break_text, character_ratio, create_layout, split_first_line, strut)
22
+
21
23
 
22
24
  def iter_line_boxes(context, box, position_y, bottom_space, skip_stack,
23
25
  containing_block, absolute_boxes, fixed_boxes,
24
- first_letter_style):
26
+ first_letter_style, first_line_style):
25
27
  """Return an iterator of ``(line, resume_at)``.
26
28
 
27
29
  ``line`` is a laid-out LineBox with as much content as possible that
@@ -30,14 +32,15 @@ def iter_line_boxes(context, box, position_y, bottom_space, skip_stack,
30
32
  """
31
33
  resolve_percentages(box, containing_block)
32
34
  if skip_stack is None:
33
- # TODO: wrong, see https://github.com/Kozea/WeasyPrint/issues/679
35
+ # TODO: wrong, see issue #679.
34
36
  resolve_one_percentage(box, 'text_indent', containing_block.width)
35
37
  else:
36
38
  box.text_indent = 0
37
39
  while True:
38
40
  line, resume_at = get_next_linebox(
39
- context, box, position_y, bottom_space, skip_stack,
40
- containing_block, absolute_boxes, fixed_boxes, first_letter_style)
41
+ context, box, position_y, bottom_space, skip_stack, containing_block,
42
+ absolute_boxes, fixed_boxes, first_letter_style, first_line_style)
43
+ first_line_style = None
41
44
  if line:
42
45
  handle_leader(context, line, containing_block)
43
46
  position_y = line.position_y + line.height
@@ -53,8 +56,16 @@ def iter_line_boxes(context, box, position_y, bottom_space, skip_stack,
53
56
 
54
57
  def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
55
58
  containing_block, absolute_boxes, fixed_boxes,
56
- first_letter_style):
57
- """Return ``(line, resume_at)``."""
59
+ first_letter_style, first_line_style):
60
+ """Return next line from given linebox.
61
+
62
+ Return ``(line, resume_at)``, where ``line`` is a new linebox copied from the
63
+ original one, with replaced children.
64
+
65
+ This function takes care of excluded floating shapes to avoid collisions.
66
+
67
+ """
68
+
58
69
  skip_stack = skip_first_whitespace(linebox, skip_stack)
59
70
  if skip_stack == 'continue':
60
71
  return None, None
@@ -67,7 +78,7 @@ def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
67
78
  # Width and height must be calculated to avoid floats
68
79
  linebox.width = inline_min_content_width(
69
80
  context, linebox, skip_stack=skip_stack, first_line=True)
70
- linebox.height, _ = strut_layout(linebox.style, context)
81
+ linebox.height, _ = strut(linebox.style)
71
82
  else:
72
83
  # No float, width and height will be set by the lines
73
84
  linebox.width = linebox.height = 0
@@ -95,7 +106,7 @@ def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
95
106
  last_letter, float_width) = split_inline_box(
96
107
  context, linebox, position_x, max_x, bottom_space, skip_stack,
97
108
  containing_block, line_absolutes, line_fixed, line_placeholders,
98
- waiting_floats, line_children)
109
+ waiting_floats, line_children, first_letter_style, first_line_style)
99
110
  linebox.width, linebox.height = line.width, line.height
100
111
 
101
112
  if is_phantom_linebox(line) and not preserved_line_break:
@@ -126,7 +137,7 @@ def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
126
137
  line.translate(offset_x, offset_y)
127
138
  # Avoid floating point errors, as position_y - top + top != position_y
128
139
  # Removing this line breaks the position == linebox.position test below
129
- # See https://github.com/Kozea/WeasyPrint/issues/583
140
+ # See issue #583.
130
141
  line.position_y = position_y
131
142
 
132
143
  if line.height <= candidate_height:
@@ -137,6 +148,16 @@ def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
137
148
  context.excluded_shapes = excluded_shapes
138
149
  position_x, position_y, available_width = avoid_collisions(
139
150
  context, line, containing_block, outer=False)
151
+
152
+ if first_line_style:
153
+ first_line_box = line.copy_with_children(line.children)
154
+ first_line_box.element_tag += '::first-line'
155
+ first_line_box.style = first_line_box.style.copy()
156
+ for key, value in first_line_style.items():
157
+ first_line_box.style[key] = value
158
+ line.children = [first_line_box]
159
+ _adjust_line_height(first_line_box)
160
+
140
161
  if containing_block.style['direction'] == 'ltr':
141
162
  condition = (position_x, position_y) == (
142
163
  original_position_x, original_position_y)
@@ -169,8 +190,9 @@ def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
169
190
  fixed_boxes, bottom_space, skip_stack=None)
170
191
  float_children.append(new_waiting_float)
171
192
  if waiting_float_resume_at:
172
- context.broken_out_of_flow[new_waiting_float] = (
173
- waiting_float, containing_block, waiting_float_resume_at)
193
+ context.add_broken_out_of_flow(
194
+ new_waiting_float, waiting_float, containing_block,
195
+ waiting_float_resume_at)
174
196
  if float_children:
175
197
  line.children += tuple(float_children)
176
198
 
@@ -280,8 +302,9 @@ def first_letter_to_box(box, skip_stack, first_letter_style):
280
302
  first_letter = ''
281
303
  child = box.children[0]
282
304
  if isinstance(child, boxes.TextBox):
283
- letter_style = computed_from_cascaded(
284
- cascaded={}, parent_style=first_letter_style, element=None)
305
+ letter_style = box.style.copy()
306
+ for key, value in first_letter_style.items():
307
+ letter_style[key] = value
285
308
  if child.element_tag.endswith('::first-letter'):
286
309
  letter_box = boxes.InlineBox(
287
310
  f'{box.element_tag}::first-letter', letter_style,
@@ -308,10 +331,10 @@ def first_letter_to_box(box, skip_stack, first_letter_style):
308
331
  # "This type of initial letter is similar to an
309
332
  # inline-level element if its 'float' property is 'none',
310
333
  # otherwise it is similar to a floated element."
311
- if first_letter_style['float'] == 'none':
334
+ if letter_style['float'] == 'none':
312
335
  letter_box = boxes.InlineBox(
313
336
  f'{box.element_tag}::first-letter',
314
- first_letter_style, box.element, [])
337
+ letter_style, box.element, [])
315
338
  text_box = boxes.TextBox(
316
339
  f'{box.element_tag}::first-letter', letter_style,
317
340
  box.element, first_letter)
@@ -320,8 +343,7 @@ def first_letter_to_box(box, skip_stack, first_letter_style):
320
343
  else:
321
344
  letter_box = boxes.BlockBox(
322
345
  f'{box.element_tag}::first-letter',
323
- first_letter_style, box.element, [])
324
- letter_box.first_letter_style = None
346
+ letter_style, box.element, [])
325
347
  line_box = boxes.LineBox(
326
348
  f'{box.element_tag}::first-letter', letter_style,
327
349
  box.element, [])
@@ -334,10 +356,8 @@ def first_letter_to_box(box, skip_stack, first_letter_style):
334
356
  build.process_text_transform(text_box)
335
357
  if skip_stack and child_skip_stack:
336
358
  index, = skip_stack
337
- (child_index, grandchild_skip_stack), = (
338
- child_skip_stack.items())
339
- skip_stack = {
340
- index: {child_index + 1: grandchild_skip_stack}}
359
+ (child_index, grandchild_skip_stack), = child_skip_stack.items()
360
+ skip_stack = {index: {child_index + 1: grandchild_skip_stack}}
341
361
  elif isinstance(child, boxes.ParentBox):
342
362
  if skip_stack:
343
363
  child_skip_stack, = skip_stack.values()
@@ -395,10 +415,9 @@ def inline_block_box_layout(context, box, position_x, skip_stack,
395
415
  box.position_x = position_x
396
416
  box.position_y = 0
397
417
  box, _, _, _, _, _ = block_container_layout(
398
- context, box, bottom_space=-inf, skip_stack=skip_stack,
399
- page_is_empty=True, absolute_boxes=absolute_boxes,
400
- fixed_boxes=fixed_boxes, adjoining_margins=None, discard=False,
401
- max_lines=None)
418
+ context, box, bottom_space=-inf, skip_stack=skip_stack, page_is_empty=True,
419
+ absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, adjoining_margins=None,
420
+ first_letter_style=None, first_line_style=None, discard=False, max_lines=None)
402
421
  box.baseline = inline_block_baseline(box)
403
422
  return box
404
423
 
@@ -438,19 +457,24 @@ def inline_block_width(box, context, containing_block):
438
457
  def split_inline_level(context, box, position_x, max_x, bottom_space,
439
458
  skip_stack, containing_block, absolute_boxes,
440
459
  fixed_boxes, line_placeholders, waiting_floats,
441
- line_children):
460
+ line_children, first_letter_style, first_line_style):
442
461
  """Fit as much content as possible from an inline-level box in a width.
443
462
 
444
- Return ``(new_box, resume_at, preserved_line_break, first_letter,
445
- last_letter)``. ``resume_at`` is ``None`` if all of the content
446
- fits. Otherwise it can be passed as a ``skip_stack`` parameter to resume
447
- where we left off.
463
+ Return ``(new_box, resume_at, preserved_line_break, first_letter, last_letter)``.
464
+ ``resume_at`` is ``None`` if all of the content fits. Otherwise it can be passed as
465
+ a ``skip_stack`` parameter to resume where we left off.
448
466
 
449
467
  ``new_box`` is non-empty (unless the box is empty) and as big as possible
450
- while being narrower than ``available_width``, if possible (may overflow
451
- is no split is possible.)
468
+ while respecting ``max_x``, if possible (may overflow is no split is possible.)
452
469
 
453
470
  """
471
+ if first_line_style:
472
+ box = box.copy()
473
+ box.style = box.style.copy()
474
+ for key, value in first_line_style.items():
475
+ if key in INHERITED:
476
+ box.style[key] = value
477
+ build.process_text_transform(box)
454
478
  resolve_percentages(box, containing_block)
455
479
  float_widths = {'left': 0, 'right': 0}
456
480
  if isinstance(box, boxes.TextBox):
@@ -488,7 +512,7 @@ def split_inline_level(context, box, position_x, max_x, bottom_space,
488
512
  last_letter, float_widths) = split_inline_box(
489
513
  context, box, position_x, max_x, bottom_space, skip_stack,
490
514
  containing_block, absolute_boxes, fixed_boxes, line_placeholders,
491
- waiting_floats, line_children)
515
+ waiting_floats, line_children, first_letter_style, first_line_style)
492
516
  elif isinstance(box, boxes.AtomicInlineLevelBox):
493
517
  new_box = atomic_box(
494
518
  context, box, position_x, skip_stack, containing_block,
@@ -568,8 +592,8 @@ def _out_of_flow_layout(context, box, containing_block, index, child,
568
592
  context, child, containing_block, absolute_boxes, fixed_boxes,
569
593
  bottom_space, skip_stack=None)
570
594
  if float_resume_at:
571
- context.broken_out_of_flow[child] = (
572
- child, containing_block, float_resume_at)
595
+ context.add_broken_out_of_flow(
596
+ child, child, containing_block, float_resume_at)
573
597
  waiting_children.append((index, new_child, child))
574
598
  child = new_child
575
599
 
@@ -602,10 +626,10 @@ def _out_of_flow_layout(context, box, containing_block, index, child,
602
626
  context.running_elements[running_name][page].append(child)
603
627
 
604
628
 
605
- def _break_waiting_children(context, box, max_x, bottom_space,
606
- initial_skip_stack, absolute_boxes, fixed_boxes,
607
- line_placeholders, waiting_floats, line_children,
608
- children, waiting_children):
629
+ def _break_waiting_children(context, box, max_x, bottom_space, initial_skip_stack,
630
+ absolute_boxes, fixed_boxes, line_placeholders,
631
+ waiting_floats, line_children, children, waiting_children,
632
+ first_letter_style, first_line_style):
609
633
  if waiting_children:
610
634
  # Too wide, try to cut inside waiting children, starting from the end.
611
635
  # TODO: we should take care of children added into absolute_boxes,
@@ -631,7 +655,7 @@ def _break_waiting_children(context, box, max_x, bottom_space,
631
655
  context, original_child, child.position_x, max_x,
632
656
  bottom_space, child_skip_stack, box, absolute_boxes,
633
657
  fixed_boxes, line_placeholders, waiting_floats,
634
- line_children)
658
+ line_children, first_letter_style, first_line_style)
635
659
  if child_resume_at:
636
660
  break
637
661
  max_x -= 1
@@ -654,10 +678,38 @@ def _break_waiting_children(context, box, max_x, bottom_space,
654
678
  return {children[-1][0] + 1: None}
655
679
 
656
680
 
681
+ def _adjust_line_height(box):
682
+ """Set margins to the half leading to respect line height.
683
+
684
+ Also compensate for borders and padding, we want margin_height() == line_height.
685
+
686
+ """
687
+ line_height, box.baseline = strut(box.style)
688
+ box.height = box.style['font_size']
689
+ half_leading = (line_height - box.height) / 2
690
+ box.margin_top = half_leading - box.border_top_width - box.padding_top
691
+ box.margin_bottom = half_leading - box.border_bottom_width - box.padding_bottom
692
+
693
+
657
694
  def split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,
658
- containing_block, absolute_boxes, fixed_boxes,
659
- line_placeholders, waiting_floats, line_children):
660
- """Same behavior as split_inline_level."""
695
+ containing_block, absolute_boxes, fixed_boxes, line_placeholders,
696
+ waiting_floats, line_children, first_letter_style,
697
+ first_line_style):
698
+ """Fit as much content as possible from an inline box in a width.
699
+
700
+ Return ``(new_box, resume_at, preserved_line_break, first_letter, last_letter)``.
701
+ ``resume_at`` is ``None`` if all of the content fits. Otherwise it can be passed as
702
+ a ``skip_stack`` parameter to resume where we left off.
703
+
704
+ ``new_box`` is non-empty (unless the box is empty) and as big as possible while
705
+ respecting ``max_x``, if possible (may overflow is no split is possible.)
706
+
707
+ This is the recursive step that loops over the children of the box; base steps of
708
+ recursion are handled by ``split_inline_level()``.
709
+
710
+ In this phase, excluded floating shapes are ignored.
711
+
712
+ """
661
713
 
662
714
  # In some cases (shrink-to-fit result being the preferred width)
663
715
  # max_x is coming from Pango itself,
@@ -715,7 +767,8 @@ def split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,
715
767
  split_inline_level(
716
768
  context, child, position_x, available_width, bottom_space,
717
769
  skip_stack, containing_block, absolute_boxes, fixed_boxes,
718
- line_placeholders, child_waiting_floats, line_children))
770
+ line_placeholders, child_waiting_floats, line_children,
771
+ first_letter_style, first_line_style))
719
772
  if box.style['direction'] == 'rtl':
720
773
  end_spacing = left_spacing
721
774
  max_x -= new_float_widths['left']
@@ -730,7 +783,11 @@ def split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,
730
783
  split_inline_level(
731
784
  context, child, position_x, available_width, bottom_space,
732
785
  skip_stack, containing_block, absolute_boxes, fixed_boxes,
733
- line_placeholders, child_waiting_floats, line_children))
786
+ line_placeholders, child_waiting_floats, line_children,
787
+ first_letter_style, first_line_style))
788
+
789
+ float_widths['left'] = max(float_widths['left'], new_float_widths['left'])
790
+ float_widths['right'] = max(float_widths['right'], new_float_widths['right'])
734
791
 
735
792
  skip_stack = None
736
793
  if preserved:
@@ -767,19 +824,19 @@ def split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,
767
824
  # May be None where we have an empty TextBox.
768
825
  assert isinstance(child, boxes.TextBox)
769
826
  else:
827
+ # Store lines to get previous break points.
770
828
  if isinstance(box, boxes.LineBox):
771
829
  line_children.append((index, new_child))
772
- trailing_whitespace = (
773
- isinstance(new_child, boxes.TextBox) and
774
- new_child.text and
775
- unicodedata.category(new_child.text[-1]) == 'Zs')
776
- new_position_x = new_child.position_x + new_child.margin_width()
777
830
 
778
- if new_position_x > max_x and not trailing_whitespace:
831
+ # Check that text doesn’t overflow.
832
+ new_position_x = new_child.position_x + new_child.margin_width()
833
+ if new_position_x - trailing_whitespace_size(context, new_child) > max_x:
834
+ # Text overflows, find previous break point.
779
835
  previous_resume_at = _break_waiting_children(
780
836
  context, containing_block, max_x, bottom_space, initial_skip_stack,
781
- absolute_boxes, fixed_boxes, line_placeholders,
782
- waiting_floats, line_children, children, waiting_children)
837
+ absolute_boxes, fixed_boxes, line_placeholders, waiting_floats,
838
+ line_children, children, waiting_children, first_letter_style,
839
+ first_line_style)
783
840
  if previous_resume_at:
784
841
  resume_at = previous_resume_at
785
842
  break
@@ -836,15 +893,7 @@ def split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,
836
893
  new_box.width = position_x - content_box_left
837
894
  new_box.translate(dx=float_widths['left'], ignore_floats=True)
838
895
 
839
- line_height, new_box.baseline = strut_layout(box.style, context)
840
- new_box.height = box.style['font_size']
841
- half_leading = (line_height - new_box.height) / 2
842
- # Set margins to the half leading but also compensate for borders and
843
- # paddings. We want margin_height() == line_height
844
- new_box.margin_top = (
845
- half_leading - new_box.border_top_width - new_box.padding_top)
846
- new_box.margin_bottom = (
847
- half_leading - new_box.border_bottom_width - new_box.padding_bottom)
896
+ _adjust_line_height(new_box)
848
897
 
849
898
  if new_box.style['position'] == 'relative':
850
899
  for absolute_box in absolute_boxes:
@@ -900,7 +949,7 @@ def split_text_box(context, box, available_width, skip, is_line_start=True):
900
949
  # "only the 'line-height' is used when calculating the height
901
950
  # of the line box."
902
951
  # Set margins so that margin_height() == line_height
903
- line_height, _ = strut_layout(box.style, context)
952
+ line_height, _ = strut(box.style)
904
953
  half_leading = (line_height - height) / 2
905
954
  box.margin_top = half_leading
906
955
  box.margin_bottom = half_leading
@@ -1019,7 +1068,7 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
1019
1068
  if vertical_align == 'baseline':
1020
1069
  child_baseline_y = baseline_y
1021
1070
  elif vertical_align == 'middle':
1022
- one_ex = box.style['font_size'] * character_ratio(box.style, 'x')
1071
+ one_ex = box.style['font_size'] * character_ratio(box.style, 'ex')
1023
1072
  top = baseline_y - (one_ex + child.margin_height()) / 2
1024
1073
  child_baseline_y = top + child.baseline
1025
1074
  elif vertical_align == 'text-top':
@@ -1037,6 +1086,10 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
1037
1086
  # Later, we will assume for this subtree that its baseline
1038
1087
  # is at y=0.
1039
1088
  child_baseline_y = 0
1089
+ elif isinstance(vertical_align, Pending):
1090
+ height, _ = strut(box.style)
1091
+ child_baseline_y = baseline_y - percentage(
1092
+ vertical_align, box.style, height)
1040
1093
  else:
1041
1094
  # Numeric value: The child’s baseline is `vertical_align` above
1042
1095
  # (lower y) the parent’s baseline.
weasyprint/layout/page.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """Layout for pages and CSS3 margin boxes."""
2
2
 
3
3
  import copy
4
- from collections import namedtuple
4
+ from collections import defaultdict, namedtuple
5
5
  from math import inf
6
6
 
7
- from ..css import computed_from_cascaded
7
+ from ..css import AnonymousStyle
8
8
  from ..formatting_structure import boxes, build
9
9
  from ..logger import PROGRESS_LOGGER
10
10
  from .absolute import absolute_box_layout, absolute_layout
@@ -377,8 +377,7 @@ def make_margin_boxes(context, page, state):
377
377
  style = context.style_for(page.page_type, at_keyword)
378
378
  if style is None:
379
379
  # doesn't affect counters
380
- style = computed_from_cascaded(
381
- element=None, cascaded={}, parent_style=page.style)
380
+ style = AnonymousStyle(page.style)
382
381
  _standardize_page_based_counters(style, at_keyword)
383
382
  box = boxes.MarginBox(at_keyword, style)
384
383
  # Empty boxes should not be generated, but they may be needed for
@@ -493,7 +492,8 @@ def margin_box_content_layout(context, page, box):
493
492
  box, resume_at, next_page, _, _, _ = block_container_layout(
494
493
  context, box, bottom_space=-inf, skip_stack=None, page_is_empty=True,
495
494
  absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes,
496
- adjoining_margins=None, discard=False, max_lines=None)
495
+ adjoining_margins=None, first_letter_style=None, first_line_style=None,
496
+ discard=False, max_lines=None)
497
497
  assert resume_at is None
498
498
  for absolute_box in positioned_boxes:
499
499
  absolute_layout(
@@ -622,38 +622,71 @@ def make_page(context, root_box, page_type, resume_at, page_number,
622
622
  context.reported_footnotes = reported_footnotes[i:]
623
623
  break
624
624
 
625
+ # Display out-of-flow boxes broken on the previous page.
626
+ # TODO: we shouldn’t separate broken in-flow and out-of-flow layout.
625
627
  page_is_empty = True
626
628
  adjoining_margins = []
627
629
  positioned_boxes = [] # Mixed absolute and fixed
628
630
  out_of_flow_boxes = []
631
+ excluded_shapes = defaultdict(list)
629
632
  broken_out_of_flow = {}
630
633
  context_out_of_flow = context.broken_out_of_flow.values()
631
634
  context.broken_out_of_flow = broken_out_of_flow
632
- for box, containing_block, skip_stack in context_out_of_flow:
635
+ for box, containing_block, context_box, skip_stack in context_out_of_flow:
636
+ if context_box:
637
+ context.create_block_formatting_context(context_box)
633
638
  box.position_y = root_box.content_box_y()
634
639
  if box.is_floated():
635
640
  out_of_flow_box, out_of_flow_resume_at = float_layout(
636
641
  context, box, containing_block, positioned_boxes,
637
642
  positioned_boxes, 0, skip_stack)
643
+ excluded_shapes[context_box].append(out_of_flow_box)
638
644
  else:
639
645
  assert box.is_absolutely_positioned()
640
646
  out_of_flow_box, out_of_flow_resume_at = absolute_box_layout(
641
647
  context, box, containing_block, positioned_boxes, 0,
642
648
  skip_stack)
643
649
  out_of_flow_boxes.append(out_of_flow_box)
650
+ page_is_empty = False
644
651
  if out_of_flow_resume_at:
645
- broken_out_of_flow[out_of_flow_box] = (
646
- box, containing_block, out_of_flow_resume_at)
652
+ context.add_broken_out_of_flow(
653
+ out_of_flow_box, box, containing_block, out_of_flow_resume_at)
654
+ if context_box:
655
+ context.finish_block_formatting_context()
656
+
657
+ # Set excluded shapes from broken out-of-flow for in-flow content.
658
+ for context_box, shapes in excluded_shapes.items():
659
+ context._excluded_shapes[context_box] = shapes
660
+
661
+ # Display in-flow content.
662
+ initial_root_box = root_box
663
+ initial_resume_at = resume_at
647
664
  root_box, resume_at, next_page, _, _, _ = block_level_layout(
648
665
  context, root_box, 0, resume_at, initial_containing_block,
649
666
  page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins)
650
- assert root_box
667
+ if not root_box:
668
+ # In-flow page rendering didn’t progress, only out-of-flow did. Render empty box
669
+ # at skip_stack and force fragmentation to make the root box and its descendants
670
+ # cover the whole page height.
671
+ assert not page_is_empty
672
+ box = parent = initial_root_box = initial_root_box.deepcopy()
673
+ skip_stack = initial_resume_at
674
+ while skip_stack and len(skip_stack) == 1:
675
+ (skip, skip_stack), = skip_stack.items()
676
+ box, parent = box.children[skip], box
677
+ parent.children = []
678
+ parent.force_fragmentation = True
679
+ root_box, _, _, _, _, _ = block_level_layout(
680
+ context, initial_root_box, 0, initial_resume_at, initial_containing_block,
681
+ True, positioned_boxes, positioned_boxes, adjoining_margins)
682
+ resume_at = initial_resume_at
651
683
  root_box.children = out_of_flow_boxes + root_box.children
652
684
 
653
685
  footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
654
686
  footnote_area = block_level_layout(
655
- context, footnote_area, -inf, None, footnote_area.page, True,
656
- positioned_boxes, positioned_boxes)[0]
687
+ context, footnote_area, bottom_space=-inf, skip_stack=None,
688
+ containing_block=footnote_area.page, page_is_empty=True,
689
+ absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes)[0]
657
690
  footnote_area.translate(dy=-footnote_area.margin_height())
658
691
 
659
692
  page.fixed_boxes = [
@@ -664,7 +697,7 @@ def make_page(context, root_box, page_type, resume_at, page_number,
664
697
  context, absolute_box, page, positioned_boxes, bottom_space=0,
665
698
  skip_stack=None)
666
699
 
667
- context.finish_block_formatting_context(root_box)
700
+ context.finish_block_formatting_context()
668
701
 
669
702
  page.children = [root_box, footnote_area]
670
703
 
@@ -2,19 +2,22 @@
2
2
 
3
3
  from math import inf
4
4
 
5
+ from ..css import resolve_math
6
+ from ..css.functions import check_math
5
7
  from ..formatting_structure import boxes
6
8
 
7
9
 
8
- def percentage(value, refer_to):
10
+ def percentage(value, computed, refer_to):
9
11
  """Return the percentage of the reference value, or the value unchanged.
10
12
 
11
- ``refer_to`` is the length for 100%. If ``refer_to`` is not a number, it
12
- just replaces percentages.
13
+ ``refer_to`` is the length for 100%.
13
14
 
14
15
  """
16
+ if check_math(value):
17
+ value = resolve_math(value, computed, refer_to=refer_to)
15
18
  if value is None or value == 'auto':
16
19
  return value
17
- elif value.unit == 'px':
20
+ elif value.unit.lower() == 'px':
18
21
  return value.value
19
22
  else:
20
23
  assert value.unit == '%'
@@ -31,7 +34,7 @@ def resolve_one_percentage(box, property_name, refer_to):
31
34
  # box.style has computed values
32
35
  value = box.style[property_name]
33
36
  # box attributes are used values
34
- percent = percentage(value, refer_to)
37
+ percent = percentage(value, box.style, refer_to)
35
38
  setattr(box, property_name, percent)
36
39
  if property_name in ('min_width', 'min_height') and percent == 'auto':
37
40
  setattr(box, property_name, 0)
@@ -75,10 +78,10 @@ def resolve_percentages(box, containing_block):
75
78
  # Special handling when the height of the containing block
76
79
  # depends on its content.
77
80
  height = box.style['height']
78
- if height == 'auto' or height.unit == '%':
81
+ if height == 'auto' or check_math(height) or height.unit == '%':
79
82
  box.height = 'auto'
80
83
  else:
81
- assert height.unit == 'px'
84
+ assert height.unit.lower() == 'px'
82
85
  box.height = height.value
83
86
  resolve_one_percentage(box, 'min_height', 0)
84
87
  resolve_one_percentage(box, 'max_height', inf)
@@ -104,7 +107,8 @@ def resolve_percentages(box, containing_block):
104
107
  def resolve_radii_percentages(box):
105
108
  for corner in ('top_left', 'top_right', 'bottom_right', 'bottom_left'):
106
109
  property_name = f'border_{corner}_radius'
107
- rx, ry = box.style[property_name]
110
+ computed = box.style[property_name]
111
+ rx, ry = computed
108
112
 
109
113
  # Short track for common case
110
114
  if (0, 'px') in (rx, ry):
@@ -116,8 +120,8 @@ def resolve_radii_percentages(box):
116
120
  setattr(box, property_name, (0, 0))
117
121
  break
118
122
  else:
119
- rx = percentage(rx, box.border_width())
120
- ry = percentage(ry, box.border_height())
123
+ rx = percentage(rx, box.style, box.border_width())
124
+ ry = percentage(ry, box.style, box.border_height())
121
125
  setattr(box, property_name, (rx, ry))
122
126
 
123
127