teste_docs / document /formatters /table_fit.py
gui-sparim's picture
Mantém todos os exercícios nas tabelas com ajuste automático da fonte
4e531da verified
Raw History Blame Contribute Delete
5.2 kB
"""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