gui-sparim commited on
Commit
3051b24
·
verified ·
1 Parent(s): 2370bf0

Upload docx_formatters.py

Browse files
Files changed (1) hide show
  1. docx_formatters.py +482 -0
docx_formatters.py ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ docx_formatters.py
3
+
4
+ Funções auxiliares para formatação de documentos DOCX.
5
+ Não contém conteúdo/texto - apenas lógica de formatação.
6
+ """
7
+
8
+ from docx.shared import Pt, Inches, Cm, RGBColor
9
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
10
+ from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
11
+ from docx.oxml.ns import qn
12
+ from docx.oxml import OxmlElement
13
+
14
+ WD_ALIGN_VERTICAL = WD_CELL_VERTICAL_ALIGNMENT
15
+
16
+
17
+ # ============================================================================
18
+ # NUMERAÇÃO AUTOMÁTICA DE SEÇÕES
19
+ # ============================================================================
20
+
21
+ class NumeradorSecoes:
22
+ """
23
+ Gerencia numeração automática hierárquica de seções.
24
+
25
+ Uso:
26
+ num = NumeradorSecoes()
27
+ num.secao("SOLICITAÇÃO") # 1. SOLICITAÇÃO
28
+ num.subsecao("Considerações") # 1.1 Considerações
29
+ num.subsecao("Documentação") # 1.2 Documentação
30
+ num.secao("IMÓVEL") # 2. IMÓVEL
31
+ num.subsecao("Descrição") # 2.1 Descrição
32
+ num.subsubsecao("Detalhe") # 2.1.1 Detalhe
33
+ """
34
+
35
+ def __init__(self, secao_inicial=0):
36
+ """
37
+ Args:
38
+ secao_inicial: Número inicial para seções (0 = começa em 1)
39
+ """
40
+ self.contadores = [secao_inicial, 0, 0, 0] # [secao, subsecao, subsubsecao, subsubsubsecao]
41
+
42
+ def _reset_niveis_abaixo(self, nivel):
43
+ """Reseta contadores de níveis abaixo do especificado."""
44
+ for i in range(nivel + 1, len(self.contadores)):
45
+ self.contadores[i] = 0
46
+
47
+ def secao(self):
48
+ """Incrementa e retorna número da seção (ex: '1')."""
49
+ self.contadores[0] += 1
50
+ self._reset_niveis_abaixo(0)
51
+ return str(self.contadores[0])
52
+
53
+ def subsecao(self):
54
+ """Incrementa e retorna número da subseção (ex: '1.1')."""
55
+ self.contadores[1] += 1
56
+ self._reset_niveis_abaixo(1)
57
+ return f"{self.contadores[0]}.{self.contadores[1]}"
58
+
59
+ def subsubsecao(self):
60
+ """Incrementa e retorna número da sub-subseção (ex: '1.1.1')."""
61
+ self.contadores[2] += 1
62
+ self._reset_niveis_abaixo(2)
63
+ return f"{self.contadores[0]}.{self.contadores[1]}.{self.contadores[2]}"
64
+
65
+ def subsubsubsecao(self):
66
+ """Incrementa e retorna número da sub-sub-subseção (ex: '1.1.1.1')."""
67
+ self.contadores[3] += 1
68
+ return f"{self.contadores[0]}.{self.contadores[1]}.{self.contadores[2]}.{self.contadores[3]}"
69
+
70
+ def numero_atual(self, nivel=0):
71
+ """Retorna o número atual de um nível sem incrementar."""
72
+ if nivel == 0:
73
+ return str(self.contadores[0])
74
+ elif nivel == 1:
75
+ return f"{self.contadores[0]}.{self.contadores[1]}"
76
+ elif nivel == 2:
77
+ return f"{self.contadores[0]}.{self.contadores[1]}.{self.contadores[2]}"
78
+ else:
79
+ return f"{self.contadores[0]}.{self.contadores[1]}.{self.contadores[2]}.{self.contadores[3]}"
80
+
81
+ def definir_secao(self, numero):
82
+ """Define manualmente o número da seção atual."""
83
+ self.contadores[0] = numero
84
+ self._reset_niveis_abaixo(0)
85
+
86
+
87
+ # ============================================================================
88
+ # HELPERS BÁSICOS
89
+ # ============================================================================
90
+
91
+ def aplicar_cor_run(run, cor):
92
+ """
93
+ Aplica cor RGB a um run.
94
+
95
+ Args:
96
+ run: Run do python-docx
97
+ cor: tuple (r,g,b) ou RGBColor
98
+ """
99
+ if cor:
100
+ if isinstance(cor, tuple):
101
+ run.font.color.rgb = RGBColor(cor[0], cor[1], cor[2])
102
+ else:
103
+ run.font.color.rgb = cor
104
+
105
+
106
+ def set_cell_shading(cell, color):
107
+ """
108
+ Define a cor de fundo de uma célula.
109
+
110
+ Args:
111
+ cell: Célula da tabela
112
+ color: Cor em hex (ex: "E6E6E6")
113
+ """
114
+ shading = OxmlElement('w:shd')
115
+ shading.set(qn('w:fill'), color)
116
+ cell._tc.get_or_add_tcPr().append(shading)
117
+
118
+
119
+ # ============================================================================
120
+ # CRIAÇÃO DE PARÁGRAFOS
121
+ # ============================================================================
122
+
123
+ def criar_paragrafo_formatado(doc, texto, negrito=False, sublinhado=False, italico=False,
124
+ tamanho=11, cor=None, alinhamento=WD_ALIGN_PARAGRAPH.JUSTIFY,
125
+ espaco_antes=0, espaco_depois=6, recuo_esq=0,
126
+ recuo_primeira_linha=0, fonte='Arial'):
127
+ """
128
+ Cria um parágrafo com formatação completa.
129
+
130
+ Args:
131
+ doc: Documento python-docx
132
+ texto: Texto do parágrafo
133
+ negrito, sublinhado, italico: Formatação do texto
134
+ tamanho: Tamanho da fonte em pontos
135
+ cor: RGBColor ou tuple (r,g,b)
136
+ alinhamento: WD_ALIGN_PARAGRAPH constant
137
+ espaco_antes, espaco_depois: Espaçamento em pontos
138
+ recuo_esq: Recuo esquerdo em inches
139
+ recuo_primeira_linha: Recuo da primeira linha em cm
140
+ fonte: Nome da fonte
141
+
142
+ Returns:
143
+ Parágrafo criado
144
+ """
145
+ p = doc.add_paragraph()
146
+ run = p.add_run(texto)
147
+
148
+ run.bold = negrito
149
+ run.underline = sublinhado
150
+ run.italic = italico
151
+ run.font.size = Pt(tamanho)
152
+ run.font.name = fonte
153
+ aplicar_cor_run(run, cor)
154
+
155
+ p.alignment = alinhamento
156
+ p.paragraph_format.space_before = Pt(espaco_antes)
157
+ p.paragraph_format.space_after = Pt(espaco_depois)
158
+ if recuo_esq:
159
+ p.paragraph_format.left_indent = Inches(recuo_esq)
160
+ if recuo_primeira_linha:
161
+ p.paragraph_format.first_line_indent = Cm(recuo_primeira_linha)
162
+
163
+ return p
164
+
165
+
166
+ # ============================================================================
167
+ # TÍTULOS E SEÇÕES
168
+ # ============================================================================
169
+
170
+ def add_heading_custom(doc, text, level=1):
171
+ """Adiciona título customizado."""
172
+ configs = {
173
+ 1: {'tamanho': 14, 'antes': 18, 'depois': 12},
174
+ 2: {'tamanho': 12, 'antes': 12, 'depois': 6},
175
+ }
176
+ cfg = configs.get(level, {'tamanho': 11, 'antes': 6, 'depois': 6})
177
+
178
+ return criar_paragrafo_formatado(
179
+ doc, text, negrito=True, sublinhado=True, tamanho=cfg['tamanho'],
180
+ espaco_antes=cfg['antes'], espaco_depois=cfg['depois'],
181
+ alinhamento=WD_ALIGN_PARAGRAPH.LEFT
182
+ )
183
+
184
+
185
+ def add_section_title(doc, number, text):
186
+ """Adiciona título de seção principal (ex: 1. SOLICITAÇÃO)."""
187
+ return criar_paragrafo_formatado(
188
+ doc, f"{number}. {text}", negrito=True, tamanho=12,
189
+ espaco_antes=18, espaco_depois=12, alinhamento=WD_ALIGN_PARAGRAPH.LEFT
190
+ )
191
+
192
+
193
+ def add_subsection_title(doc, number, text):
194
+ """Adiciona título de subseção (ex: 1.1 Considerações)."""
195
+ return criar_paragrafo_formatado(
196
+ doc, f"{number} {text}", negrito=True, tamanho=11,
197
+ espaco_antes=12, espaco_depois=6, recuo_esq=0.3,
198
+ alinhamento=WD_ALIGN_PARAGRAPH.LEFT
199
+ )
200
+
201
+
202
+ def add_subsubsection_title(doc, number, text):
203
+ """Adiciona título de sub-subseção (ex: 3.2.1)."""
204
+ return criar_paragrafo_formatado(
205
+ doc, f"{number} {text}", negrito=True, tamanho=11,
206
+ espaco_antes=10, espaco_depois=4, recuo_esq=0.5,
207
+ alinhamento=WD_ALIGN_PARAGRAPH.LEFT
208
+ )
209
+
210
+
211
+ def add_subsubsubsection_title(doc, number, text):
212
+ """Adiciona título de sub-sub-subseção (ex: 3.2.1.1)."""
213
+ return criar_paragrafo_formatado(
214
+ doc, f"{number} {text}", negrito=True, tamanho=10,
215
+ espaco_antes=8, espaco_depois=4, recuo_esq=0.7,
216
+ alinhamento=WD_ALIGN_PARAGRAPH.LEFT
217
+ )
218
+
219
+
220
+ # ============================================================================
221
+ # TEXTO DO CORPO
222
+ # ============================================================================
223
+
224
+ def add_body_text(doc, text, cor=None, indent=True):
225
+ """Adiciona texto do corpo com formatação padrão."""
226
+ return criar_paragrafo_formatado(
227
+ doc, text, tamanho=11, cor=cor,
228
+ espaco_depois=6, recuo_esq=0.3 if indent else 0,
229
+ recuo_primeira_linha=1.25 if indent else 0
230
+ )
231
+
232
+
233
+ def add_placeholder_text(doc, text="[Preencher informações]", indent=True):
234
+ """Adiciona texto placeholder em vermelho."""
235
+ return criar_paragrafo_formatado(
236
+ doc, text, tamanho=11, cor=RGBColor(255, 0, 0),
237
+ espaco_depois=6, recuo_esq=0.3 if indent else 0,
238
+ recuo_primeira_linha=1.25 if indent else 0
239
+ )
240
+
241
+
242
+ def add_bullet_text(doc, text, indent_level=1):
243
+ """Adiciona texto com bullet/recuo."""
244
+ return criar_paragrafo_formatado(
245
+ doc, f"• {text}", tamanho=11,
246
+ espaco_depois=3, recuo_esq=0.3 + (indent_level * 0.2)
247
+ )
248
+
249
+
250
+ # ============================================================================
251
+ # TABELAS
252
+ # ============================================================================
253
+
254
+ def formatar_celula_tabela(cell, texto, negrito=False, cor=None, shading_color=None):
255
+ """
256
+ Formata uma célula de tabela com configurações padrão.
257
+
258
+ Args:
259
+ cell: Célula da tabela
260
+ texto: Texto a inserir
261
+ negrito: Se o texto deve ser negrito
262
+ cor: Cor do texto (tuple ou RGBColor)
263
+ shading_color: Cor de fundo da célula (hex string)
264
+
265
+ Returns:
266
+ Run criado
267
+ """
268
+ p = cell.paragraphs[0]
269
+
270
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
271
+ p.paragraph_format.left_indent = Cm(0)
272
+ p.paragraph_format.right_indent = Cm(0)
273
+ p.paragraph_format.first_line_indent = Cm(0)
274
+ p.paragraph_format.space_before = Pt(0)
275
+ p.paragraph_format.space_after = Pt(0)
276
+
277
+ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
278
+
279
+ run = p.add_run(texto)
280
+ run.font.size = Pt(9)
281
+ run.font.name = 'Arial'
282
+ run.bold = negrito
283
+
284
+ if cor:
285
+ aplicar_cor_run(run, cor)
286
+
287
+ if shading_color:
288
+ set_cell_shading(cell, shading_color)
289
+
290
+ return run
291
+
292
+ from docx.shared import Inches
293
+ from docx.enum.table import WD_TABLE_ALIGNMENT
294
+
295
+ def add_simple_table(doc, dados_tabela, header_row=True, largura_colunas=None):
296
+ """
297
+ Adiciona uma tabela simples ao documento.
298
+
299
+ Args:
300
+ doc: Documento
301
+ dados_tabela: Lista de listas. Cada célula pode ser string ou dict {'texto', 'cor'}
302
+ header_row: Se True, primeira linha é cabeçalho (negrito com fundo cinza)
303
+ largura_colunas: Lista de inteiros representando proporção da largura de cada coluna.
304
+ Por padrão, todas iguais (1).
305
+
306
+ Returns:
307
+ Tabela criada ou None se dados vazios
308
+ """
309
+ if not dados_tabela or not dados_tabela[0]:
310
+ return None
311
+
312
+ num_colunas = len(dados_tabela[0])
313
+
314
+ # Se não passou largura_colunas, define 1 para todas
315
+ if largura_colunas is None:
316
+ largura_colunas = [1] * num_colunas
317
+ elif len(largura_colunas) != num_colunas:
318
+ raise ValueError("largura_colunas deve ter o mesmo número de colunas de dados_tabela")
319
+
320
+ # Normaliza a largura relativa
321
+ total = sum(largura_colunas)
322
+ proporcoes = [x / total for x in largura_colunas]
323
+
324
+ # Cria tabela
325
+ table = doc.add_table(rows=len(dados_tabela), cols=num_colunas)
326
+ table.style = 'Table Grid'
327
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
328
+
329
+ # Determinar largura total da tabela (aprox. 6.5 polegadas)
330
+ largura_total = Inches(6.5)
331
+ larguras_em_colunas = [largura_total * p for p in proporcoes]
332
+
333
+ for i, row_data in enumerate(dados_tabela):
334
+ row = table.rows[i]
335
+ for j, cell_data in enumerate(row_data):
336
+ if j >= len(row.cells):
337
+ continue
338
+
339
+ cell = row.cells[j]
340
+
341
+ if isinstance(cell_data, dict):
342
+ texto = cell_data.get('texto', '')
343
+ cor = cell_data.get('cor')
344
+ else:
345
+ texto = str(cell_data) if cell_data else ""
346
+ cor = None
347
+
348
+ is_header = header_row and i == 0
349
+
350
+ # Formata a célula (sua função existente)
351
+ formatar_celula_tabela(
352
+ cell, texto,
353
+ negrito=is_header,
354
+ cor=cor,
355
+ shading_color="E6E6E6" if is_header else None
356
+ )
357
+
358
+ # Define largura da coluna
359
+ cell.width = larguras_em_colunas[j]
360
+
361
+ doc.add_paragraph()
362
+ return table
363
+
364
+ def configurar_linha_tabela_altura(row, altura_cm=0.6):
365
+ """
366
+ Configura altura mínima de uma linha de tabela.
367
+
368
+ Args:
369
+ row: Linha da tabela
370
+ altura_cm: Altura em centímetros
371
+ """
372
+ tr = row._tr
373
+ trPr = tr.get_or_add_trPr()
374
+ trHeight = OxmlElement('w:trHeight')
375
+ trHeight.set(qn('w:val'), str(int(Cm(altura_cm).twips)))
376
+ trHeight.set(qn('w:hRule'), 'atLeast')
377
+ trPr.append(trHeight)
378
+
379
+
380
+ def criar_celula_cabecalho_tabela(cell, texto, recuo_primeira_linha_cm=1.0):
381
+ """
382
+ Formata uma célula de cabeçalho de seção na tabela principal.
383
+
384
+ Args:
385
+ cell: Célula da tabela
386
+ texto: Texto do cabeçalho
387
+ recuo_primeira_linha_cm: Recuo da primeira linha
388
+ """
389
+ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
390
+
391
+ p = cell.paragraphs[0]
392
+ p.paragraph_format.left_indent = Cm(0)
393
+ p.paragraph_format.first_line_indent = Cm(recuo_primeira_linha_cm)
394
+ p.alignment = WD_ALIGN_PARAGRAPH.LEFT
395
+
396
+ run = p.add_run(texto)
397
+ run.bold = True
398
+ run.underline = True
399
+ run.font.size = Pt(11)
400
+ run.font.name = 'Arial'
401
+
402
+ set_cell_shading(cell, "E6E6E6")
403
+
404
+
405
+ def criar_celula_dados_tabela(cell, texto, is_label=False):
406
+ """
407
+ Formata uma célula de dados na tabela principal.
408
+
409
+ Args:
410
+ cell: Célula da tabela
411
+ texto: Texto da célula
412
+ is_label: Se True, formata como rótulo (negrito)
413
+ """
414
+ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
415
+
416
+ p = cell.paragraphs[0]
417
+ p.paragraph_format.left_indent = Cm(0)
418
+ p.paragraph_format.first_line_indent = Cm(0)
419
+ p.paragraph_format.space_before = Pt(0)
420
+ p.paragraph_format.space_after = Pt(0)
421
+
422
+ run = p.add_run(texto)
423
+ run.bold = is_label
424
+ run.font.size = Pt(9)
425
+ run.font.name = 'Arial'
426
+
427
+
428
+ # ============================================================================
429
+ # IMAGENS
430
+ # ============================================================================
431
+
432
+ def inserir_imagem_de_documento(doc, imagem_data, largura_inches=5.5):
433
+ """
434
+ Insere uma imagem extraída de outro documento.
435
+
436
+ Args:
437
+ doc: Documento destino
438
+ imagem_data: Dict com 'paragraph_element' e 'doc' (documento origem)
439
+ largura_inches: Largura da imagem
440
+
441
+ Returns:
442
+ True se inseriu com sucesso, False caso contrário
443
+ """
444
+ if not imagem_data:
445
+ return False
446
+
447
+ try:
448
+ from io import BytesIO
449
+
450
+ para_elem = imagem_data['paragraph_element']
451
+ doc_origem = imagem_data['doc']
452
+
453
+ ns_a = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
454
+ ns_r = '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}'
455
+
456
+ for blip in para_elem.iter(f'{ns_a}blip'):
457
+ embed_id = blip.get(f'{ns_r}embed')
458
+ if embed_id:
459
+ try:
460
+ image_part = doc_origem.part.related_parts[embed_id]
461
+ image_stream = BytesIO(image_part.blob)
462
+
463
+ p = doc.add_paragraph()
464
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
465
+ run = p.add_run()
466
+ run.add_picture(image_stream, width=Inches(largura_inches))
467
+ return True
468
+ except Exception as e:
469
+ print(f"Erro ao copiar imagem: {e}")
470
+ return False
471
+ except Exception as e:
472
+ print(f"Erro ao processar imagem: {e}")
473
+ return False
474
+
475
+ return False
476
+
477
+
478
+ def add_image_placeholder(doc):
479
+ """Adiciona placeholder de imagem."""
480
+ p = doc.add_paragraph()
481
+ p.add_run("[IMAGEM]").italic = True
482
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER