Repository: Kozea/WeasyPrint

Issue:
Bug with columns
I've noticed that sometimes splitting into columns are incorrect. Here is a way that may reproduce it. You may need it few times to notice a bug.

```python
import random
import string
from weasyprint import HTML

h = f"""
<html><body>"""

text = ''

# prefill
for _ in range(60):
    text += ' ' + random.choice(string.ascii_letters) * random.randint(10,20)

# growing text with different words. Maybe it will create the `bad` one.
for p in range(20):
    word_length = random.randint(10, 15)
    text += ' ' + random.choice(string.ascii_letters) * word_length
    h += "<div style='columns: 2; page-break-before: always; '><p>"
    s = str(p) + ' --- ' + text
    h += s
    h += "</p></div>"
h += "</body></html>"
print(h)
doc = HTML(string=h).render()
doc.write_pdf('two.pdf')

```
Here is an example output with problem.
![image](https://user-images.githubusercontent.com/4893428/229906578-7091d42d-9f7c-451a-82fc-20b13bc9607b.png)
In this case the problem is on pages 9 and 10. There is only one narrow column.

It happen only when text fully fit in one column and reaches page bottom. If there is less or more text, then everything ok.

Also it never happens on first page. Looks like `page-break-before` somehow involved.




Discussion:
Hi!

I’m sorry, but I’ve not been able to reproduce your bug. Do you use the latest version of WeasyPrint? If you do, could you please share a PDF with the problem?
Here they are
[two.html.zip](https://github.com/Kozea/WeasyPrint/files/11183409/two.html.zip)
[two.pdf](https://github.com/Kozea/WeasyPrint/files/11183410/two.pdf)

weasyprint.__version__ is '58.1'

Update. This bug somehow interferes with vertical margins. I could not catch a bug when top+bottom margin is 0. But if I set them high enough, then I see this bug. 
I bet you'll catch a bug here:

```python
from pathlib import Path
from weasyprint import HTML

h = """
<html><head><style>
        @page { 
            size: 100mm 100mm;
            margin: 0mm 0mm 0mm 0mm;
        }
        p {margin-bottom:30mm;
            margint-top: 30mm;
            line-height: 2mm; 
            font-size:2mm;
        }
</style></head><body>"""

k=20
for i in range(k, k+30):
    h += "<div style='columns: 2; page-break-before: always'><p style='background: red'>"
    h += f"{i}aaaaaaaaaaaaaaaaa " * i
    h += "</p></div>"
h += "</body></html>"

doc = HTML(string=h).render()
Path('two.html').write_text(h)
doc.write_pdf('two.pdf')
```

BTW, if I put margins on inner `<p>` then bug appears more often, than if it were on outer `<div>`

[two.pdf](https://github.com/Kozea/WeasyPrint/files/11189632/two.pdf) - in this PDF bug started on 16th page
[two.html.zip](https://github.com/Kozea/WeasyPrint/files/11189639/two.html.zip)


UPDATE: The code above has type error `margint-top`. If i fix this error, but starts on the first page and ends on 17th. 
One more example, where no fonts in use. Try it. And then change margins of `<p>` to zero and try again.
And I was wrong on the first mesage. `page-break-before` does not matter.
```python
from pathlib import Path
from weasyprint import HTML

h = """<html>
<head>
    <style>
        @page {
            size: 100mm 100mm;
            margin: 0mm 0mm 0mm 0mm;
        }
        p {
            margin-bottom: 30mm;
            margin-top: 30mm;
            line-height: 2mm;
            font-size: 2mm;
        }
    </style>
</head>
<body>
<div style='columns: 2'>
    <p style='background: red'><img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
        <img src='https://placehold.co/150x20'/>
    </p>
</div>
</body>
</html>


"""

doc = HTML(string=h).render()
Path('two.html').write_text(h)
doc.write_pdf('two.pdf')

```

[two margins=30mm.pdf](https://github.com/Kozea/WeasyPrint/files/11189694/two.margins.30mm.pdf) - Bug
[two margins=0.pdf](https://github.com/Kozea/WeasyPrint/files/11189693/two.margins.0.pdf) - No bug

Yup... It's all bout margins.
> Yup... It's all bout margins.

Definitely
I think the trick is here:
https://github.com/Kozea/WeasyPrint/blob/15b83090d00c384350aa01b6ca9a3dbfab340cc3/weasyprint/layout/column.py#L91

It says `If it doesn’t fit, we keep the partial rendering`. I think this is wrong. If the whole object does not fit, **but there are more columns available on the right**, we should try to split it into columns.

I tried the following change:

                everything_fits = (
                    not column_skip_stack and
                    max(consumed_heights) <= max_height)

into 

                available_height = max_height * (count - i)
                everything_fits = (
                    not column_skip_stack and
                    max(consumed_heights) <= available_height)

In most simple cases, this works. I cannot see corner cases however.

@luca-vercelli Thanks a lot for your work on this and for your pull request!

> It says `If it doesn’t fit, we keep the partial rendering`. I think this is wrong. If the whole object does not fit, **but there are more columns available on the right**, we should try to split it into columns.

That’s what we already do. The loop above this code already renders the content in `count` columns, so the test used to see if everything fits is good in my opinion: after we rendered all the columns, we check whether the higher column overflows (`max(consumed_heights) <= max_height`). If it doesn’t, we keep the rendering on this page and let the rest for the next page. If it does, we balance.

As explained above, the problem appears only with margins. We can assume that the overall algorithm is correct, and that the problem comes from margins.

To understand what’s going on, your test case is actually great! In your test case, the problem is that everything fits in the first column, but the `consumed_heights` is wrong: the images fit, but not the bottom margin. As when we break pages, that’s OK: we have to ignore the bottom margin even if it overflows. But `consumed_heights` includes this bottom margin, and `everything_fits` is thus `False` when it should be `True`.

The solution is to remove the bottom margin of the last child from consumed heights, so that we can actually know if everything fits. I’ll open a pull request to see if it works!



Patch:
diff --git a/weasyprint/layout/column.py b/weasyprint/layout/column.py
index 691ba8cb8..375141f89 100644
--- a/weasyprint/layout/column.py
+++ b/weasyprint/layout/column.py
@@ -190,6 +190,7 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block,
                         in_flow_children[-1].margin_height() +
                         in_flow_children[-1].position_y - current_position_y)
                     empty_space = height - consumed_height
+                    consumed_height -= in_flow_children[-1].margin_bottom
 
                     # Get the minimum size needed to render the next box
                     if column_skip_stack:


Test patch:
diff --git a/tests/layout/test_column.py b/tests/layout/test_column.py
index 4bee7b57e..c61192ea8 100644
--- a/tests/layout/test_column.py
+++ b/tests/layout/test_column.py
@@ -834,6 +834,30 @@ def test_columns_padding():
     assert [column.position_y for column in columns] == [1, 1, 1]
 
 
+@assert_no_logs
+def test_columns_bottom_margin():
+    page, = render_pages('''
+      <style>
+        div { columns: 2; column-gap: 1px }
+        body { margin: 0; font: 2px / 1 weasyprint }
+        p { margin-bottom: 1em }
+        @page { margin: 0; size: 5px 7px; font-size: 1px }
+      </style>
+      <div><p>a b c</p></div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    column1, column2 = div.children
+    p, = column1.children
+    line1, line2 = p.children
+    assert line1.children[0].text == 'a'
+    assert line2.children[0].text == 'b'
+    p, = column2.children
+    line1, = p.children
+    assert line1.children[0].text == 'c'
+
+
 @assert_no_logs
 def test_columns_relative():
     page, = render_pages('''