Repository: Kozea/WeasyPrint (python)

Issue:
Table overflow issue
This was tested in `WeasyPrint version 62.3` and `WeasyPrint version 63.0`.
This happens when `overflow-wrap` is set to `break-word` or `normal`. `anywhere` seems to work fine.

Input:
```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width initial-scale=1.0">
  <title>Table with Links</title>
  <style>
    table {
      width: 100%;
      max-width: 10px;
      border: 1px solid black;
      overflow-wrap: break-word; /* or normal */
    }

    th td {
      border: 1px solid black;
      padding: 8px;
      text-align: left;
    }

    a {
      margin-right: 8px;
      text-decoration: none;
      color: blue;
    }
  </style>
</head>
<body>
  <h1>Example 1</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">123.456.78</a>,
          <a href="https://example.org">234.567.89</a>,
          <a href="https://example.org">345.678.90</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>Example 2</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">foooooobar</a>,
          <a href="https://example.org">foooooobar</a>,
          <a href="https://example.org">foooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>Example 3</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">S 123.456.78</a>,
          <a href="https://example.org">S 234.567.89</a>,
          <a href="https://example.org">S 345.678.90</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>Example 3</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">S foooooobar</a>,
          <a href="https://example.org">S foooooobar</a>,
          <a href="https://example.org">S foooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
</body>
</html>


```

Weasyprint PDF rendering:
![Image](https://github.com/user-attachments/assets/f41b826d-e3d5-496d-a370-5bbf977bc1a8)

Firefox rendering:
![Image](https://github.com/user-attachments/assets/c0a20b66-adc1-4887-bff3-d79675475049)

Pull request:
Fix table overflow edge cases
Fixes #2325

Not sure this is the best way to do this.

Patch:
diff --git a/.github/workflows/exe.yml b/.github/workflows/exe.yml
index bf39eab8..40b16763 100644
--- a/.github/workflows/exe.yml
+++ b/.github/workflows/exe.yml
@@ -20,7 +20,7 @@ jobs:
       - name: Install requirements
         run: python -m pip install . pyinstaller
       - name: Generate executable
-        run: python -m PyInstaller weasyprint/__main__.py -n weasyprint -F
+        run: python -m PyInstaller weasyprint/__main__.py -n weasyprint -F --hidden-import "fontTools.ttLib.tables.V_A_R_C_"
       - name: Test executable
         run: dist/weasyprint --info
       - name: Store executable
diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py
index e3ab421d..04203187 100644
--- a/weasyprint/layout/preferred.py
+++ b/weasyprint/layout/preferred.py
@@ -13,7 +13,7 @@ from functools import cache
 from math import inf
 
 from ..formatting_structure import boxes
-from ..text.line_break import split_first_line
+from ..text.line_break import can_break_text, split_first_line
 from .replaced import default_image_sizing
 
 
@@ -306,6 +306,7 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
         elif isinstance(child, boxes.TextBox):
             space_collapse = child.style['white_space'] in (
                 'normal', 'nowrap', 'pre-line')
+            text_wrap = child.style['white_space'] in ('normal', 'pre-wrap', 'pre-line')
             if skip_stack is None:
                 skip = 0
             else:
@@ -314,25 +315,26 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
             child_text = child.text.encode()[(skip or 0):]
             if is_line_start and space_collapse:
                 child_text = child_text.lstrip(b' ')
-            if minimum and child_text == b' ':
-                lines = [0, 0]
-            else:
-                max_width = 0 if minimum else None
-                lines = []
-                resume_index = new_resume_index = 0
-                while new_resume_index is not None:
-                    resume_index += new_resume_index
-                    _, _, new_resume_index, width, _, _ = (
-                        split_first_line(
-                            child_text[resume_index:].decode(), child.style,
-                            context, max_width, child.justification_spacing,
-                            is_line_start=is_line_start, minimum=True))
-                    lines.append(width)
-                    if first_line:
-                        break
-                if first_line and new_resume_index:
-                    current_line += lines[0]
+            max_width = 0 if minimum else None
+            lines = []
+            resume_index = new_resume_index = 0
+            while new_resume_index is not None:
+                resume_index += new_resume_index
+                _, _, new_resume_index, width, _, _ = split_first_line(
+                    child_text[resume_index:].decode(), child.style, context, max_width,
+                    child.justification_spacing, is_line_start=is_line_start,
+                    minimum=True)
+                lines.append(width)
+                if first_line:
                     break
+            if first_line and new_resume_index:
+                current_line += lines[0]
+                break
+            # TODO: use the real next character instead of 'a' to detect line breaks.
+            can_break = can_break_text(
+                child_text.decode()[-1:] + 'a', child.style['lang'])
+            if minimum and text_wrap and can_break:
+                lines.append(0)
         else:
             # https://www.w3.org/TR/css-text-3/#overflow-wrap
             # "The line breaking behavior of a replaced element
diff --git a/weasyprint/text/ffi.py b/weasyprint/text/ffi.py
index b5a9cf23..341014b3 100644
--- a/weasyprint/text/ffi.py
+++ b/weasyprint/text/ffi.py
@@ -1,7 +1,6 @@
 """Imports of dynamic libraries used for text layout."""
 
 import os
-import sys
 from contextlib import suppress
 
 import cffi
@@ -435,7 +434,7 @@ def _dlopen(ffi, *names, allow_fail=False):
     return ffi.dlopen(names[0], flags)  # pragma: no cover
 
 
-if hasattr(os, 'add_dll_directory') and not hasattr(sys, 'frozen'):  # pragma: no cover
+if hasattr(os, 'add_dll_directory'):  # pragma: no cover
     dll_directories = os.getenv(
         'WEASYPRINT_DLL_DIRECTORIES',
         'C:\\msys64\\mingw64\\bin;'


Test patch:
diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py
index 721a6e7c..94601dbd 100644
--- a/tests/layout/test_table.py
+++ b/tests/layout/test_table.py
@@ -1558,6 +1558,43 @@ def test_layout_table_auto_51():
     assert abs(td_2.width - 70) < 0.1
 
 
+@assert_no_logs
+def test_layout_table_auto_52():
+    # Test regression:
+    # https://github.com/Kozea/WeasyPrint/issues/2325
+    page, = render_pages('''
+      <style>
+        @page { size: 20px }
+      </style>
+      <table style="font-family: weasyprint; border-spacing: 1px;
+                    font-size: 2px; line-height: 1">
+        <tr>
+          <td><img src=pattern.png></td>
+          <td>
+            <span>foo</span>,
+            <span>foo</span>,
+            <span>foo</span>
+          </td>
+        </tr>
+      </table>
+    ''')
+    html, = page.children
+    body, = html.children
+    table_wrapper, = body.children
+    table, = table_wrapper.children
+    row_group, = table.children
+    row, = row_group.children
+    td_1, td_2 = row.children
+    assert table.width == 20
+    assert table_wrapper.position_x == 0
+    assert table.position_x == 0
+    assert td_1.position_x == 1  # spacing
+    assert td_1.width == 4  # image width
+    assert td_2.position_x == td_1.width + 2 * 1  # 2 * spacing
+    assert td_2.width == table.width - td_1.width - 3 * 1  # 3 * spacing
+    assert td_2.height == 3 * 2  # 3 lines * line height
+
+
 @assert_no_logs
 @pytest.mark.parametrize(
     'body_width, table_width, check_width, positions, widths', (