""" Formatadores de tabelas. """ from docx import Document from docx.shared import Pt, Inches, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT from docx.oxml.ns import qn from docx.oxml import OxmlElement from collections.abc import Sequence as SequenceABC from typing import List, Optional, Sequence, Union, Dict, Any from .paragraph import aplicar_cor_run WD_ALIGN_VERTICAL = WD_CELL_VERTICAL_ALIGNMENT def set_cell_shading(cell, color: str) -> None: """ Define a cor de fundo de uma célula. Args: cell: Célula da tabela color: Cor em hex (ex: "E6E6E6") """ shading = OxmlElement('w:shd') shading.set(qn('w:fill'), color) cell._tc.get_or_add_tcPr().append(shading) def set_cell_text_rotation(cell, direcao: str = 'btLr') -> None: """ Define a direção/rotação do texto em uma célula. Args: cell: Célula da tabela direcao: Direção do texto: - 'btLr': bottom-to-top, left-to-right (90° - vertical) - 'tbRl': top-to-bottom, right-to-left (270°) """ tcPr = cell._tc.get_or_add_tcPr() textDirection = OxmlElement('w:textDirection') textDirection.set(qn('w:val'), direcao) tcPr.append(textDirection) def set_cell_no_wrap(cell) -> None: """Evita quebra automática de linha em uma célula.""" tcPr = cell._tc.get_or_add_tcPr() no_wrap = OxmlElement('w:noWrap') no_wrap.set(qn('w:val'), '1') tcPr.append(no_wrap) def formatar_celula_tabela(cell, texto: str, negrito: bool = False, cor=None, shading_color: Optional[str] = None, rotacao: bool = False, no_wrap: bool = False): """ Formata uma célula de tabela com configurações padrão. Args: cell: Célula da tabela texto: Texto a inserir negrito: Se o texto deve ser negrito cor: Cor do texto (tuple ou RGBColor) shading_color: Cor de fundo da célula (hex string) rotacao: Se True, rotaciona o texto 90° (vertical) Returns: Run criado """ p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.left_indent = Cm(0) p.paragraph_format.right_indent = Cm(0) p.paragraph_format.first_line_indent = Cm(0) p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER run = p.add_run(texto) run.font.size = Pt(9) run.font.name = 'Arial' run.bold = negrito if cor: aplicar_cor_run(run, cor) if shading_color: set_cell_shading(cell, shading_color) if rotacao: set_cell_text_rotation(cell) if no_wrap: set_cell_no_wrap(cell) return run def add_simple_table( doc: Document, dados_tabela: List[List[Union[str, Dict[str, Any]]]], header_row: bool = True, largura_colunas: Optional[List[float]] = None, rotacao_cabecalho: Union[bool, Sequence[bool]] = False, largura_total: Optional[int] = None, largura_colunas_absolutas: bool = False, no_quebrar_linha: bool = False, ): """ Adiciona uma tabela simples ao documento. Args: doc: Documento dados_tabela: Lista de listas. Cada célula pode ser string ou dict {'texto', 'cor'} header_row: Se True, primeira linha é cabeçalho (negrito com fundo cinza) largura_colunas: Lista de inteiros representando proporção da largura de cada coluna. rotacao_cabecalho: Se True, rotaciona todos os cabeçalhos 90°. Também aceita uma lista de booleanos, um por coluna. largura_total: Largura total disponível para a tabela. Quando omitida, usa 6,5 polegadas. largura_colunas_absolutas: Se True, largura_colunas contém larguras em EMUs. no_quebrar_linha: Se True, evita quebra automática de linha nas células. Returns: Tabela criada ou None se dados vazios """ if not dados_tabela or not dados_tabela[0]: return None num_colunas = len(dados_tabela[0]) if largura_colunas is None: largura_colunas = [1] * num_colunas elif len(largura_colunas) != num_colunas: raise ValueError("largura_colunas deve ter o mesmo número de colunas de dados_tabela") if isinstance(rotacao_cabecalho, SequenceABC) and not isinstance( rotacao_cabecalho, (str, bytes) ): if len(rotacao_cabecalho) != num_colunas: raise ValueError("rotacao_cabecalho deve ter o mesmo número de colunas de dados_tabela") rotacoes_cabecalho = [bool(valor) for valor in rotacao_cabecalho] else: rotacoes_cabecalho = [bool(rotacao_cabecalho)] * num_colunas if largura_total is None: largura_total = Inches(6.5) table = doc.add_table(rows=len(dados_tabela), cols=num_colunas) table.style = 'Table Grid' table.alignment = WD_TABLE_ALIGNMENT.CENTER table.autofit = False if largura_colunas_absolutas: larguras_em_colunas = [int(largura) for largura in largura_colunas] else: total = sum(largura_colunas) proporcoes = [x / total for x in largura_colunas] larguras_em_colunas = [int(largura_total * p) for p in proporcoes] for j, largura in enumerate(larguras_em_colunas): table.columns[j].width = largura for i, row_data in enumerate(dados_tabela): row = table.rows[i] for j, cell_data in enumerate(row_data): if j >= len(row.cells): continue cell = row.cells[j] if isinstance(cell_data, dict): texto = cell_data.get('texto', '') cor = cell_data.get('cor') else: texto = str(cell_data) if cell_data else "" cor = None is_header = header_row and i == 0 formatar_celula_tabela( cell, texto, negrito=is_header, cor=cor, shading_color="E6E6E6" if is_header else None, rotacao=is_header and rotacoes_cabecalho[j], no_wrap=no_quebrar_linha or is_header, ) cell.width = larguras_em_colunas[j] if header_row and len(dados_tabela) > 1: configurar_linha_cabecalho_repetir(table.rows[0]) # Ajustar altura do cabeçalho para texto rotacionado. if header_row and any(rotacoes_cabecalho) and dados_tabela: header_texts = [] for j, cell_data in enumerate(dados_tabela[0]): if not rotacoes_cabecalho[j]: continue if isinstance(cell_data, dict): header_texts.append(cell_data.get('texto', '')) else: header_texts.append(str(cell_data) if cell_data else "") if header_texts: max_len = max(len(t) for t in header_texts) altura_cm = max(2.8, max_len * 0.25 + 0.9) configurar_linha_tabela_altura(table.rows[0], altura_cm) doc.add_paragraph() return table def configurar_linha_tabela_altura(row, altura_cm: float = 0.6) -> None: """ Configura altura mínima de uma linha de tabela. Args: row: Linha da tabela altura_cm: Altura em centímetros """ tr = row._tr trPr = tr.get_or_add_trPr() trHeight = OxmlElement('w:trHeight') trHeight.set(qn('w:val'), str(int(Cm(altura_cm).twips))) trHeight.set(qn('w:hRule'), 'atLeast') trPr.append(trHeight) def configurar_linha_cabecalho_repetir(row) -> None: """Marca a linha como cabeçalho repetível nas quebras de página.""" tr = row._tr trPr = tr.get_or_add_trPr() tbl_header = OxmlElement('w:tblHeader') tbl_header.set(qn('w:val'), '1') trPr.append(tbl_header) def criar_celula_cabecalho_tabela(cell, texto: str, recuo_primeira_linha_cm: float = 1.0) -> None: """ Formata uma célula de cabeçalho de seção na tabela principal. Args: cell: Célula da tabela texto: Texto do cabeçalho recuo_primeira_linha_cm: Recuo da primeira linha """ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER p = cell.paragraphs[0] p.paragraph_format.left_indent = Cm(0) p.paragraph_format.first_line_indent = Cm(recuo_primeira_linha_cm) p.alignment = WD_ALIGN_PARAGRAPH.LEFT run = p.add_run(texto) run.bold = True run.underline = True run.font.size = Pt(11) run.font.name = 'Arial' set_cell_shading(cell, "E6E6E6") def criar_celula_dados_tabela(cell, texto: str, is_label: bool = False) -> None: """ Formata uma célula de dados na tabela principal. Args: cell: Célula da tabela texto: Texto da célula is_label: Se True, formata como rótulo (negrito) """ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER p = cell.paragraphs[0] p.paragraph_format.left_indent = Cm(0) p.paragraph_format.first_line_indent = Cm(0) p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) run = p.add_run(texto) run.bold = is_label run.font.size = Pt(9) run.font.name = 'Arial'