From 281db4a62d4ace44c19ca603d965beb615e93a1d Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 15 May 2023 16:21:45 +0200 Subject: [PATCH 01/29] Always use x_object notation, not xobject --- weasyprint/images.py | 2 +- weasyprint/pdf/stream.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/weasyprint/images.py b/weasyprint/images.py index 58f96f9fc..ca30d9697 100644 --- a/weasyprint/images.py +++ b/weasyprint/images.py @@ -116,7 +116,7 @@ def cache_image_data(self, data, filename=None, alpha=False): key = f'{self.id}{int(alpha)}{self._dpi or ""}' return LazyImage(self._cache, key, data) - def get_xobject(self, width, height, interpolate): + def get_x_object(self, width, height, interpolate): if self.mode in ('RGB', 'RGBA'): color_space = '/DeviceRGB' elif self.mode in ('L', 'LA'): diff --git a/weasyprint/pdf/stream.py b/weasyprint/pdf/stream.py index abb1e06a2..824974ce2 100644 --- a/weasyprint/pdf/stream.py +++ b/weasyprint/pdf/stream.py @@ -380,8 +380,8 @@ def add_image(self, image, width, height, interpolate, ratio): width, height = thumbnail.width, thumbnail.height image.image_data = image.cache_image_data(image_file.getvalue()) - xobject = image.get_xobject(width, height, interpolate) - self._images[image_name] = xobject + x_object = image.get_x_object(width, height, interpolate) + self._images[image_name] = x_object return image_name def add_pattern(self, x, y, width, height, repeat_width, repeat_height, From be72f5c9651f0df58cd6d96161e00330ef2f03fc Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 15 May 2023 16:35:01 +0200 Subject: [PATCH 02/29] =?UTF-8?q?Don=E2=80=99t=20pass=20useless=20width=20?= =?UTF-8?q?and=20height=20arguments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weasyprint/images.py | 3 +-- weasyprint/pdf/stream.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/weasyprint/images.py b/weasyprint/images.py index ca30d9697..38cf105c0 100644 --- a/weasyprint/images.py +++ b/weasyprint/images.py @@ -93,7 +93,6 @@ def draw(self, stream, concrete_width, concrete_height, image_rendering): if self.width <= 0 or self.height <= 0: return - width, height = self.width, self.height interpolate = 'true' if image_rendering == 'auto' else 'false' ratio = 1 if self._dpi: @@ -103,7 +102,7 @@ def draw(self, stream, concrete_width, concrete_height, image_rendering): dpi = max(self.width / width_inches, self.height / height_inches) if dpi > self._dpi: ratio = self._dpi / dpi - image_name = stream.add_image(self, width, height, interpolate, ratio) + image_name = stream.add_image(self, interpolate, ratio) stream.transform( concrete_width, 0, 0, -concrete_height, 0, concrete_height) diff --git a/weasyprint/pdf/stream.py b/weasyprint/pdf/stream.py index 824974ce2..175e3c485 100644 --- a/weasyprint/pdf/stream.py +++ b/weasyprint/pdf/stream.py @@ -362,7 +362,8 @@ def add_group(self, x, y, width, height): self._x_objects[group.id] = group return group - def add_image(self, image, width, height, interpolate, ratio): + def add_image(self, image, interpolate, ratio): + width, height = image.width, image.height image_name = f'i{image.id}{width}{height}{interpolate}{ratio}' self._x_objects[image_name] = None # Set by write_pdf if image_name in self._images: From 33892cd1740d0a2c50a111d94cd31a72315ead5d Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 15 May 2023 22:45:50 +0200 Subject: [PATCH 03/29] =?UTF-8?q?Don=E2=80=99t=20duplicate=20images=20draw?= =?UTF-8?q?n=20with=20multiple=20dpi=20ratios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the same image is displayed twice, with two different sizes, we now only store the image once, with the maximum size required. Smaller images are then drawn with a better resolution than expected, but the PDF is obviously smaller than storing the low-quality version in addition of the high-quality one. Fix #1877. --- weasyprint/images.py | 21 +++++++++++++++++---- weasyprint/pdf/__init__.py | 11 +++++++++-- weasyprint/pdf/stream.py | 24 ++++++++---------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/weasyprint/images.py b/weasyprint/images.py index 38cf105c0..74e43c2ca 100644 --- a/weasyprint/images.py +++ b/weasyprint/images.py @@ -93,7 +93,7 @@ def draw(self, stream, concrete_width, concrete_height, image_rendering): if self.width <= 0 or self.height <= 0: return - interpolate = 'true' if image_rendering == 'auto' else 'false' + interpolate = image_rendering == 'auto' ratio = 1 if self._dpi: pt_to_in = 4 / 3 / 96 @@ -115,7 +115,20 @@ def cache_image_data(self, data, filename=None, alpha=False): key = f'{self.id}{int(alpha)}{self._dpi or ""}' return LazyImage(self._cache, key, data) - def get_x_object(self, width, height, interpolate): + def get_x_object(self, interpolate, dpi_ratio): + if dpi_ratio == 1: + width, height = self.width, self.height + else: + thumbnail = Image.open(io.BytesIO(self.image_data.data)) + width = max(1, int(round(self.width * dpi_ratio))) + height = max(1, int(round(self.height * dpi_ratio))) + thumbnail.thumbnail((width, height)) + image_file = io.BytesIO() + thumbnail.save( + image_file, format=thumbnail.format, optimize=self.optimize) + width, height = thumbnail.width, thumbnail.height + self.image_data = self.cache_image_data(image_file.getvalue()) + if self.mode in ('RGB', 'RGBA'): color_space = '/DeviceRGB' elif self.mode in ('L', 'LA'): @@ -133,7 +146,7 @@ def get_x_object(self, width, height, interpolate): 'Height': height, 'ColorSpace': color_space, 'BitsPerComponent': 8, - 'Interpolate': interpolate, + 'Interpolate': 'true' if interpolate else 'false', }) if self.format == 'JPEG': @@ -175,7 +188,7 @@ def get_x_object(self, width, height, interpolate): 'Height': height, 'ColorSpace': '/DeviceGray', 'BitsPerComponent': 8, - 'Interpolate': interpolate, + 'Interpolate': 'true' if interpolate else 'false', }) else: png_data = self._get_png_data( diff --git a/weasyprint/pdf/__init__.py b/weasyprint/pdf/__init__.py index 6bfb3ddd1..b0b060149 100644 --- a/weasyprint/pdf/__init__.py +++ b/weasyprint/pdf/__init__.py @@ -62,12 +62,19 @@ def _use_references(pdf, resources, images): for key, x_object in resources.get('XObject', {}).items(): # Images if x_object is None: - x_object = images[key] - if x_object.number is not None: + image_data = images[key] + x_object = image_data['x_object'] + + if x_object is not None: # Image already added to PDF resources['XObject'][key] = x_object.reference continue + image = image_data['image'] + dpi_ratio = max(image_data['dpi_ratios']) + x_object = image.get_x_object(image_data['interpolate'], dpi_ratio) + image_data['x_object'] = x_object + pdf.add_object(x_object) resources['XObject'][key] = x_object.reference diff --git a/weasyprint/pdf/stream.py b/weasyprint/pdf/stream.py index 175e3c485..e2a126f4b 100644 --- a/weasyprint/pdf/stream.py +++ b/weasyprint/pdf/stream.py @@ -8,7 +8,6 @@ from fontTools import subset from fontTools.ttLib import TTFont, TTLibError, ttFont from fontTools.varLib.mutator import instantiateVariableFont -from PIL import Image from ..logger import LOGGER from ..matrix import Matrix @@ -363,26 +362,19 @@ def add_group(self, x, y, width, height): return group def add_image(self, image, interpolate, ratio): - width, height = image.width, image.height - image_name = f'i{image.id}{width}{height}{interpolate}{ratio}' + image_name = f'i{image.id}{int(interpolate)}' self._x_objects[image_name] = None # Set by write_pdf if image_name in self._images: # Reuse image already stored in document + self._images[image_name]['dpi_ratios'].add(ratio) return image_name - if ratio != 1: - thumbnail = Image.open(io.BytesIO(image.image_data.data)) - width = int(round(image.width * ratio)) - height = int(round(image.height * ratio)) - thumbnail.thumbnail((max(1, width), max(1, height))) - image_file = io.BytesIO() - thumbnail.save( - image_file, format=thumbnail.format, optimize=image.optimize) - width, height = thumbnail.width, thumbnail.height - image.image_data = image.cache_image_data(image_file.getvalue()) - - x_object = image.get_x_object(width, height, interpolate) - self._images[image_name] = x_object + self._images[image_name] = { + 'image': image, + 'interpolate': interpolate, + 'dpi_ratios': {ratio}, + 'x_object': None, # Set by write_pdf + } return image_name def add_pattern(self, x, y, width, height, repeat_width, repeat_height, From f55b82365c7d199879108d08222dc3892127327b Mon Sep 17 00:00:00 2001 From: kygoh Date: Wed, 7 Jun 2023 17:19:05 +0000 Subject: [PATCH 04/29] Create test case for table cell min-width ignored --- tests/layout/test_table.py | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py index 12c6cb92c..2832ef8c1 100644 --- a/tests/layout/test_table.py +++ b/tests/layout/test_table.py @@ -2912,3 +2912,64 @@ def test_table_different_display(): ''') + + +@assert_no_logs +def test_min_width_with_overflow(): + # issue 1383 + page, = render_pages(''' + + + + + + + + + + + + + + + + +
Normal Key 1Normal Value 1
Normal Key 2Normal Value 2
+ + + + + + + + + + + +
Short valueWorks as expected
Long ValueAnnoyingly breaks my table layout: Sed ut perspiciatis + unde omnis iste natus error sit voluptatem + accusantium doloremque laudantium, totam rem aperiam, + eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. +
+ + ''') + html, = page.children + body, = html.children + table_wrapper_1, table_wrapper_2 = body.children + + table1, = table_wrapper_1.children + tbody1, = table1.children + tr1, tr2 = tbody1.children + table1_td1, table1_td2 = tr1.children + + table2, = table_wrapper_2.children + tbody2, = table2.children + tr1, tr2 = tbody2.children + table2_td1, table2_td2 = tr1.children + + assert table1_td1.min_width == table2_td1.min_width + assert table1_td1.width == table2_td1.width From 8b17b1c604f3663a09e702a52d6050afa661d8f8 Mon Sep 17 00:00:00 2001 From: kygoh Date: Wed, 7 Jun 2023 17:29:15 +0000 Subject: [PATCH 05/29] Fix ignored min-width when computing cell measures --- weasyprint/layout/preferred.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index c508399de..be8150c94 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -225,8 +225,15 @@ def column_group_content_width(context, box): def table_cell_min_content_width(context, box, outer): """Return the min-content width for a ``TableCellBox``.""" + # See https://www.w3.org/TR/css-tables-3/#outer-min-content + min_width = box.style['min_width'] + if min_width == 'auto': + min_width = 0 + else: + min_width = min_width.value children_widths = [ - min_content_width(context, child) for child in box.children + max(min_width, min_content_width(context, child)) + for child in box.children if not child.is_absolutely_positioned()] children_min_width = margin_width( box, max(children_widths) if children_widths else 0) From 3c6696ed90edd20e7262e545d77ae2767df7156d Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Tue, 20 Jun 2023 15:37:39 +0200 Subject: [PATCH 06/29] Fix named pages inheritence --- tests/layout/test_page.py | 26 ++++++++++++++++++++++++ weasyprint/formatting_structure/boxes.py | 9 ++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/layout/test_page.py b/tests/layout/test_page.py index 4e9f0d277..33de4f44c 100644 --- a/tests/layout/test_page.py +++ b/tests/layout/test_page.py @@ -817,6 +817,32 @@ def test_page_names_9(): assert article.element_tag == 'article' +@assert_no_logs +def test_page_names_10(): + pages = render_pages(''' + +
running
+
fixed
+
+ text +
+ text +
+ ''') + page1, page2 = pages + + assert (page1.width, page1.height) == (100, 100) + + assert (page2.width, page2.height) == (100, 100) + + @assert_no_logs @pytest.mark.parametrize('style, line_counts', ( ('orphans: 2; widows: 2', [4, 3]), diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index 93c378ffc..d07bb95ec 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -377,13 +377,14 @@ def get_wrapped_table(self): def page_values(self): start_value, end_value = super().page_values() - if self.children: - if len(self.children) == 1: - page_values = self.children[0].page_values() + children = [c for c in self.children if not(c.is_absolutely_positioned() or c.is_running() or c.is_footnote())] + if children: + if len(children) == 1: + page_values = children[0].page_values() start_value = page_values[0] or start_value end_value = page_values[1] or end_value else: - start_box, end_box = self.children[0], self.children[-1] + start_box, end_box = children[0], children[-1] start_value = start_box.page_values()[0] or start_value end_value = end_box.page_values()[1] or end_value return start_value, end_value From e530a5ea93d8dfc084817e97309e42ff59e0957a Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Thu, 29 Jun 2023 15:21:13 +0200 Subject: [PATCH 07/29] Update code style and tests --- tests/layout/test_page.py | 14 ++++++++++++-- weasyprint/formatting_structure/boxes.py | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/layout/test_page.py b/tests/layout/test_page.py index 33de4f44c..04c7aa047 100644 --- a/tests/layout/test_page.py +++ b/tests/layout/test_page.py @@ -831,16 +831,26 @@ def test_page_names_10():
running
fixed
- text +

text

- text +
text
''') page1, page2 = pages assert (page1.width, page1.height) == (100, 100) + html, runing = page1.children + body, = html.children + fixed, section, = body.children + h1, pagebreak = section.children + assert h1.element_tag == 'h1' assert (page2.width, page2.height) == (100, 100) + html, running = page2.children + fixed, body = html.children + section, = body.children + article, = section.children + assert article.element_tag == 'article' @assert_no_logs diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index d07bb95ec..b31540790 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -377,7 +377,10 @@ def get_wrapped_table(self): def page_values(self): start_value, end_value = super().page_values() - children = [c for c in self.children if not(c.is_absolutely_positioned() or c.is_running() or c.is_footnote())] + children = [ + c for c in self.children + if not (c.is_absolutely_positioned() or c.is_running()) + ] if children: if len(children) == 1: page_values = children[0].page_values() From cf815043b9c14e97aaf318ccbe9dca4b83ce760a Mon Sep 17 00:00:00 2001 From: Obeida Shamoun <2135622+oshmoun@users.noreply.github.com> Date: Tue, 25 Jul 2023 00:49:43 +0200 Subject: [PATCH 08/29] Add support for textLength and lengthAdjust in SVG text elements --- weasyprint/draw.py | 4 ++-- weasyprint/svg/text.py | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index c2fa6e92d..b34ad61de 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -1098,7 +1098,7 @@ def draw_emojis(stream, font_size, x, y, emojis): def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y, - angle=0): + angle=0, scale_x=1): """Draw the given ``textbox`` line to the document ``stream``.""" # Don’t draw lines with only invisible characters if not textbox.text.strip(): @@ -1153,7 +1153,7 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y, utf8_text = textbox.pango_layout.text.encode() previous_utf8_position = 0 - matrix = Matrix(1, 0, 0, -1, x, y) + matrix = Matrix(scale_x, 0, 0, -1, x, y) if angle: a, c = cos(angle), sin(angle) b, d = -c, a diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index 48be5e7a7..dac539f8e 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -68,9 +68,42 @@ def text(svg, node, font_size): ([pl.pop(0) if pl else None for pl in (x, y, dx, dy, rotate)], char) for char in node.text] + # Get textLength and lengthAdjust, if specified + text_length = 0 + if 'textLength' in node.attrib: + text_length = size(normalize(node.attrib['textLength']), font_size, svg.inner_width) + length_adjust = node.attrib.get('lengthAdjust') + # any invalid lengthAdjust value reverts to the default 'spacing' + if length_adjust not in ['spacing', 'spacingAndGlyphs']: + length_adjust = 'spacing' + + letter_spacing = svg.length(node.get('letter-spacing'), font_size) + scale_x = 1 + if text_length: + # calculate the number of spaces to be considered for the text + # only deduct 0.5 and not 1 since the last letter has a half-space + # towards the end of the text element + spaces_count = (len(node.text) - 0.5) + # only adjust letter spacing to fit textLength if: + # - lengthAdjust is set to 'spacing' + # - text is longer than 1 glyph + if length_adjust == 'spacing' and len(node.text) > 1: + # browsers interpret letter-spacing as a negative offset when textLength is set, + # so doing the same here + # TODO: check if that behaviour is according to specs + letter_spacing = round((text_length - width - letter_spacing) / spaces_count) + if length_adjust == 'spacingAndGlyphs': + # scale letter_spacing up/down to textLength + width_with_spacing = width + spaces_count * letter_spacing + letter_spacing *= text_length/width_with_spacing + # calculate the glyphs scaling factor by: + # - deducting the scaled letter_spacing from textLength + # - dividing the calculated value by the original width + spaceless_text_length = text_length - spaces_count * letter_spacing + scale_x = spaceless_text_length/width + # Align text box horizontally x_align = 0 - letter_spacing = svg.length(node.get('letter-spacing'), font_size) text_anchor = node.get('text-anchor') # TODO: use real values ascent, descent = font_size * .8, font_size * .2 @@ -134,8 +167,10 @@ def text(svg, node, font_size): letter, style, svg.context, inf, 0) x = svg.cursor_position[0] if x is None else x y = svg.cursor_position[1] if y is None else y + width = width*scale_x if i: x += letter_spacing + x_position = x + svg.cursor_d_position[0] + x_align y_position = y + svg.cursor_d_position[1] + y_align cursor_position = x + width, y @@ -152,7 +187,7 @@ def text(svg, node, font_size): svg.fill_stroke(node, font_size, text=True) emojis = draw_first_line( svg.stream, TextBox(layout, style), 'none', 'none', - x_position, y_position, angle) + x_position, y_position, angle, scale_x) emoji_lines.append((font_size, x, y, emojis)) svg.cursor_position = cursor_position From 8c7dbb117699ab813ab15540d394b0e9ac9a025c Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 25 Jul 2023 13:07:52 +0200 Subject: [PATCH 09/29] Clean code style Mainly keep the 79-character limit and add spaces around operators. --- weasyprint/svg/text.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index dac539f8e..4f9efac97 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -71,7 +71,8 @@ def text(svg, node, font_size): # Get textLength and lengthAdjust, if specified text_length = 0 if 'textLength' in node.attrib: - text_length = size(normalize(node.attrib['textLength']), font_size, svg.inner_width) + text_length = size( + normalize(node.attrib['textLength']), font_size, svg.inner_width) length_adjust = node.attrib.get('lengthAdjust') # any invalid lengthAdjust value reverts to the default 'spacing' if length_adjust not in ['spacing', 'spacingAndGlyphs']: @@ -83,15 +84,16 @@ def text(svg, node, font_size): # calculate the number of spaces to be considered for the text # only deduct 0.5 and not 1 since the last letter has a half-space # towards the end of the text element - spaces_count = (len(node.text) - 0.5) + spaces_count = len(node.text) - 0.5 # only adjust letter spacing to fit textLength if: # - lengthAdjust is set to 'spacing' # - text is longer than 1 glyph if length_adjust == 'spacing' and len(node.text) > 1: - # browsers interpret letter-spacing as a negative offset when textLength is set, - # so doing the same here + # browsers interpret letter-spacing as a negative offset when + # textLength is set, so doing the same here # TODO: check if that behaviour is according to specs - letter_spacing = round((text_length - width - letter_spacing) / spaces_count) + letter_spacing = round( + (text_length - width - letter_spacing) / spaces_count) if length_adjust == 'spacingAndGlyphs': # scale letter_spacing up/down to textLength width_with_spacing = width + spaces_count * letter_spacing @@ -100,7 +102,7 @@ def text(svg, node, font_size): # - deducting the scaled letter_spacing from textLength # - dividing the calculated value by the original width spaceless_text_length = text_length - spaces_count * letter_spacing - scale_x = spaceless_text_length/width + scale_x = spaceless_text_length / width # Align text box horizontally x_align = 0 From f4f9d3460d670690406ed6079aaa3f19a87a80f4 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 25 Jul 2023 13:10:28 +0200 Subject: [PATCH 10/29] Add extra missing spaces --- weasyprint/svg/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index 4f9efac97..b8d5a9539 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -97,7 +97,7 @@ def text(svg, node, font_size): if length_adjust == 'spacingAndGlyphs': # scale letter_spacing up/down to textLength width_with_spacing = width + spaces_count * letter_spacing - letter_spacing *= text_length/width_with_spacing + letter_spacing *= text_length / width_with_spacing # calculate the glyphs scaling factor by: # - deducting the scaled letter_spacing from textLength # - dividing the calculated value by the original width From 50710de8e935e19aeb3ce4a9bf8be831f193602a Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 25 Jul 2023 13:11:31 +0200 Subject: [PATCH 11/29] Usee *= when possible --- weasyprint/svg/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index b8d5a9539..02862ac86 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -169,7 +169,7 @@ def text(svg, node, font_size): letter, style, svg.context, inf, 0) x = svg.cursor_position[0] if x is None else x y = svg.cursor_position[1] if y is None else y - width = width*scale_x + width *= scale_x if i: x += letter_spacing From d001a23ead415ad5b622315d5ae8a718a426b5c7 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 25 Jul 2023 14:46:18 +0200 Subject: [PATCH 12/29] Give matrix instead of separated values to draw_first_line --- weasyprint/draw.py | 13 +++---------- weasyprint/svg/text.py | 10 +++++++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index b34ad61de..b82a4f3c7 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -4,7 +4,7 @@ import operator from colorsys import hsv_to_rgb, rgb_to_hsv from io import BytesIO -from math import ceil, cos, floor, pi, sin, sqrt, tan +from math import ceil, floor, pi, sqrt, tan from xml.etree import ElementTree from PIL import Image @@ -1073,7 +1073,7 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis): textbox.pango_layout.reactivate(textbox.style) stream.begin_text() emojis = draw_first_line( - stream, textbox, text_overflow, block_ellipsis, x, y) + stream, textbox, text_overflow, block_ellipsis, Matrix(d=-1, e=x, f=y)) stream.end_text() draw_emojis(stream, textbox.style['font_size'], x, y, emojis) @@ -1097,8 +1097,7 @@ def draw_emojis(stream, font_size, x, y, emojis): stream.pop_state() -def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y, - angle=0, scale_x=1): +def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix): """Draw the given ``textbox`` line to the document ``stream``.""" # Don’t draw lines with only invisible characters if not textbox.text.strip(): @@ -1152,12 +1151,6 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y, utf8_text = textbox.pango_layout.text.encode() previous_utf8_position = 0 - - matrix = Matrix(scale_x, 0, 0, -1, x, y) - if angle: - a, c = cos(angle), sin(angle) - b, d = -c, a - matrix = Matrix(a, b, c, d) @ matrix stream.text_matrix(*matrix.values) last_font = None string = '' diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index 02862ac86..2af46a8f0 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -1,7 +1,8 @@ """Draw text.""" -from math import inf, radians +from math import cos, inf, radians, sin +from ..matrix import Matrix from .bounding_box import EMPTY_BOUNDING_BOX, extend_bounding_box from .utils import normalize, size @@ -187,9 +188,12 @@ def text(svg, node, font_size): layout.reactivate(style) svg.fill_stroke(node, font_size, text=True) + matrix = Matrix(a=scale_x, d=-1, e=x_position, f=y_position) + if angle: + a, c = cos(angle), sin(angle) + matrix = Matrix(a, -c, c, a) @ matrix emojis = draw_first_line( - svg.stream, TextBox(layout, style), 'none', 'none', - x_position, y_position, angle, scale_x) + svg.stream, TextBox(layout, style), 'none', 'none', matrix) emoji_lines.append((font_size, x, y, emojis)) svg.cursor_position = cursor_position From cd5608c03d782f56b638a0aed14aad962f944bba Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 25 Jul 2023 15:41:33 +0200 Subject: [PATCH 13/29] Simplify logic for textLength and lengthAdjust We now: - use a normal way of counting spaces - simplify fallback value management for lengthAdjust - always override letter spacing value when textLength is set with lengthAdjust="spacing" - always override width when textLength is set --- weasyprint/svg/text.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index 2af46a8f0..8f2f932d8 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -69,33 +69,13 @@ def text(svg, node, font_size): ([pl.pop(0) if pl else None for pl in (x, y, dx, dy, rotate)], char) for char in node.text] - # Get textLength and lengthAdjust, if specified - text_length = 0 - if 'textLength' in node.attrib: - text_length = size( - normalize(node.attrib['textLength']), font_size, svg.inner_width) - length_adjust = node.attrib.get('lengthAdjust') - # any invalid lengthAdjust value reverts to the default 'spacing' - if length_adjust not in ['spacing', 'spacingAndGlyphs']: - length_adjust = 'spacing' - letter_spacing = svg.length(node.get('letter-spacing'), font_size) + text_length = svg.length(node.get('textLength'), font_size) scale_x = 1 - if text_length: + if text_length and node.text: # calculate the number of spaces to be considered for the text - # only deduct 0.5 and not 1 since the last letter has a half-space - # towards the end of the text element - spaces_count = len(node.text) - 0.5 - # only adjust letter spacing to fit textLength if: - # - lengthAdjust is set to 'spacing' - # - text is longer than 1 glyph - if length_adjust == 'spacing' and len(node.text) > 1: - # browsers interpret letter-spacing as a negative offset when - # textLength is set, so doing the same here - # TODO: check if that behaviour is according to specs - letter_spacing = round( - (text_length - width - letter_spacing) / spaces_count) - if length_adjust == 'spacingAndGlyphs': + spaces_count = len(node.text) - 1 + if normalize(node.attrib.get('lengthAdjust')) == 'spacingAndGlyphs': # scale letter_spacing up/down to textLength width_with_spacing = width + spaces_count * letter_spacing letter_spacing *= text_length / width_with_spacing @@ -104,6 +84,10 @@ def text(svg, node, font_size): # - dividing the calculated value by the original width spaceless_text_length = text_length - spaces_count * letter_spacing scale_x = spaceless_text_length / width + elif spaces_count: + # adjust letter spacing to fit textLength + letter_spacing = (text_length - width) / spaces_count + width = text_length # Align text box horizontally x_align = 0 From 166ec93981b17b93a9c128137ee570664fc26184 Mon Sep 17 00:00:00 2001 From: Obeida Shamoun <2135622+oshmoun@users.noreply.github.com> Date: Sat, 29 Jul 2023 13:26:09 +0200 Subject: [PATCH 14/29] add tests for svg textLength and lengthAdjust --- tests/draw/svg/test_text.py | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/draw/svg/test_text.py b/tests/draw/svg/test_text.py index 90c20969e..3d038f0dc 100644 --- a/tests/draw/svg/test_text.py +++ b/tests/draw/svg/test_text.py @@ -301,3 +301,73 @@ def test_text_rotate(assert_pixels): rotate="180" letter-spacing="2">abc ''') + + +@assert_no_logs +def test_text_text_length(assert_pixels): + assert_pixels(''' + __RRRRRR____________ + __RRRRRR____________ + __BB__BB__BB________ + __BB__BB__BB________ + ''', ''' + + + + abc + + abc + + ''') + + +@assert_no_logs +def test_text_length_adjust_glyphs_only(assert_pixels): + assert_pixels(''' + __RRRRRR____________ + __RRRRRR____________ + __BBBBBBBBBBBB______ + __BBBBBBBBBBBB______ + ''', ''' + + + + abc + + abc + + ''') + + +@assert_no_logs +def test_text_length_adjust_spacing_and_glyphs(assert_pixels): + assert_pixels(''' + __RR_RR_RR__________ + __RR_RR_RR__________ + __BBBB__BBBB__BBBB__ + __BBBB__BBBB__BBBB__ + ''', ''' + + + abc + + abc + + + ''') From ce8f925dc609d69266891d1f39cf55ef478f52ac Mon Sep 17 00:00:00 2001 From: Sahil Rohilla Date: Thu, 3 Aug 2023 03:12:42 +0530 Subject: [PATCH 15/29] Do not break boxes with defined height and hidden overflow. FIX: #1904 --- weasyprint/layout/block.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py index e7caf2485..c6f6bf934 100644 --- a/weasyprint/layout/block.py +++ b/weasyprint/layout/block.py @@ -501,6 +501,12 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, new_child.border_box_y() + new_child.border_height()) page_overflow = context.overflows_page( bottom_space, new_content_position_y) + + # Do not break boxes with defined height and hidden overflow. + # https://github.com/Kozea/WeasyPrint/issues/1904 + if box.style['overflow'] == 'hidden' and box.style['height'] != 'auto': + page_overflow = False + if page_overflow and not page_is_empty_with_no_children: # The child content overflows the page area, display it on the # next page. From 1232f31053251b4b25e69957a98e7f6aaa56bd81 Mon Sep 17 00:00:00 2001 From: Sahil Rohilla Date: Thu, 3 Aug 2023 07:18:20 +0530 Subject: [PATCH 16/29] same condition for _out_of_flow_layout --- weasyprint/layout/block.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py index c6f6bf934..c376e1548 100644 --- a/weasyprint/layout/block.py +++ b/weasyprint/layout/block.py @@ -249,6 +249,8 @@ def _out_of_flow_layout(context, box, index, child, new_children, # New page if overflow page_overflow = context.overflows_page( bottom_space, new_child.position_y + new_child.height) + if box.style['overflow'] == 'hidden' and box.style['height'] != 'auto': + page_overflow = False if (page_is_empty and not new_children) or not page_overflow: new_child.index = index new_children.append(new_child) @@ -501,12 +503,8 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, new_child.border_box_y() + new_child.border_height()) page_overflow = context.overflows_page( bottom_space, new_content_position_y) - - # Do not break boxes with defined height and hidden overflow. - # https://github.com/Kozea/WeasyPrint/issues/1904 if box.style['overflow'] == 'hidden' and box.style['height'] != 'auto': page_overflow = False - if page_overflow and not page_is_empty_with_no_children: # The child content overflows the page area, display it on the # next page. From d2e3d10c3ba516d0a4539993e94692d8461c2ed9 Mon Sep 17 00:00:00 2001 From: Andy Lenards Date: Fri, 4 Aug 2023 15:29:23 -0700 Subject: [PATCH 17/29] Include POSIX compliant single quotes --- docs/contribute.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contribute.rst b/docs/contribute.rst index e586fae94..052240c37 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -13,7 +13,7 @@ install WeasyPrint dependencies. git clone https://github.com/Kozea/WeasyPrint.git cd WeasyPrint python -m venv venv - venv/bin/pip install -e .[doc,test] + venv/bin/pip install -e '.[doc,test]' You can then launch Python to test your changes. From 153ce9eeebd20d7eda56809637000458c0eaef08 Mon Sep 17 00:00:00 2001 From: Andy Lenards Date: Fri, 4 Aug 2023 15:36:35 -0700 Subject: [PATCH 18/29] Mention Ghostscript required for tests --- docs/contribute.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/contribute.rst b/docs/contribute.rst index e586fae94..c146f2902 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -44,6 +44,8 @@ Tests Tests are stored in the ``tests`` folder at the top of the repository. They use the pytest_ library. +Test required Ghostscript_ to be installed and available on the local path. + You can launch tests using the following command:: venv/bin/python -m pytest @@ -55,6 +57,7 @@ style:: venv/bin/python -m flake8 .. _pytest: https://docs.pytest.org/ +.. _Ghostscript: https://www.ghostscript.com/ .. _isort: https://pycqa.github.io/isort/ .. _flake8: https://flake8.pycqa.org/ From a228088b9b7894f94e9f2dba54b57a119ef20adf Mon Sep 17 00:00:00 2001 From: Andy Lenards Date: Fri, 4 Aug 2023 15:41:48 -0700 Subject: [PATCH 19/29] Fix wording --- docs/contribute.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contribute.rst b/docs/contribute.rst index c146f2902..cf15c8f43 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -44,7 +44,7 @@ Tests Tests are stored in the ``tests`` folder at the top of the repository. They use the pytest_ library. -Test required Ghostscript_ to be installed and available on the local path. +Tests require Ghostscript_ to be installed and available on the local path. You can launch tests using the following command:: From e1f4ef8fd3810b0fe0c5b885db62bc6f4e2a9ba5 Mon Sep 17 00:00:00 2001 From: Lucie Anglade Date: Sat, 5 Aug 2023 11:06:27 +0200 Subject: [PATCH 20/29] Remove useless attributes --- weasyprint/formatting_structure/boxes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index 93c378ffc..fc69a83e2 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -601,9 +601,6 @@ class TableColumnGroupBox(ParentBox): internal_table_or_caption = True proper_parents = (TableBox, InlineTableBox) - # Default value. May be overriden on instances. - span = 1 - # Columns groups never have margins or paddings margin_top = 0 margin_bottom = 0 @@ -638,9 +635,6 @@ class TableColumnBox(ParentBox): internal_table_or_caption = True proper_parents = (TableBox, InlineTableBox, TableColumnGroupBox) - # Default value. May be overriden on instances. - span = 1 - # Columns never have margins or paddings margin_top = 0 margin_bottom = 0 From bfc0fd00a9d4f19aa2aa080e8489242a34b8985d Mon Sep 17 00:00:00 2001 From: Sahil Rohilla Date: Sun, 6 Aug 2023 05:18:38 +0530 Subject: [PATCH 21/29] Balance columns before "column-span: all" Fix #1914. --- weasyprint/layout/column.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/weasyprint/layout/column.py b/weasyprint/layout/column.py index deb7e17f5..fd08c8a58 100644 --- a/weasyprint/layout/column.py +++ b/weasyprint/layout/column.py @@ -60,12 +60,14 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block, # ] columns_and_blocks = [] column_children = [] + spanner_index = -1 skip, = skip_stack.keys() if skip_stack else (0,) for i, child in enumerate(box.children[skip:], start=skip): if child.style['column_span'] == 'all': if column_children: columns_and_blocks.append( (i - len(column_children), column_children)) + spanner_index = i columns_and_blocks.append((i, child.copy())) column_children = [] continue @@ -261,7 +263,8 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block, # Everything fits, start expanding columns at the average # of the column heights max_height -= last_footnotes_height - if style['column_fill'] == 'balance': + if (style['column_fill'] == 'balance' or + index < spanner_index): balancing = True height = sum(consumed_heights) / count else: From a39d4252d5c59ff5c1fa6119b96ea262a829d766 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 6 Aug 2023 14:00:38 +0200 Subject: [PATCH 22/29] Add test for column span and balance Related to #1934. --- tests/layout/test_column.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/layout/test_column.py b/tests/layout/test_column.py index be266bd26..bb7fe4c36 100644 --- a/tests/layout/test_column.py +++ b/tests/layout/test_column.py @@ -417,6 +417,38 @@ def ghi assert column3.children[0].children[0].children[0].text == 'ghi' +@assert_no_logs +def test_column_span_balance(): + page, = render_pages(''' + +
+ abc def +
line1
+ ghi jkl +
+ ''') + html, = page.children + body, = html.children + div, = body.children + column1, column2, section, column3 = div.children + assert (column1.position_x, column1.position_y) == (0, 0) + assert (column2.position_x, column2.position_y) == (4, 0) + assert (section.position_x, section.position_y) == (0, 1) + assert (column3.position_x, column3.position_y) == (0, 2) + + assert column1.children[0].children[0].children[0].text == 'abc' + assert column2.children[0].children[0].children[0].text == 'def' + assert section.children[0].children[0].text == 'line1' + assert column3.children[0].children[0].children[0].text == 'ghi' + assert column3.children[0].children[1].children[0].text == 'jkl' + + @assert_no_logs def test_columns_multipage(): page1, page2 = render_pages(''' From 897d3acc67d71d09c75c6aadd75506dc463e0bae Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 6 Aug 2023 14:27:27 +0200 Subject: [PATCH 23/29] Avoid extra variable for column balancing Related to #1934. --- weasyprint/layout/column.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/weasyprint/layout/column.py b/weasyprint/layout/column.py index fd08c8a58..6f6e2c6d6 100644 --- a/weasyprint/layout/column.py +++ b/weasyprint/layout/column.py @@ -60,14 +60,12 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block, # ] columns_and_blocks = [] column_children = [] - spanner_index = -1 skip, = skip_stack.keys() if skip_stack else (0,) for i, child in enumerate(box.children[skip:], start=skip): if child.style['column_span'] == 'all': if column_children: columns_and_blocks.append( (i - len(column_children), column_children)) - spanner_index = i columns_and_blocks.append((i, child.copy())) column_children = [] continue @@ -264,7 +262,7 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block, # of the column heights max_height -= last_footnotes_height if (style['column_fill'] == 'balance' or - index < spanner_index): + index < columns_and_blocks[-1][0]): balancing = True height = sum(consumed_heights) / count else: From 243aa2b6d42eee2ef097e6e5c489fef59692f57a Mon Sep 17 00:00:00 2001 From: Sahil Rohilla Date: Fri, 4 Aug 2023 11:57:05 +0530 Subject: [PATCH 24/29] test overflow hidden for _in_flow_layout and _out_of_flow_layout --- tests/layout/test_block.py | 26 ++++++++++++++++++++++++++ weasyprint/layout/block.py | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/layout/test_block.py b/tests/layout/test_block.py index c321cef83..176c8354e 100644 --- a/tests/layout/test_block.py +++ b/tests/layout/test_block.py @@ -743,6 +743,32 @@ def test_overflow_auto(): assert article.height == 50 + 10 + 10 +def test_overflow_hidden_in_flow_layout(): + page, = render_pages(''' +
+
abc
+
def
+
+ ''') + html, = page.children + body, = html.children + parent_div, = body.children + assert parent_div.height == 3 + + +def test_overflow_hidden_out_of_flow_layout(): + page, = render_pages(''' +
+
abc
+
def
+
+ ''') + html, = page.children + body, = html.children + parent_div, = body.children + assert parent_div.height == 3 + + @assert_no_logs def test_box_margin_top_repagination(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/943 diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py index c376e1548..b09afeafa 100644 --- a/weasyprint/layout/block.py +++ b/weasyprint/layout/block.py @@ -503,7 +503,8 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, new_child.border_box_y() + new_child.border_height()) page_overflow = context.overflows_page( bottom_space, new_content_position_y) - if box.style['overflow'] == 'hidden' and box.style['height'] != 'auto': + if (box.style['overflow'] == 'hidden' and + box.style['height'] != 'auto'): page_overflow = False if page_overflow and not page_is_empty_with_no_children: # The child content overflows the page area, display it on the From 6977e79ce94ef605d39276702e3484d2a9570b35 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 9 Aug 2023 20:14:07 +0200 Subject: [PATCH 25/29] Only draw required glyph with OpenType-SVG fonts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s really slow when SVGs are huge. NotoColorEmoji uses one single 14MB SVG to store everything and let rendering implementations do the complex work. Google, Adobe, Microsoft: you’re not nice at all. You have endless engineers to add incredibly complex code to handle this in Chrome, Photoshop, Windows. Others just don’t. Many applications just crash or don’t display anything with this kind of fonts. Fix #1935. --- weasyprint/draw.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index c2fa6e92d..8dd147092 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -1236,7 +1236,16 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y, hb_data = harfbuzz.hb_blob_get_data(hb_blob, stream.length) if hb_data != ffi.NULL: svg_data = ffi.unpack(hb_data, int(stream.length[0])) + # Do as explained in specification + # https://learn.microsoft.com/typography/opentype/spec/svg tree = ElementTree.fromstring(svg_data) + defs = ElementTree.Element('defs') + for child in list(tree): + defs.append(child) + tree.remove(child) + tree.append(defs) + ElementTree.SubElement( + tree, 'use', attrib={'href': f'#glyph{glyph}'}) image = SVGImage(tree, None, None, stream) a = d = font.widths[glyph] / 1000 / font.upem * font_size emojis.append([image, font, a, d, x_advance, 0]) From c4d663d06e99feca722bc1a8820a5f4781074fce Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 19 Aug 2023 10:25:28 +0200 Subject: [PATCH 26/29] =?UTF-8?q?Don=E2=80=99t=20draw=20clipPath=20when=20?= =?UTF-8?q?defined=20after=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #1595. --- weasyprint/svg/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/weasyprint/svg/__init__.py b/weasyprint/svg/__init__.py index ae9ed6250..ffb6720e6 100644 --- a/weasyprint/svg/__init__.py +++ b/weasyprint/svg/__init__.py @@ -418,8 +418,10 @@ def draw_node(self, node, font_size, fill_stroke=True): width, height = self.point( node.get('width'), node.get('height'), font_size) self.stream.transform(a=width, d=height, e=x, f=y) + original_tag = clip_path._etree_node.tag clip_path._etree_node.tag = 'g' self.draw_node(clip_path, font_size, fill_stroke=False) + clip_path._etree_node.tag = original_tag # At least set the clipping area to an empty path, so that it’s # totally clipped when the clipping path is empty. self.stream.rectangle(0, 0, 0, 0) From 5ae1aabcbcea076e4a3cbbc0dd242a9369ae4adb Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 19 Aug 2023 12:00:34 +0200 Subject: [PATCH 27/29] Improve and document page name inheritance Related to #1897. --- weasyprint/formatting_structure/boxes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index 9d6b22cfa..9d8dcf250 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -377,10 +377,11 @@ def get_wrapped_table(self): def page_values(self): start_value, end_value = super().page_values() + # TODO: We should find Class A possible page breaks according to + # https://drafts.csswg.org/css-page-3/#propdef-page + # Keep only children in normal flow for now. children = [ - c for c in self.children - if not (c.is_absolutely_positioned() or c.is_running()) - ] + child for child in self.children if child.is_in_normal_flow()] if children: if len(children) == 1: page_values = children[0].page_values() From 2326bcc636e7dc3fda544f5cb7b6c58f0d94b010 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 20 Aug 2023 15:07:55 +0200 Subject: [PATCH 28/29] Define monolithic boxes Related to #1936. --- weasyprint/formatting_structure/boxes.py | 10 ++++++++++ weasyprint/layout/block.py | 22 ++++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index 9d8dcf250..64995b895 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -289,6 +289,16 @@ def is_in_normal_flow(self): self.is_floated() or self.is_absolutely_positioned() or self.is_running() or self.is_footnote()) + def is_monolithic(self): + """Return whether this box is monolithic.""" + # https://www.w3.org/TR/css-break-3/#monolithic + return ( + isinstance(self, AtomicInlineLevelBox) or + isinstance(self, ReplacedBox) or + self.style['overflow'] in ('auto', 'scroll') or + (self.style['overflow'] == 'hidden' and + self.style['height'] != 'auto')) + # Start and end page values for named pages def page_values(self): diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py index b09afeafa..2e1dc9958 100644 --- a/weasyprint/layout/block.py +++ b/weasyprint/layout/block.py @@ -249,9 +249,11 @@ def _out_of_flow_layout(context, box, index, child, new_children, # New page if overflow page_overflow = context.overflows_page( bottom_space, new_child.position_y + new_child.height) - if box.style['overflow'] == 'hidden' and box.style['height'] != 'auto': - page_overflow = False - if (page_is_empty and not new_children) or not page_overflow: + add_child = ( + (page_is_empty and not new_children) or + not page_overflow or + box.is_monolithic()) + if add_child: new_child.index = index new_children.append(new_child) else: @@ -501,19 +503,19 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, new_child.content_box_y() + new_child.height) new_position_y = ( new_child.border_box_y() + new_child.border_height()) - page_overflow = context.overflows_page( + content_page_overflow = context.overflows_page( bottom_space, new_content_position_y) - if (box.style['overflow'] == 'hidden' and - box.style['height'] != 'auto'): - page_overflow = False - if page_overflow and not page_is_empty_with_no_children: + border_page_overflow = context.overflows_page( + bottom_space, new_position_y) + can_break = not ( + page_is_empty_with_no_children or box.is_monolithic()) + if can_break and content_page_overflow: # The child content overflows the page area, display it on the # next page. remove_placeholders( context, [new_child], absolute_boxes, fixed_boxes) new_child = None - elif not page_is_empty_with_no_children and context.overflows_page( - bottom_space, new_position_y): + elif can_break and border_page_overflow: # The child border/padding overflows the page area, do the # layout again with a higher bottom_space value. remove_placeholders( From 3fdd3c51623eea7c9b35069b5100e65e4c7fdc08 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 25 Aug 2023 21:55:19 +0200 Subject: [PATCH 29/29] =?UTF-8?q?Use=20bleed=20area=20for=20page=E2=80=99s?= =?UTF-8?q?=20painting=20area?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also use border box for canvas. Fix #1943. --- tests/draw/test_background.py | 13 ++++++------ weasyprint/draw.py | 25 +++++------------------- weasyprint/formatting_structure/boxes.py | 13 ++++++++++++ weasyprint/layout/background.py | 12 +++++++----- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/draw/test_background.py b/tests/draw/test_background.py index 4383005b4..6e2614f28 100644 --- a/tests/draw/test_background.py +++ b/tests/draw/test_background.py @@ -1054,13 +1054,14 @@ def test_background_size_clip(assert_pixels): @assert_no_logs def test_bleed_background_size_clip(assert_pixels): + # Regression test for https://github.com/Kozea/WeasyPrint/issues/1943 assert_pixels(''' - RRRRRR - RBBBBR - RBRBBR - RBBBBR - RBBBBR - RRRRRR + BBBBBB + BBBBBB + BBRBBB + BBBBBB + BBBBBB + BBBBBB ''', '''