weasyprint 64.1__py3-none-any.whl → 65.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,5 @@
1
1
  /* Default stylesheet for PDF forms */
2
2
 
3
- button, input, select, textarea {
4
- appearance: auto;
5
- }
6
-
7
- select option,
8
- select:not([multiple])::before,
9
- input:not([type="submit"])::before {
10
- visibility: hidden;
11
- }
12
-
13
- textarea {
14
- color: transparent;
15
- }
3
+ button, input, select, textarea { appearance: auto }
4
+ select option, select:not([multiple])::before, input:not([type="submit"])::before {visibility: hidden }
5
+ textarea { text-indent: 10000% } /* Hide text but don’t change color used by PDF form */
@@ -208,6 +208,15 @@ def font_variant(tokens):
208
208
  return values
209
209
 
210
210
 
211
+ @descriptor('font-face')
212
+ @comma_separated_list
213
+ @single_token
214
+ def unicode_range(token):
215
+ """``unicode_range`` descriptor validation."""
216
+ if token.type == 'unicode-range':
217
+ return token
218
+
219
+
211
220
  @descriptor('counter-style')
212
221
  def system(tokens):
213
222
  """``system`` descriptor validation."""
weasyprint/draw/border.py CHANGED
@@ -419,9 +419,9 @@ def clip_border_segment(stream, style, width, side, border_box,
419
419
  else:
420
420
  # 2x - 1/2 dashes
421
421
  dash = length / (dash_length + dash_length % 2 - 0.5)
422
- dashes1 = int(ceil((chl1 - dash / 2) / dash))
423
- dashes2 = int(ceil((chl2 - dash / 2) / dash))
424
- line = int(floor(line_length / dash))
422
+ dashes1 = ceil((chl1 - dash / 2) / dash)
423
+ dashes2 = ceil((chl2 - dash / 2) / dash)
424
+ line = floor(line_length / dash)
425
425
 
426
426
  def draw_dots(dashes, line, way, x, y, px, py, chl):
427
427
  if not dashes:
@@ -483,7 +483,7 @@ def clip_border_segment(stream, style, width, side, border_box,
483
483
  stream.end()
484
484
  dash = length / (
485
485
  round(length / dash) - (round(length / dash) + 1) % 2) or 1
486
- for i in range(0, int(round(length / dash)), 2):
486
+ for i in range(0, round(length / dash), 2):
487
487
  if side == 'top':
488
488
  stream.rectangle(bbx + i * dash, bby, dash, width)
489
489
  elif side == 'right':
weasyprint/draw/text.py CHANGED
@@ -134,7 +134,6 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
134
134
  first_line, index = textbox.pango_layout.get_first_line()
135
135
 
136
136
  utf8_text = textbox.pango_layout.text.encode()
137
- previous_utf8_position = 0
138
137
  stream.set_text_matrix(*matrix.values)
139
138
  previous_pango_font = None
140
139
  string = ''
@@ -152,8 +151,11 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
152
151
  clusters = glyph_string.log_clusters
153
152
 
154
153
  # Get positions of the glyphs in the UTF-8 string.
155
- utf8_positions = [offset + clusters[i] for i in range(1, num_glyphs)]
156
- utf8_positions.append(offset + glyph_item.item.length)
154
+ utf8_positions = [offset + clusters[i] for i in range(num_glyphs)]
155
+ if glyph_item.item.analysis.level % 2:
156
+ utf8_positions.insert(0, offset + glyph_item.item.length) # rtl
157
+ else:
158
+ utf8_positions.append(offset + glyph_item.item.length) # ltr
157
159
 
158
160
  pango_font = glyph_item.item.analysis.font
159
161
  if pango_font != previous_pango_font:
@@ -161,6 +163,10 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
161
163
  previous_pango_font = pango_font
162
164
  font, font_size = stream.add_font(pango_font)
163
165
 
166
+ # Workaround for https://gitlab.gnome.org/GNOME/pango/-/issues/530.
167
+ if pango.pango_version() < 14802:
168
+ font_size = textbox.style['font_size']
169
+
164
170
  # Go through the run glyphs.
165
171
  if string:
166
172
  stream.show_text(string)
@@ -175,7 +181,6 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
175
181
  glyph & pango.PANGO_GLYPH_UNKNOWN_FLAG):
176
182
  string += f'>{-width / font_size}<'
177
183
  continue
178
- utf8_position = utf8_positions[i]
179
184
 
180
185
  offset = glyph_info.geometry.x_offset / font_size
181
186
  rise = glyph_info.geometry.y_offset / 1000
@@ -202,8 +207,8 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
202
207
  if glyph not in font.widths:
203
208
  pango.pango_font_get_glyph_extents(
204
209
  pango_font, glyph, stream.ink_rect, stream.logical_rect)
205
- font.widths[glyph] = int(round(
206
- stream.logical_rect.width * 1000 * FROM_UNITS / font_size))
210
+ font.widths[glyph] = round(
211
+ stream.logical_rect.width * 1000 * FROM_UNITS / font_size)
207
212
 
208
213
  # Set kerning, word spacing, letter spacing.
209
214
  kerning = int(
@@ -213,9 +218,8 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
213
218
 
214
219
  # Create mapping between glyphs and characters.
215
220
  if glyph not in font.cmap:
216
- utf8_slice = slice(previous_utf8_position, utf8_position)
221
+ utf8_slice = slice(*sorted(utf8_positions[i:i+2]))
217
222
  font.cmap[glyph] = utf8_text[utf8_slice].decode()
218
- previous_utf8_position = utf8_position
219
223
 
220
224
  # Create list of emojis.
221
225
  if font.svg:
@@ -724,9 +724,7 @@ def is_whitespace(box, _has_non_whitespace=re.compile('\\S').search):
724
724
 
725
725
 
726
726
  def wrap_improper(box, children, wrapper_type, test=None):
727
- """
728
- Wrap consecutive children that do not pass ``test`` in a box of type
729
- ``wrapper_type``.
727
+ """Wrap consecutive children that do not pass ``test`` in a ``wrapper_type`` box.
730
728
 
731
729
  ``test`` defaults to children being of the same type as ``wrapper_type``.
732
730
 
@@ -744,17 +742,7 @@ def wrap_improper(box, children, wrapper_type, test=None):
744
742
  improper = []
745
743
  yield child
746
744
  else:
747
- # Whitespace either fail the test or were removed earlier,
748
- # so there is no need to take special care with the definition
749
- # of "consecutive".
750
- if isinstance(box, boxes.FlexContainerBox):
751
- # The display value of a flex item must be "blockified", see
752
- # https://www.w3.org/TR/css-flexbox-1/#flex-items
753
- # TODO: These blocks are currently ignored, we should
754
- # "blockify" them and their children.
755
- pass
756
- else:
757
- improper.append(child)
745
+ improper.append(child)
758
746
  if improper:
759
747
  wrapper = wrapper_type.anonymous_from(box, children=[])
760
748
  # Apply the rules again on the new wrapper
@@ -1023,6 +1011,7 @@ def flex_children(box, children):
1023
1011
  if isinstance(box, boxes.FlexContainerBox):
1024
1012
  flex_children = []
1025
1013
  for child in children:
1014
+ child.is_floated = lambda: False
1026
1015
  if child.is_in_normal_flow():
1027
1016
  child.is_flex_item = True
1028
1017
  if isinstance(child, boxes.TextBox) and not child.text.strip(' '):
@@ -1030,8 +1019,14 @@ def flex_children(box, children):
1030
1019
  # affected by the white-space property"
1031
1020
  # https://www.w3.org/TR/css-flexbox-1/#flex-items
1032
1021
  continue
1033
- if isinstance(child, boxes.InlineLevelBox):
1034
- anonymous = boxes.BlockBox.anonymous_from(box, [child])
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
1035
1030
  anonymous.is_flex_item = True
1036
1031
  flex_children.append(anonymous)
1037
1032
  else:
@@ -1069,7 +1064,12 @@ def grid_children(box, children):
1069
1064
  # affected by the white-space property"
1070
1065
  # https://drafts.csswg.org/css-grid-2/#grid-item
1071
1066
  continue
1072
- if isinstance(child, boxes.InlineLevelBox):
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
1073
  anonymous = boxes.BlockBox.anonymous_from(child, [child])
1074
1074
  anonymous.style = child.style
1075
1075
  child.is_grid_item = False
weasyprint/images.py CHANGED
@@ -127,8 +127,8 @@ class RasterImage:
127
127
  width, height = self.width, self.height
128
128
  else:
129
129
  thumbnail = Image.open(io.BytesIO(self.image_data.data))
130
- width = max(1, int(round(self.width * dpi_ratio)))
131
- height = max(1, int(round(self.height * dpi_ratio)))
130
+ width = max(1, round(self.width * dpi_ratio))
131
+ height = max(1, round(self.height * dpi_ratio))
132
132
  thumbnail.thumbnail((width, height))
133
133
  image_file = io.BytesIO()
134
134
  thumbnail.save(
@@ -215,6 +215,12 @@ def layout_document(html, root_box, context, max_loops=8):
215
215
  yield page
216
216
 
217
217
 
218
+ class FakeList(list):
219
+ """List in which you can’t append objects."""
220
+ def append(self, item):
221
+ pass
222
+
223
+
218
224
  class LayoutContext:
219
225
  def __init__(self, style_for, get_image_from_uri, font_config,
220
226
  counter_style, target_collector):
@@ -271,6 +277,17 @@ class LayoutContext:
271
277
  else:
272
278
  self.excluded_shapes = None
273
279
 
280
+ def create_flex_formatting_context(self):
281
+ self.excluded_shapes = FakeList()
282
+ self._excluded_shapes_lists.append(self.excluded_shapes)
283
+
284
+ def finish_flex_formatting_context(self, root_box):
285
+ self._excluded_shapes_lists.pop()
286
+ if self._excluded_shapes_lists:
287
+ self.excluded_shapes = self._excluded_shapes_lists[-1]
288
+ else:
289
+ self.excluded_shapes = None
290
+
274
291
  def get_string_set_for(self, page, name, keyword='first'):
275
292
  """Resolve value of string function."""
276
293
  return self.get_string_or_element_for(
@@ -205,7 +205,7 @@ def absolute_block(context, box, containing_block, fixed_boxes, bottom_space,
205
205
  new_box, resume_at, _, _, _ = flex_layout(
206
206
  context, box, bottom_space, skip_stack, containing_block,
207
207
  page_is_empty=True, absolute_boxes=absolute_boxes,
208
- fixed_boxes=fixed_boxes)
208
+ fixed_boxes=fixed_boxes, discard=False)
209
209
  elif isinstance(box, (boxes.GridContainerBox)):
210
210
  new_box, resume_at, _, _, _ = grid_layout(
211
211
  context, box, bottom_space, skip_stack, containing_block,
@@ -10,7 +10,7 @@ from .float import avoid_collisions, float_layout, get_clearance
10
10
  from .grid import grid_layout
11
11
  from .inline import iter_line_boxes
12
12
  from .min_max import handle_min_max_width
13
- from .percent import resolve_percentages, resolve_position_percentages
13
+ from .percent import percentage, resolve_percentages, resolve_position_percentages
14
14
  from .replaced import block_replaced_box_layout
15
15
  from .table import table_layout, table_wrapper_width
16
16
 
@@ -81,7 +81,7 @@ def block_level_layout_switch(context, box, bottom_space, skip_stack,
81
81
  elif isinstance(box, boxes.FlexBox):
82
82
  result = flex_layout(
83
83
  context, box, bottom_space, skip_stack, containing_block,
84
- page_is_empty, absolute_boxes, fixed_boxes)
84
+ page_is_empty, absolute_boxes, fixed_boxes, discard)
85
85
  elif isinstance(box, boxes.GridBox):
86
86
  result = grid_layout(
87
87
  context, box, bottom_space, skip_stack, containing_block,
@@ -496,11 +496,24 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
496
496
  adjoining_margins = []
497
497
  position_y = box.content_box_y()
498
498
 
499
- if adjoining_margins and box.is_table_wrapper:
500
- collapsed_margin = collapse_margin(adjoining_margins)
501
- child.position_y += collapsed_margin
502
- position_y += collapsed_margin
503
- adjoining_margins = []
499
+ # TODO: Merge this with block_container_layout, block_level_layout, _in_flow_layout,
500
+ # and check code above.
501
+ if adjoining_margins:
502
+ if box.is_table_wrapper: # should not be a special case
503
+ collapsed_margin = collapse_margin(adjoining_margins)
504
+ child.position_y += collapsed_margin
505
+ position_y += collapsed_margin
506
+ adjoining_margins = []
507
+ elif not isinstance(child, boxes.BlockBox): # blocks handle that themselves
508
+ if child.style['margin_top'] == 'auto':
509
+ margin_top = 0
510
+ else:
511
+ margin_top = percentage(child.style['margin_top'], box.width)
512
+ adjoining_margins.append(margin_top)
513
+ offset_y = collapse_margin(adjoining_margins) - margin_top
514
+ child.position_y += offset_y
515
+ position_y += offset_y
516
+ adjoining_margins = []
504
517
 
505
518
  page_is_empty_with_no_children = page_is_empty and not any(
506
519
  child for child in new_children
@@ -515,15 +528,6 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
515
528
  fixed_boxes, adjoining_margins, discard, max_lines)
516
529
 
517
530
  if new_child is not None:
518
- # We need to do this after the child layout to have the
519
- # used value for margin_top (eg. it might be a percentage.)
520
- if not isinstance(new_child, (boxes.BlockBox, boxes.TableBox)):
521
- adjoining_margins.append(new_child.margin_top)
522
- offset_y = (
523
- collapse_margin(adjoining_margins) - new_child.margin_top)
524
- new_child.translate(0, offset_y)
525
- # else: blocks handle that themselves.
526
-
527
531
  if not collapsing_through:
528
532
  new_content_position_y = (
529
533
  new_child.content_box_y() + new_child.height)
@@ -629,13 +633,12 @@ def block_container_layout(context, box, bottom_space, skip_stack,
629
633
  page_is_empty, absolute_boxes, fixed_boxes,
630
634
  adjoining_margins, discard, max_lines):
631
635
  """Set the ``box`` height."""
632
- # TODO: boxes.FlexBox is allowed here because flex_layout calls
633
- # block_container_layout, there's probably a better solution.
634
- assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
636
+ assert isinstance(box, boxes.BlockContainerBox)
635
637
 
636
638
  if box.establishes_formatting_context():
637
639
  context.create_block_formatting_context()
638
640
 
641
+ # TODO: merge this with _in_flow_layout, flex_layout…
639
642
  is_start = skip_stack is None
640
643
  box.remove_decoration(start=not is_start, end=False)
641
644
 
@@ -753,7 +756,7 @@ def block_container_layout(context, box, bottom_space, skip_stack,
753
756
  max_lines)
754
757
  elif stop:
755
758
  if box.height != 'auto':
756
- if context.overflows(box.position_y + box.height, position_y):
759
+ if context.overflows(box.position_y + box.border_height(), position_y):
757
760
  # Box heigh is fixed and it doesn’t overflow page, forget
758
761
  # overflowing children.
759
762
  resume_at = None
@@ -51,11 +51,10 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block,
51
51
  if width == 'auto' and count != 'auto':
52
52
  width = max(0, box.width - (count - 1) * gap) / count
53
53
  elif width != 'auto' and count == 'auto':
54
- count = max(1, int(floor((box.width + gap) / (width + gap))))
54
+ count = max(1, floor((box.width + gap) / (width + gap)))
55
55
  width = (box.width + gap) / count - gap
56
56
  else: # overconstrained, with width != 'auto' and count != 'auto'
57
- count = max(
58
- 1, min(count, int(floor((box.width + gap) / (width + gap)))))
57
+ count = max(1, min(count, floor((box.width + gap) / (width + gap))))
59
58
  width = (box.width + gap) / count - gap
60
59
 
61
60
  # Handle column-span property with the following structure: