File size: 5,197 Bytes
4e531da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""Fit annual tables after their values and footnote markers have been filled."""
from functools import lru_cache
from math import ceil
from pathlib import Path
import re

from PIL import ImageFont
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

from .table import set_table_font_size


@lru_cache(maxsize=2)
def _font(bold=False):
    # The same bundled metrics are available locally and in Spaces. DejaVu Sans
    # is slightly wider than Arial, leaving a margin for Word font substitution.
    from matplotlib import get_data_path
    name = 'DejaVuSans-Bold.ttf' if bold else 'DejaVuSans.ttf'
    return ImageFont.truetype(str(Path(get_data_path()) / 'fonts' / 'ttf' / name), 1000)


@lru_cache(maxsize=8192)
def _width(text, bold=False):
    return _font(bold).getlength(text) / 1000


def _lines(text, width, size, bold=False):
    """Estimate wrapping using glyph widths, preserving explicit line breaks."""
    count = 0
    for line in text.split('\n'):
        used = 0
        count += 1
        for word in re.findall(r'\S+', line):
            length = _width(word, bold) * size
            gap = _width(' ', bold) * size if used else 0
            if used and used + gap + length > width:
                count += 1
                used = 0
                gap = 0
            if length > width:
                extra = max(0, ceil(length / width) - 1)
                count += extra
                used = length - extra * width
            else:
                used += gap + length
    return count


def _margin(cell, side, default):
    margins = cell._tc.get_or_add_tcPr().find(qn('w:tcMar'))
    node = margins.find(qn('w:' + side)) if margins is not None else None
    return int(node.get(qn('w:w'), default)) / 20 if node is not None else default / 20


def fit_table_to_page(doc, table, notes=()):
    """Select the largest half-point size <= 8 that fits the populated table.

    Currency, numbers and year labels must fit on one line. Other text can wrap.
    Height includes cell padding, the caption allowance and 7 pt footnotes. This
    is a conservative estimate: Word remains responsible for final pagination.
    The years always remain columns of the same table, even for longer tables.
    """
    section = doc.sections[-1]
    page_width = (section.page_width - section.left_margin - section.right_margin) / 12700
    page_height = (section.page_height - section.top_margin - section.bottom_margin) / 12700
    note_height = sum(_lines(str(note), page_width, 7) * 8 + 4 for note in notes)
    available_height = max(1, page_height - 30 - note_height)
    rows = []
    for row in table.rows:
        row_props = row._tr.get_or_add_trPr()
        if row_props.find(qn('w:cantSplit')) is None:
            row_props.append(OxmlElement('w:cantSplit'))
        cells = []
        for cell in row.cells:
            props = cell._tc.get_or_add_tcPr()
            for node in list(props.findall(qn('w:noWrap'))):
                props.remove(node)
            width = max(1, cell.width / 12700 - _margin(cell, 'left', 80) - _margin(cell, 'right', 80) - 2)
            paragraphs = []
            for paragraph in cell.paragraphs:
                marker_count = len(paragraph._p.findall('.//' + qn('w:footnoteReference')))
                text = paragraph.text + '1' * marker_count
                bold = any(run.bold for run in paragraph.runs)
                paragraphs.append((text, bold, paragraph))
            cells.append((width, _margin(cell, 'top', 65) + _margin(cell, 'bottom', 65), paragraphs))
        rows.append(cells)

    def fits(size):
        height = 0
        for cells in rows:
            row_height = 0
            for width, padding, paragraphs in cells:
                cell_height = padding
                for text, bold, paragraph in paragraphs:
                    # Do not split an amount, a date, a coefficient or IPTU year
                    # into fragments to make the width calculation succeed.
                    single_line = bool(re.fullmatch(r'(?:R\$\s*)?[−+\-]?\d[\d.,%/\s]*|IPTU\s+\d{4}', text.strip()))
                    if single_line and _width(text, bold) * size > width:
                        return False
                    cell_height += _lines(text, width, size, bold) * size * 1.15
                    cell_height += (paragraph.paragraph_format.space_before or 0) / 12700
                    cell_height += (paragraph.paragraph_format.space_after or 0) / 12700
                row_height = max(row_height, cell_height)
            height += row_height
        return height <= available_height

    # w:sz stores integer half-points: smaller steps would be rounded away.
    size = 8
    while size > 0.5 and not fits(size):
        size -= 0.5
    set_table_font_size(table, size)
    keep_together = fits(size)
    for row_index, row in enumerate(table.rows):
        for cell in row.cells:
            for paragraph in cell.paragraphs:
                paragraph.paragraph_format.line_spacing = 1.05
                paragraph.paragraph_format.keep_with_next = keep_together and row_index < len(table.rows) - 1
                paragraph.paragraph_format.keep_together = True
    return size