weasyprint 65.0__py3-none-any.whl → 66.0__py3-none-any.whl

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