import os import sys import tkinter as tk from tkinter import ttk from datetime import datetime from docx import Document from tkinter import filedialog from PIL import Image import tempfile from docx.shared import Inches, Pt, RGBColor import io TEMPLATE_DOCX = "Corpus.docx" def resource_path(relative_path: str) -> str: try: base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) def image_to_png_stream(image_path: str) -> io.BytesIO: with Image.open(image_path) as img: if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") bio = io.BytesIO() img.save(bio, format="PNG") bio.seek(0) return bio def insert_image_at_placeholder(doc: Document, placeholder: str, image_path: str, width_inches: float = 6.20) -> bool: if not image_path or not os.path.exists(image_path): return False try: img_stream = image_to_png_stream(image_path) except Exception as e: raise ValueError(f"Não consegui abrir/convert a imagem:\n{image_path}\n\nDetalhes:\n{e}") def replace_in_paragraph(p) -> bool: full_text = "".join(run.text for run in p.runs) if placeholder not in full_text: return False for r in p.runs: r.text = "" run = p.add_run() run.add_picture(img_stream, width=Inches(width_inches)) return True for p in doc.paragraphs: if replace_in_paragraph(p): return True for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: if replace_in_paragraph(p): return True for section in doc.sections: for p in section.header.paragraphs: if replace_in_paragraph(p): return True for p in section.footer.paragraphs: if replace_in_paragraph(p): return True return False def insert_images_at_placeholders(doc: Document, placeholders: dict, width_inches: float = 6.20) -> list: missing = [] for placeholder, image_path in placeholders.items(): if not image_path: continue ok = insert_image_at_placeholder(doc, placeholder, image_path, width_inches=width_inches) if not ok: missing.append(placeholder) return missing def insert_tecnicas_utilizadas(doc: Document, placeholder: str, tecnicas: list) -> bool: def replace_in_paragraph(p) -> bool: full_text = "".join(run.text for run in p.runs) if placeholder not in full_text: return False for r in p.runs: r.text = "" first_block = True for t in tecnicas: if not t.get("selected"): continue nome = (t.get("name") or "").strip() desc = (t.get("desc") or "").strip() fotos = t.get("photos") or [] if not nome: continue if not first_block: p.add_run().add_break() p.add_run().add_break() run_title = p.add_run(nome) run_title.font.name = "Verdana" run_title.font.size = Pt(11) run_title.font.color.rgb = RGBColor(0x4A, 0x66, 0xAC) p.add_run().add_break() if desc: run_desc = p.add_run(desc) run_desc.font.name = "Verdana" run_desc.font.size = Pt(10) run_desc.font.color.rgb = RGBColor(0x66, 0x66, 0x66) p.add_run().add_break() if fotos: for i, image_path in enumerate(fotos): if not image_path or not os.path.exists(image_path): continue try: img_stream = image_to_png_stream(image_path) except Exception as e: raise ValueError(f"Não consegui abrir/convert a imagem:\n{image_path}\n\nDetalhes:\n{e}") run_img = p.add_run() run_img.add_picture(img_stream, width=Inches(2.80)) is_last = i == len(fotos) - 1 if not is_last: if i % 2 == 0: p.add_run(" ") else: p.add_run().add_break() p.add_run().add_break() first_block = False return True for p in doc.paragraphs: if replace_in_paragraph(p): return True for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: if replace_in_paragraph(p): return True for section in doc.sections: for p in section.header.paragraphs: if replace_in_paragraph(p): return True for p in section.footer.paragraphs: if replace_in_paragraph(p): return True return False def insert_metodos_utilizados(doc: Document, placeholder: str, values: dict) -> bool: items = [] seen = set() for group_key in ("inicio_pesquisa", "tecnicas"): for t in values.get(group_key) or []: if not t.get("selected"): continue name = (t.get("name") or "").strip() if not name or name in seen: continue seen.add(name) items.append(name) if not items: return False def replace_in_paragraph(p) -> bool: full_text = "".join(run.text for run in p.runs) if placeholder not in full_text: return False for r in p.runs: r.text = "" for i, name in enumerate(items): run = p.add_run(f"- {name}") run.font.name = "Verdana" run.font.size = Pt(10) run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) if i < len(items) - 1: p.add_run().add_break() return True for p in doc.paragraphs: if replace_in_paragraph(p): return True for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: if replace_in_paragraph(p): return True for section in doc.sections: for p in section.header.paragraphs: if replace_in_paragraph(p): return True for p in section.footer.paragraphs: if replace_in_paragraph(p): return True return False def insert_multiple_images_at_placeholder(doc: Document, placeholder: str, image_paths: list, width_inches: float = 2.80) -> bool: if not image_paths: return False def replace_in_paragraph(p) -> bool: full_text = "".join(run.text for run in p.runs) if placeholder not in full_text: return False for r in p.runs: r.text = "" for i, image_path in enumerate(image_paths): if not image_path or not os.path.exists(image_path): continue try: img_stream = image_to_png_stream(image_path) except Exception as e: raise ValueError(f"Não consegui abrir/convert a imagem:\n{image_path}\n\nDetalhes:\n{e}") run = p.add_run() run.add_picture(img_stream, width=Inches(width_inches)) is_last = i == len(image_paths) - 1 if not is_last: if i % 2 == 0: p.add_run(" ") else: p.add_run().add_break() return True for p in doc.paragraphs: if replace_in_paragraph(p): return True for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: if replace_in_paragraph(p): return True for section in doc.sections: for p in section.header.paragraphs: if replace_in_paragraph(p): return True for p in section.footer.paragraphs: if replace_in_paragraph(p): return True return False def fatal_popup_and_exit(title: str, msg: str, exit_code: int = 1): win = tk.Tk() win.title(title) win.geometry("520x260") win.resizable(False, False) frm = ttk.Frame(win, padding=14) frm.pack(fill="both", expand=True) ttk.Label(frm, text=title, font=("Segoe UI", 12, "bold")).pack(anchor="w", pady=(0, 8)) txt = tk.Text(frm, height=8, wrap="word") txt.pack(fill="both", expand=True) txt.insert("1.0", msg) txt.config(state="disabled") def close_and_exit(): win.destroy() raise SystemExit(exit_code) ttk.Button(frm, text="OK", command=close_and_exit).pack(anchor="e", pady=(10, 0)) win.protocol("WM_DELETE_WINDOW", close_and_exit) win.mainloop() def info_popup(title: str, msg: str): win = tk.Toplevel() win.title(title) win.geometry("520x240") win.resizable(False, False) frm = ttk.Frame(win, padding=14) frm.pack(fill="both", expand=True) ttk.Label(frm, text=title, font=("Segoe UI", 12, "bold")).pack(anchor="w", pady=(0, 8)) txt = tk.Text(frm, height=7, wrap="word") txt.pack(fill="both", expand=True) txt.insert("1.0", msg) txt.config(state="disabled") def close(): win.destroy() ttk.Button(frm, text="OK", command=close).pack(anchor="e", pady=(10, 0)) win.grab_set() win.focus_force() def replace_placeholders_in_doc(doc: Document, mapping: dict) -> None: for p in doc.paragraphs: text = p.text new_text = text for k, v in mapping.items(): new_text = new_text.replace(k, v) if new_text != text: p.text = new_text for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: text = p.text new_text = text for k, v in mapping.items(): new_text = new_text.replace(k, v) if new_text != text: p.text = new_text def generate_output_filename() -> str: stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return f"{stamp}.docx" def build_presence_text(values: dict) -> str: parts = [] present_people = [] presencas = values.get("presencas") or [] if not presencas: if values.get("segurado_presente"): presencas.append( { "tipo": "segurado", "nome": values.get("segurado_nome"), "info": values.get("segurado_info"), } ) if values.get("lesado_presente"): presencas.append( { "tipo": "lesado", "nome": values.get("lesado_nome"), "info": values.get("lesado_info"), } ) if values.get("outro_presente"): presencas.append( { "tipo": "outro", "nome": values.get("outro_nome"), "info": values.get("outro_info"), } ) def join_people(items: list) -> str: if not items: return "" if len(items) == 1: return items[0] return ", ".join(items[:-1]) + " e " + items[-1] for item in presencas: tipo = (item.get("tipo") or "").strip().lower() nome = (item.get("nome") or "").strip() info = (item.get("info") or "").strip() if not nome: continue if tipo == "segurado": present_people.append(f"o segurado, {nome}") if info: parts.append(f"O segurado, {nome} informou que {info}.") elif tipo == "lesado": present_people.append(f"o lesado, {nome}") if info: parts.append(f"O lesado, {nome} informou que {info}.") else: present_people.append(nome) if info: parts.append(f"{nome} informou que {info}.") header = "" if present_people: joined = join_people(present_people) if len(present_people) == 1: header = f"{joined} esteve presente aquando a pesquisa." else: header = f"{joined} estiveram presentes aquando a pesquisa." if header and parts: return header + "\n\n" + "\n".join(parts) if header: return header if parts: return "\n".join(parts) return "" def run_generation(values: dict, root: tk.Tk): mapping = { "{{nproc}}": (values.get("nproc") or "").strip(), "{{segurado}}": (values.get("segurado") or "").strip(), "{{comp}}": (values.get("comp") or "").strip(), "{{terceiro}}": (values.get("terceiro") or "").strip(), "{{data}}": (values.get("data") or "").strip(), "{{local da visita}}": (values.get("local") or "").strip(), "{{descriçãoimovel}}": (values.get("descriçãoimovel") or "").strip(), "{{anoconstr}}": (values.get("anoconstr") or "").strip(), "{{presencas}}": build_presence_text(values), "{{descricao}}": (values.get("descricao") or "").strip(), "{{area}}": (values.get("area") or "").strip(), } template_path = resource_path(TEMPLATE_DOCX) if not os.path.exists(template_path): try: root.destroy() except Exception: pass fatal_popup_and_exit( "Erro: template não encontrado", f"Não encontrei o template '{TEMPLATE_DOCX}'.\n\n" f"Coloca o ficheiro '{TEMPLATE_DOCX}' na mesma pasta do executável e volta a executar." ) return output_docx = generate_output_filename() try: doc = Document(template_path) replace_placeholders_in_doc(doc, mapping) foto1_path = (values.get("foto1_path") or "").strip() fotos2_paths = values.get("fotos2_paths") or [] placeholders = { "{{foto1}}": foto1_path, } missing = insert_images_at_placeholders(doc, placeholders, width_inches=6.20) if missing: info_popup( "Aviso", "Não encontrei os placeholders no template (corpo/cabeçalho/rodapé):\n" + ", ".join(missing) ) if fotos2_paths: ok_multi = insert_multiple_images_at_placeholder(doc, "{{foto2}}", fotos2_paths, width_inches=2.80) if not ok_multi: info_popup( "Aviso", "Não encontrei o placeholder {{foto2}} no template (corpo/cabeçalho/rodapé)." ) tecnicas = values.get("tecnicas") or [] if tecnicas: ok_tecnicas = insert_tecnicas_utilizadas(doc, "{{tecnicasutilizadas}}", tecnicas) if not ok_tecnicas: info_popup( "Aviso", "Não encontrei o placeholder {{tecnicasutilizadas}} no template (corpo/cabeçalho/rodapé)." ) inicio_pesquisa = values.get("inicio_pesquisa") or [] if inicio_pesquisa: ok_inicio = insert_tecnicas_utilizadas(doc, "{{iniciopesquisa}}", inicio_pesquisa) if not ok_inicio: info_popup( "Aviso", "Não encontrei o placeholder {{iniciopesquisa}} no template (corpo/cabeçalho/rodapé)." ) if values.get("inicio_pesquisa") or values.get("tecnicas"): ok_metodos = insert_metodos_utilizados(doc, "{{metodosutilizados}}", values) if not ok_metodos: info_popup( "Aviso", "Não encontrei o placeholder {{metodosutilizados}} no template (corpo/cabeçalho/rodapé)." ) doc.save(output_docx) info_popup("Sucesso", f"Documento gerado com sucesso:\n\n{output_docx}") except Exception as e: try: root.destroy() except Exception: pass fatal_popup_and_exit( "Erro ao gerar documento", f"Falhou ao gerar o documento.\n\nDetalhes:\n{e}" ) def choose_image(foto_var): path = filedialog.askopenfilename( title="Selecionar fotografia", filetypes=[ ("Imagens", "*.png *.jpg *.jpeg *.bmp"), ("Todos os ficheiros", "*.*"), ], ) if path: foto_var.set(path) def choose_images(fotos_var, label_var): paths = filedialog.askopenfilenames( title="Selecionar fotografias", filetypes=[ ("Imagens", "*.png *.jpg *.jpeg *.bmp"), ("Todos os ficheiros", "*.*"), ], ) if paths: fotos_var.set(list(paths)) label_var.set(f"{len(paths)} imagem(ns)") def main(): root = tk.Tk() root.title("Preencher Corpus (Word)") root.geometry("1120x640") root.resizable(False, False) frm = ttk.Frame(root, padding=16) frm.pack(fill="both", expand=True) fields = [("Nº do Processo:", "nproc"),("Segurado:", "segurado"), ("Companhia:", "comp"), ("Terceiro:", "terceiro"), ("Data da visita:", "data"), ("Local da visita:", "local"), ("Descrição do imóvel:", "descriçãoimovel"), ("Ano de Construção:", "anoconstr"), ("Descrição da pesquisa:", "descricao"), ("Área do do espaço:", "area") ] entries = {} foto_var = tk.StringVar(value="") fotos2_var = tk.Variable(value=[]) fotos2_label = tk.StringVar(value="") tecnicas_data = [] inicio_data = [] for i, (label, key) in enumerate(fields): ttk.Label(frm, text=label).grid(row=i, column=0, sticky="w", pady=6) ent = ttk.Entry(frm, width=80) ent.grid(row=i, column=1, sticky="w", pady=6) entries[key] = ent if key == "descriçãoimovel": ttk.Button( frm, text="Escolher imagem…", command=lambda: choose_image(foto_var) ).grid(row=i, column=2, sticky="w", padx=8) ttk.Label( frm, textvariable=foto_var, width=28 ).grid(row=i, column=4, sticky="w") if key == "descricao": ttk.Button( frm, text="Escolher imagens…", command=lambda: choose_images(fotos2_var, fotos2_label) ).grid(row=i, column=2, sticky="w", padx=8) ttk.Label( frm, textvariable=fotos2_label, width=14 ).grid(row=i, column=4, sticky="w") def build_tecnicas_window(title: str, data_list: list): win = tk.Toplevel(root) win.title(title) win.geometry("980x520") win.resizable(False, False) container = ttk.Frame(win, padding=16) container.pack(fill="both", expand=True) if not data_list: tecnicas_list = [ "Controlo Visual", "Medição de humidade", "Câmara térmica", "Câmara endoscópica", "Obturação", "Teste de pressão", "Teste com corantes", ] for nome in tecnicas_list: data_list.append({ "name": nome, "var": tk.BooleanVar(value=False), "desc": tk.StringVar(value=""), "photos": tk.Variable(value=[]), "label": tk.StringVar(value=""), }) def set_row_state(row_widgets, enabled: bool): state = "normal" if enabled else "disabled" for w in row_widgets: w.configure(state=state) for idx, item in enumerate(data_list): row = ttk.Frame(container) row.pack(fill="x", pady=6) chk = ttk.Checkbutton( row, text=item["name"], variable=item["var"], command=lambda it=item: set_row_state(it["widgets"], it["var"].get()) ) chk.grid(row=0, column=0, sticky="w") ttk.Label(row, text="Descrição:").grid(row=0, column=1, sticky="w", padx=(12, 4)) entry = ttk.Entry(row, textvariable=item["desc"], width=50) entry.grid(row=0, column=2, sticky="w") btn = ttk.Button( row, text="Escolher fotos…", command=lambda it=item: choose_images(it["photos"], it["label"]) ) btn.grid(row=0, column=3, sticky="w", padx=10) lbl = ttk.Label(row, textvariable=item["label"], width=18) lbl.grid(row=0, column=4, sticky="w") item["widgets"] = [entry, btn] set_row_state(item["widgets"], item["var"].get()) ttk.Button(container, text="Fechar", command=win.destroy).pack(anchor="e", pady=(12, 0)) win.grab_set() win.focus_force() ttk.Button( frm, text="Técnicas utilizadas…", command=lambda: build_tecnicas_window("Técnicas utilizadas", tecnicas_data) ).grid(row=len(fields), column=0, sticky="w", pady=(14, 0)) ttk.Button( frm, text="Início da pesquisa…", command=lambda: build_tecnicas_window("Início da pesquisa", inicio_data) ).grid(row=len(fields), column=1, sticky="w", pady=(14, 0), padx=(12, 0)) presence_frame = ttk.LabelFrame(frm, text="Presencas na pesquisa", padding=12) presence_frame.grid(row=len(fields) + 1, column=0, columnspan=4, sticky="w", pady=(12, 6)) presenca_rows = { "segurado": [], "lesado": [], "outro": [], } type_frames = {} type_labels = { "segurado": "segurado", "lesado": "lesado", "outro": "outro", } def remove_presence_row(tipo: str, row_data: dict): row_data["frame"].destroy() presenca_rows[tipo] = [r for r in presenca_rows[tipo] if r is not row_data] def add_presence_row(tipo: str): container = type_frames[tipo] row = ttk.Frame(container) row.pack(fill="x", pady=3) nome_var = tk.StringVar(value="") info_var = tk.StringVar(value="") ttk.Label(row, text=f"Nome do {type_labels[tipo]}:").grid(row=0, column=0, sticky="w") ttk.Entry(row, textvariable=nome_var, width=30).grid(row=0, column=1, sticky="w", padx=6) ttk.Label(row, text="Declaracoes:").grid(row=0, column=2, sticky="w", padx=(12, 0)) ttk.Entry(row, textvariable=info_var, width=42).grid(row=0, column=3, sticky="w", padx=6) row_data = { "frame": row, "nome": nome_var, "info": info_var, } ttk.Button( row, text="Remover", command=lambda t=tipo, d=row_data: remove_presence_row(t, d) ).grid(row=0, column=4, sticky="w", padx=(8, 0)) presenca_rows[tipo].append(row_data) def build_presence_section(parent, row_index: int, tipo: str, titulo: str): sec = ttk.Frame(parent) sec.grid(row=row_index, column=0, sticky="w", pady=4) ttk.Label(sec, text=titulo).grid(row=0, column=0, sticky="w") ttk.Button( sec, text=f"Adicionar {type_labels[tipo]}", command=lambda t=tipo: add_presence_row(t) ).grid(row=0, column=1, sticky="w", padx=(10, 0)) container = ttk.Frame(sec) container.grid(row=1, column=0, columnspan=2, sticky="w", pady=(6, 0)) type_frames[tipo] = container build_presence_section(presence_frame, 0, "segurado", "Segurados") build_presence_section(presence_frame, 1, "lesado", "Lesados") build_presence_section(presence_frame, 2, "outro", "Outros") add_presence_row("segurado") def on_generate(): values = {k: e.get() for k, e in entries.items()} values["foto1_path"] = foto_var.get() values["fotos2_paths"] = list(fotos2_var.get()) if fotos2_var.get() else [] values["tecnicas"] = [ { "name": t["name"], "selected": t["var"].get(), "desc": t["desc"].get(), "photos": list(t["photos"].get()) if t["photos"].get() else [], } for t in tecnicas_data ] values["inicio_pesquisa"] = [ { "name": t["name"], "selected": t["var"].get(), "desc": t["desc"].get(), "photos": list(t["photos"].get()) if t["photos"].get() else [], } for t in inicio_data ] values["presencas"] = [] for tipo in ("segurado", "lesado", "outro"): for row in presenca_rows[tipo]: values["presencas"].append( { "tipo": tipo, "nome": row["nome"].get(), "info": row["info"].get(), } ) run_generation(values, root) btns = ttk.Frame(frm) btns.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="w", pady=(18, 0)) ttk.Button(btns, text="Gerar Word", command=on_generate).pack(side="left") ttk.Button(btns, text="Sair", command=root.destroy).pack(side="left", padx=10) root.mainloop() if __name__ == "__main__": main()