Spaces:
Sleeping
Sleeping
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
|